Problem


LRU Cache 를 구현하는 문제입니다.

Least Recently Used (LRU) 란 새로운 데이터를 삽입할 때 사용 빈도수가 낮은 데이터부터 삭제하는 캐싱 기법입니다.



Solution

원래 정석대로 구현하자면 Key - Value 를 갖는 Node 클래스를 만들어서 연결 리스트로 구현하면 되지만 Java 에는 LinkedHashMap 이라는 자료구조가 있습니다.

내부적으로 MapLinkedList 를 사용해서 데이터를 넣은 순서를 지켜줍니다.


그리고 이 LinkedHashMap 은 생성자에서 accessOrder 라는 값을 받습니다.

이 값을 true 로 넘겨주면 접근 빈도에 따라서 순서가 바뀌게 됩니다.


예를 들어 아래 코드에서 a, b 순서로 키값을 넣어서 { a=1, b=2 } 순서로 들어있지만, a 에 접근했더니 순서가 바뀝니다.

이처럼 가장 최근에 접근한 값이 연결 리스트의 마지막으로 이동하게 됩니다.

따라서 가장 사용되지 않은 값은 맨 첫번째에 남게 됩니다.

Map<String, String> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("a", "1");
map.put("b", "2"); // {a=1, b=2}
map.get("a");      // {b=2, a=1}
map.forEach((k, v) -> System.out.print(k + ": " + v + ", ")); // b: 2, a: 1

두번째 코드는 LeetCode 에서만 될 것 같은 방법 LinkedHashMap 자체를 상속받아서 구현했습니다.

removeEldestEntry 는 가장 오래된 Entry 를 삭제하기 때문에 좀더 편하게 구현할 수 있습니다.



Java Code

1) LinkedHashMap 을 클래스 내부에 선언해서 구현

class LRUCache {
    private final Map<Integer, Integer> map;
    private final int capacity;

    public LRUCache(int capacity) {
        map = new LinkedHashMap<>(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return map.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        map.put(key, value);

        if (map.size() > capacity) {
            int leastUsedKey = map.keySet().iterator().next();
            map.remove(leastUsedKey);
        }
    }
}

2) LinkedHashMap 을 상속받아서 구현

class LRUCache extends LinkedHashMap<Integer, Integer> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > capacity;
    }
}

Google Foobar Challenge 후기

Foobar Challenge 는 구글이 숨겨놓은 히든 코딩테스트 입니다.

일반적인 방법으로는 접근할 수 없고 Chrome 검색을 하다보면 화면이 깨지면서 초대 요청이 오는 걸로 알고 있습니다.

저는 우연찮게 좋은 기회가 생겨서 접해볼 수 있었습니다.

사용 가능 언어는 Java 8 과 Python 2.7.13 입니다.

Java 는 그렇다 쳐도 Python 버전이 너무 낮은 것으로 보아 레거시 시스템인 것으로 보이는데..

다른 사람들의 후기를 찾아봤을 때 연락이 오지 않는 경우도 있었다는 걸로 보아 챌린지를 완료 한다고 인터뷰 기회가 주어지는 것은 아닌 것 같습니다.


Problems

Foobar 챌린지에 등장하는 모든 문제는 구글링 하면 찾을 수 있습니다.

보통 코딩 테스트 문제는 기업에서 공개하지 않는 이상 오픈하면 안되는 걸로 알고 있는데..

오픈 마인드인건진 몰라도 문제 전문과 본인 코드까지 올려놓은 사람들이 있더라구요.

문제가 궁금하신 분들은 구글링을 통해 찾으시면 될 것 같아서 간단하게 접근법만 포스팅 하려고 합니다.

문제는 Level 1 부터 시작하며 Level 5 까지 존재합니다.

Level 1: 1 문제
Level 2: 2 문제
Level 3: 3 문제
Level 4: 2 문제
Level 5: 1 문제

Level 1 : Re-ID

소수 (Prime) 를 이어 붙여 만든 "2357111317192329..." 문자열이 존재합니다.

시작 인덱스가 주어지면 해당 인덱스부터 시작하는 5 개의 문자열을 구해야 합니다.

에라토스테네스의 체로 소수를 모두 구해서 이어 붙인 문자열을 만들고 substring 으로 답을 구했습니다.


Level 2 - 1 : En Route Salute

사람들의 이동 방향이 정해지고 만날 때마다 인사를 한다고 할 때 총 몇번의 인사가 이루어지는 지 구하는 문제입니다.

한쪽 방향 사람들만 그룹으로 묶어서 총 몇 번의 인사를 하는지 구한 뒤 2 배 하면 됩니다.


Level 2 - 2 : Power Hungry

주어진 배열의 원소들의 곱 중 가장 큰 값을 구하는 문제입니다.

숫자 크기 때문에 BigInteger 로 풀기 귀찮아서 파이썬으로 풀었습니다.

음수, 양수값을 따로 저장해두고 음수 값이 홀수개면 정렬 후에 마지막 값, 즉 절대값이 가장 작은 값을 제거합니다.

그리고 남은 값들을 전부 곱하면 됩니다.

주의할 점은 두 가지 인데 배열의 길이가 1 이고 홀수인 경우가 존재하기 때문에 예외처리가 필요합니다.

그리고 리턴할 때 문자열로 바꿔서 리턴하는 걸 주의해야 합니다.


Level 3 - 1 : Find the Access Codes

주어진 배열에서 특정 조건을 이루는 3 개 숫자 그룹에 대한 모든 경우의 수를 구하는 문제입니다.

숫자 3 개의 성립 조건을 예로 들면 [a, b, c] 라고 할 때 cb 로 나누어 떨어지고 ba 로 나누어 떨어져야 합니다.

크기가 2000 밖에 되지 않아서 O(n^2) 으로 구했습니다.

배열을 한번 돌면서 한 원소에 대하여 같은 배열에 있는 약수들의 갯수를 구하고 다시 돌면서 특정 원소의 약수의 약수 갯수를 더해주면 됩니다.


Level 3 - 2 : Bomb, Baby!

M, F 폭탄이 각각 1 개씩 있을 때 주어진 Input 만큼의 숫자로 복제하기 위해선 몇 번의 세대가 지나야 하는지 구하는 문제입니다.

한 세대가 지날때마다 M → F 또는 F → M 만큼의 폭탄을 복제할 수 있습니다.

(M, F) 의 다음 세대는 (M + F, F) 또는 (M, M + F) 가 됩니다.

숫자 크기가 10^50 이라서 파이썬으로 풀었습니다.

주어진 input 값부터 시작해서 최종적으로 (1, 1) 이 될 수 있는지를 확인합니다.

단순하게 빼기를 사용하면 시간초과가 나기 때문에 나누기와 나머지 연산을 사용해서 최적화 하면 됩니다.


Level 3 - 3 : Prepare the Bunnies' Escape

벽이 존재하는 2 차원 배열에서 오른쪽 아래에서 왼쪽 위로 이동하는 최단 거리를 구하는 문제입니다.

필요 시 벽을 한번 부술 수 있습니다.

백준의 벽부수고 이동하기 문제와 완전 동일합니다.

BFS 로 풀었습니다.


Level 4 - 1 : Running with Bunnies

영어와 한국어 능력 부족으로 해석에 굉장히 애를 먹은 문제입니다

음수 가중치가 존재하는 그래프에서 제한 시간 내에 시작 노드에서 최대한 많은 다른 노드를 방문하고 도착지에 가야 합니다.

처음 접근한 방법은 Bellman-Ford 알고리즘으로 음수 가중치를 찾고 DFS 를 사용했습니다.

여기서 한가지 간과한 점이 있는데 음수 사이클만 찾으면 된다고 생각했는데 모든 점에서 나머지 점까지의 최단 거리 또한 구해야 합니다.

그래서 플로이드 와샬로 최단 거리를 모두 구한 다음 음수 사이클 여부를 체크하고 DFS 로 답을 구했습니다.


Level 4 - 2 : Free the Bunny Prisoners

문제를 설명하긴 좀 어려운데 Combination 을 사용해서 푸는 문제입니다.


Level 5 : Dodge the Lasers!

문자열 n 이 주어지면 floor(1 * sqrt(2)) + .. + floor(n * sqrt(2)) 를 구하는 문제입니다.

n1 부터 10^100 까지의 범위입니다.

단순히 sqrt 와 루프를 사용하면 시간초과가 납니다.

접근법을 몰라서 인터넷에 있는 풀이를 참고했습니다.

그런데 봐도 무슨 얘긴지 잘 모르겠네요.


챌린지를 마치고

Level 5 까지 제출하고 나면 모든 챌린지가 끝나고 토끼가 뛰어다니는 화면이 나옵니다. (캡쳐는 못했네요)

회사 일도 겸하고 있어서 문제 요청만 해두고 널널하게 생각나면 풀고 그랬었는데 Level 4 까지는 풀만한 문제들이 나온 것 같습니다.

지인들 중에는 Level 3 부터 완전 어려운 문제들이 나온 케이스도 있는 걸로 보아 문제 푸는 시간도 고려해서 난이도가 결정되는 것 같기도 합니다. (순전히 개인적인 추측입니다)

Level 3 문제를 전부 풀면 연락처와 본인 정보를 입력할 수 있는데 서론에서도 언급했듯이 실제로 연락받는 케이스는 드물다고 합니다.

재미있는 경험이었습니다.

Problem

 

문제에서 요구하는 사항에 맞게 RandomizedSet 을 구현하는 문제입니다.

 

insert(val), remove(val), getRandom() 함수를 구현해야 하는데 모두 O(1) 의 시간복잡도를 가져야 합니다.



Solution

ArrayList<Integer>HashMap<Integer, Integer>을 이용하여 만들 수 있습니다.

 

처음에는 HashSet<Integer>을 사용하려고 했는데 getRandom() 함수에서 값을 꺼낼 때 반복문(iterator)을 사용하기 때문에 O(n) 의 시간이 걸렸습니다.

 

그래서 index-value 를 key-value 로 하고 value-index 를 key-value 로 하는 HashMap 두개를 이용해서 풀었습니다.

 

그런데 index-value 가 key-value 인 HashMap 은 결국 List 와 다를게 없어서 List 로 수정하여 최종 구현하였습니다.

 

insert(val)getRandom() 은 별다른 설명이 필요 없을 것 같고 결국 remove(val) 을 어떻게 O(1) 시간에 하느냐가 관건입니다.

 

가장 고민했던 부분은 중간에 있는 val 을 삭제할 경우 List 의 index 에 구멍이 생겨서 랜덤값이 제대로 뽑히지 않는다는 점이었습니다.

 

그래서 중간에 있는 값을 삭제하는 대신에 List 의 가장 마지막 값으로 삭제하는 위치를 채우는 방법을 사용했습니다.

 

다음 그림과 같이 List 값이 들어있다고 가정합니다.

 

 

 

여기서 remove(9) 를 호출하면 두번째 index 값이 지워져야 합니다.

 

하지만 index 값을 지우는 대신에 마지막에 있는 14 값으로 채워넣고 마지막 index 를 삭제 해버리면 list 내의 index 는 순차적으로 모두 존재함이 보장됩니다.

 



Java Code

class RandomizedSet {
    Map<Integer, Integer> map;
    List<Integer> list;

    public RandomizedSet() {
        map = new HashMap<>();
        list = new ArrayList<>();
    }

    public boolean insert(int val) {
        if (map.containsKey(val)) {
            return false;
        }

        map.put(val, list.size());
        list.add(val);
        return true;
    }

    public boolean remove(int val) {
        if (!map.containsKey(val)) {
            return false;
        }

        int lastIndex = list.size() - 1;
        int lastValue = list.get(lastIndex);
        int deletedIndex = map.get(val);

        list.set(deletedIndex, lastValue);
        map.put(lastValue, deletedIndex);

        list.remove(lastIndex);
        map.remove(val);
        return true;
    }

    public int getRandom() {
        int rand = (int) (Math.random() * list.size());
        return list.get(rand);
    }
}

Problem


이진 트리에서 같은 높이에 있는 노드들의 값을 묶는 문제입니다.

왼쪽에서 오른쪽 순서로 넣어야 합니다.



Solution

단순하게 DFS 로 해결하면 됩니다.

값을 담을 list 를 선언하고 왼쪽부터 차례대로 들어갑니다.

level 변수로 높이를 계속 갖고 들어가며 list.size()level 과 같다면 새롭게 만난 높이이기 때문에 리스트를 추가해줍니다.



Java Code

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        traverse(res, root, 0);
        return res;
    }

    private void traverse(List<List<Integer>> list, TreeNode node, int level) {
        if (node == null) return;
        if (level == list.size()) {
            list.add(new ArrayList<>());
        }
        list.get(level).add(node.val);
        traverse(list, node.left, level + 1);
        traverse(list, node.right, level + 1);
    }
}

Problem


n * m 크기의 보드가 주어지고 각 칸에 존재하는 세포가 다음 세대가 되었을 때의 상태를 구하는 문제입니다.



Solution

각 세포는 상하좌우와 대각선까지 해서 총 8 명의 이웃이 있습니다.

문제에 나와있는 세포들의 생존, 사망 조건을 정리하면 다음과 같습니다.

# 살아있는 세포
  - 이웃이 2 명 미만이면 사망
  - 이웃이 3 명 초과면 사망
  - 이웃이 2, 3 명이면 현재 상태 그대로 진행
# 죽은 세포
  - 이웃이 3 명이면 생존
  - 그 외에는 현재 상태 그대로

단순하게 이중 for 문을 돌면서 8 방향 확인하면 됩니다.

주의해야 할 점은 변경된 세포의 상태를 바로 갱신해버리면 옆에 있는 세포의 조건을 검사할 때 꼬일 수 있다는 점입니다.

임시로 2차원 배열을 선언해서 해결할 수도 있지만 Follow up 에서 in-place 로 해결해보라고 나와있습니다.

board 의 값을 바로바로 갱신하되, 0 과 1 대신에 3 과 2 로 갱신해줍니다.

현재 죽음 - 다음 세대에 살아남 ⇒ 3
현재 생존 - 다음 세대에 죽음 ⇒ 2

이렇게 변경해주면 숫자만 보고 현재 상태와 다음 세대를 알 수 있습니다.

모든 검사가 끝나면 %2 연산으로 상태를 갱신해주면 됩니다.



Java Code

class Solution {
    public void gameOfLife(int[][] board) {
        int[] dx = {-1, -1, -1, 0, 1, 1, 1, 0};
        int[] dy = {-1, 0, 1, 1, 1, 0, -1, -1};
        int n = board.length, m = board[0].length;

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                int lives = 0;

                for (int k = 0; k < 8; k++) {
                    int x = i + dx[k];
                    int y = j + dy[k];

                    if (x < 0 || x >= n || y < 0 || y >= m) continue;
                    if (board[x][y] == 1 || board[x][y] == 2) lives++;
                }

                if (board[i][j] == 0 && lives == 3) board[i][j] = 3;
                if (board[i][j] == 1 && (lives < 2 || lives > 3)) board[i][j] = 2;
            }
        }

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                board[i][j] %= 2;
            }
        }
    }
}

Problem


주어진 링크드 리스트를 홀수 번째 노드를 앞으로 짝수 번째 노드를 뒤로 하는 링크드 리스트를 만드는 문제입니다.



Solution

단순하게 반복하면 됩니다.

홀수 번째 노드를 순회하는 odd 와 짝수 번째 노드를 순회하는 even 노드를 선언합니다.

짝수 노드의 헤드는 evenHead 에 따로 저장해두고 oddeven 을 끝까지 이동시킵니다.

그리고 while 문이 끝난 후에는 odd 는 가장 마지막 값이기 때문에 evenHead 를 붙이고 맨 처음 저장된 head 를 리턴하면 됩니다.



Java Code

class Solution {
    public ListNode oddEvenList(ListNode head) {
        if (head == null) return head;

        ListNode odd = head, even = head.next, evenHead = even;

        while (odd.next != null && even.next != null) {
            odd.next = odd.next.next;
            even.next = even.next.next;
            odd = odd.next;
            even = even.next;
        }

        odd.next = evenHead;
        return head;
    }
}

Problem


nums 배열의 중복된 숫자를 찾는 문제입니다.

중복된 숫자는 여러번 나올수 있으며 나머지 숫자들은 전부 한번만 등장합니다.

nums 배열의 길이가 n 이라고 할 때 등장하는 숫자들의 범위는 1 ~ n 입니다.



Solution

토끼와 거북이 알고리즘으로 풀 수 있습니다.

nums 에 있는 값들을 인덱스로 취급해서 계속 이동합니다.

중복된 값이 최소 한개는 존재하기 때문에 반드시 싸이클이 생깁니다.

Input: [1,3,4,2,2]
1 -> 3 -> 2 (싸이클 시작점) -> 4 -> 2 (두번째 2) -> 4 -> 2 -> 4 -> ..

Input: [3,1,3,4,2]
3 (싸이클 시작점) -> 4 -> 2 -> 3 (두번째 3) -> 4 -> 2 -> ...

Input: [1,1]
1 (싸이클 시작점) -> 1 (두번째 1) -> 1 -> ...

Input: [1,1,2]
1 -> 1 -> 1 -> ...

Input: [4,1,2,3,4]
4 -> 4 -> 4 -> ...

토끼와 거북이 알고리즘을 사용하면 싸이클을 찾을 수 있을 뿐만 아니라 싸이클의 시작 지점을 찾을 수 있습니다.

싸이클은 중복된 숫자 인덱스부터 시작되기 때문에 싸이클의 시작 지점 값이 중복 값이 됩니다.



Java Code

class Solution {
    public int findDuplicate(int[] nums) {
        // find cycle
        int slow = nums[0], fast = nums[nums[0]];
        while (slow != fast) {
            slow = nums[slow];
            fast = nums[nums[fast]];
        }

        // find cycle start point        
        int index = 0;
        while (slow != index) {
            index = nums[index];
            slow = nums[slow];
        }

        return index;
    }
}

Problem


String[] strs 값이 주어지면 Anagrams 을 이루는 단어들을 한 리스트로 묶어서 출력하는 문제입니다.

Anagrams 이란 문자의 순서를 바꾸어서 만들 수 있는 다른 문자를 말하며

간단히 생각하면 같은 숫자의 문자들로 이루어진 String 들을 하나로 묶으면 됩니다.

주어지는 값은 모두 소문자이며 출력 순서는 상관 없다는 조건이 있습니다.



Solution

Hash 로 문제를 해결할 수 있습니다.

주어진 문자열이 소문자 알파벳만으로 이루어졌다는 사실을 통해서 해시를 구현할 수 있습니다.

길이 26 의 int 배열을 선언한 뒤에 알파벳 숫자만큼 카운팅 합니다.

예를 들어 bacc 라는 문자열을 받았을 때 이걸 keyArray 로 변환한다면

[1, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

이런 문자열이 됩니다.

accb 도 변환한다면 위와 같은 문자열이 됩니다.

Arrays.toString(int[] arr) 을 통해 배열을 통째로 String 으로 변환할 수 있습니다.

내부적으로 for 문과 StringBuilder 를 사용하기 때문에 시간복잡도는 O(N * K) 가 됩니다.


두 번째 솔루션은 다른 사람들의 Submission Code 를 참고했습니다.

두번째 솔루션과 같은 해시지만 배열 String 을 그대로 키 값으로 사용했던 것과 달리 Long 값을 사용합니다.

26 개의 소수값을 배열에 미리 넣어둔 뒤 알파벳의 갯수만큼 hashKey 값에 곱해줍니다.

이와 같은 풀이가 가능한 이유는 소수의 곱이라 다른 Anagrams 값과 중복될 일이 전혀 없기 때문입니다.

시간복잡도는 동일하게 O(N * K) 지만 Array to String 과정이 없기 때문에 세번째 솔루션이 미세하게 더 빠릅니다.



Java Code

// 1. 배열 자체를 String 으로 만들어서 키값으로 사용
// k 값이 작아서 그런건지 키값 String 길이가 길어서 그런건지 소트보다 속도는 느림
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<>();

        for (String str : strs) {
            int[] count = new int[26];
            for (char c : str.toCharArray()) count[c - 'a']++;
            String key = Arrays.toString(count);

            if (!map.containsKey(key)) {
                map.put(key, new LinkedList<>());
            }

            map.get(key).add(str);
        }

        return new ArrayList<>(map.values());
    }
}


// 2. 소수값을 이용하여 hash key 값을 만듬
class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 
                        23, 29, 31, 37, 41, 43, 47, 53, 
                        59, 61, 67, 71, 73, 79, 83, 89, 
                        97, 101};

        Map<Long, List<String>> map = new HashMap<>();

        for (String str : strs) {
            long hashKey = 1;
            for (char c : str.toCharArray()) {
                hashKey *= (long) primes[c - 'a'];
            }

            if (!map.containsKey(hashKey)) {
                map.put(hashKey, new LinkedList<>());
            }
            map.get(hashKey).add(str);
        }

        return new LinkedList<>(map.values());
    }
}

+ Recent posts