diff --git a/clone-graph/forest000014.java b/clone-graph/forest000014.java new file mode 100644 index 000000000..60c0a2b25 --- /dev/null +++ b/clone-graph/forest000014.java @@ -0,0 +1,77 @@ +/* +# Time Complexity: O(n * e), where e is the maximum number of edges of a node + - 전체 node를 순회하면서, 그 이웃에 해당하는 복제본을 생성해서 복제본끼리 연결해준다. + - 단, 중복 방문을 막기 위해, 복제본이 이미 이웃 복제본을 가지고 있는지 확인한다. 이 과정에서 O(e)만큼의 List 순회를 한다. + +# Space Complexity: O(n) + - 전체 Node 만큼의 메모리가 필요하다. +(Space Complexity를 계산하기 애매한 측면이 있네요. 저는 지금까지 출력은 space complexity에 포함하지 않고 계산했었는데, 그 이유는 "어떤 알고리즘을 구현하든 출력은 동일하기 때문"인데요. 이 문제의 경우에 출력은 Node 하나이지만, 실제로는 Node 전체만큼의 메모리를 반드시 생성해야 한다는 특수성이 있습니다. 그래서 "어떻게 구현하든 동일하게 사용해야만 하는 메모리는 Space Complexity에서 배제한다" 라는 논리로만 보자면 O(1)일 것 같고, "출력을 제외한 메모리 사용은 Space Complexity에 포함한다" 라는 논리대로라면 O(n)인 것 같습니다.) + + +전체 노드를 DFS로 순회하면서 이웃 노드의 복제본을 생성하여 현재 노드의 복제본과 연결을 맺어줍니다. +다만, 중복 방문을 막기 위해, 복제본이 이미 이웃 복제본을 가지고 있는지 확인한다. +또한 순환 참조(cycle 구조)를 막기 위해서, 복제본 노드를 생성시 단순히 new 키워드를 사용하지 않고, 별도의 map을 통해 싱글톤으로 생성한다. (각 노드의 val은 distinct하다는 점을 이용) + + +// Definition for a Node. +class Node { + public int val; + public List neighbors; + public Node() { + val = 0; + neighbors = new ArrayList(); + } + public Node(int _val) { + val = _val; + neighbors = new ArrayList(); + } + public Node(int _val, ArrayList _neighbors) { + val = _val; + neighbors = _neighbors; + } +} +*/ + +class Solution { + + Map map = new HashMap<>(); + + public Node cloneGraph(Node node) { + if (node == null) { + return null; + } + + Node newNode = createNode(node.val); + dfs(node); + + return newNode; + } + + public Node createNode(int val) { + if (!map.containsKey(val)) { + map.put(val, new Node(val)); + } + return map.get(val); + } + + public void dfs(Node oldNode) { + Node newNode = map.get(oldNode.val); + + for (Node oldNeighbor : oldNode.neighbors) { + boolean hasIt = false; + for (Node newNeighbor : newNode.neighbors) { + if (newNeighbor.val == oldNeighbor.val) { + hasIt = true; + break; + } + } + + if (!hasIt) { + Node newNeighbor = createNode(oldNeighbor.val); + newNode.neighbors.add(newNeighbor); + newNeighbor.neighbors.add(newNode); + dfs(oldNeighbor, newNeighbor); + } + } + } +} diff --git a/longest-common-subsequence/forest000014.java b/longest-common-subsequence/forest000014.java new file mode 100644 index 000000000..674619fe7 --- /dev/null +++ b/longest-common-subsequence/forest000014.java @@ -0,0 +1,51 @@ +/* +# Time Complexity: O(m * n), where m = text1.length(), n = text2.length() +# Space Complexity: O(m * n) + +DP로 접근했습니다. +text1[0..i], text2[0..j]의 LCS의 길이를 lcs[i][j]라고 정의하겠습니다. (0 <= i < m, 0 <= j < n) +lcs[i - 1][j - 1], lcs[i - 1][j], lcs[i][j - 1]을 미리 구해두었다면, lcs[i][j]는 아래처럼 O(1)에 계산할 수 있습니다. + +1. text1[i] != text2[j]인 경우 +lcs[i - 1][j] 를 기준으로 생각해보면, text1[i] != text2[j]이기 때문에, text1[0..i-1]에 text1[i]를 추가한다 해도, LCS의 길이에는 변화가 없습니다. +마찬가지로 lcs[i][j - 1] 을 기준으로, text2[0..j-1]에 text2[j]를 추가해도, LCS의 길이에는 변화가 없습니다. +따라서 lcs[i][j] = max(lcs[i - 1][j], lcs[i][j - 1]) 로 구할 수 있습니다. + +2. text1[i] == text2[j]인 경우 +이 경우에는, lcs[i - 1][j - 1]에서 구한 LCS에 1글자가 추가되므로, lcs[i][j] = lcs[i - 1][j - 1] + 1 로 구할 수 있습니다. + +3. i = 0 혹은 j = 0인 경우의 예외 로직을 추가하면, LCS의 길이를 구하는 2차원 DP를 구현할 수 있습니다. + +*/ + +class Solution { + public int longestCommonSubsequence(String text1, String text2) { + int m = text1.length(); + int n = text2.length(); + int[][] lcs = new int[m][n]; + + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (text1.charAt(i) == text2.charAt(j)) { + if (i == 0 || j == 0) { + lcs[i][j] = 1; + } else { + lcs[i][j] = lcs[i - 1][j - 1] + 1; + } + } else { + if (i == 0 && j == 0) { + lcs[i][j] = 0; + } else if (i == 0) { + lcs[i][j] = lcs[i][j - 1]; + } else if (j == 0) { + lcs[i][j] = lcs[i - 1][j]; + } else { + lcs[i][j] = Math.max(lcs[i][j - 1], lcs[i - 1][j]); + } + } + } + } + + return lcs[m - 1][n - 1]; + } +} diff --git a/longest-repeating-character-replacement/forest000014.java b/longest-repeating-character-replacement/forest000014.java new file mode 100644 index 000000000..0dcf21aaa --- /dev/null +++ b/longest-repeating-character-replacement/forest000014.java @@ -0,0 +1,50 @@ +/* +Time Complexity: O(n) +Space Complexity: O(1) + +투 포인터 방식으로 접근했습니다. +현재 상태에서, 두 포인터 사이의 가장 많은 문자를 제외한 나머지 문자의 개수가 k개 이하면 right를 오른쪽으로 1칸, +k개 초과면 left를 오른쪽으로 1칸씩 이동합니다. + +다만, 개인적으로 에너지가 고갈된 상태에서 풀다보니, +현재 상태에서 가장 많은 문자를 카운트하는 방식을 counts[26] 배열을 순회하는 식으로 단순하게 짰습니다. +PQ를 사용하면 조금 더 시간이 개선될 것 같습니다. +*/ +class Solution { + public int characterReplacement(String s, int k) { + int[] counts = new int[26]; + + int l = 0; + int r = 0; + int mostCount = 1; + + counts[s.charAt(0) - 'A'] = 1; + int ans = 0; + + while (r < s.length()) { + mostCount = 0; + for (int i = 0; i < 26; i++) { + if (counts[i] > mostCount) { + mostCount = counts[i]; + } + } + + if (r - l + 1 - mostCount <= k) { + if (r - l + 1 > ans) { + ans = r - l + 1; + } + + r++; + if (r == s.length()) { + break; + } + counts[s.charAt(r) - 'A']++; + } else { + counts[s.charAt(l) - 'A']--; + l++; + } + } + + return ans; + } +} diff --git a/number-of-1-bits/forest000014.java b/number-of-1-bits/forest000014.java new file mode 100644 index 000000000..0a232ee93 --- /dev/null +++ b/number-of-1-bits/forest000014.java @@ -0,0 +1,88 @@ +/* +solution 1. bit operation +# Time Complexity: O(1) + - n의 사이즈는 32 bit로 일정 + - 2 * 32 회 연산 필요 +# Space Complexity: O(1) + +bit를 하나씩 센다. + + +solution 2. bit operation (advanced) +# Time Complexity: O(1) + - 2 * b 회 연산 필요 (b는 1인 bit의 개수) +# Space Complexity: O(1) + +n &= (n - 1) 연산을 통해, 마지막 bit를 한번에 하나씩 제거하면서 bit를 센다. +1인 bit의 개수만큼 연산을 하므로, 평균적으로 solution 1보다는 연산 횟수가 적다. + + +solution 3. 8-bit chunk lookup table +# Time Complexity: O(n) + - n / 8 회 연산 필요 +# Space Complexity: O(1) + - 2^8 사이즈의 int 배열 사용 + +이진수 00000000 ~ 11111111 에 대해서, 각 수에 bit 1이 몇 개 등장하는지 미리 lookup table에 저장해둔다. +그리고 n을 8 bit 단위로 잘라서, loopup table에서 조회하여 누적해준다. +연산 횟수가 n / 8로 줄어든다는 장점이 있으나, lookup table을 미리 계산하거나 런타임에 계산해야 하고, lookup table 사이즈만큼의 메모리를 더 사용해야 한다는 트레이드 오프가 있다. + + +solution 4. population count 알고리즘 +# Time Complexity: O(1) + - 5 회 연산 필요 +# Space Complexity: O(1) + +각 단계를 진행할 때마다, 2, 4, 8, 16, 32 bit chunk 안의 1 bit의 개수를 센다. + + +solution 5. 자바 내장 함수 Integer.bitCount() 사용 +# Time Complexity: O(1) +# Space Complexity: O(1) + +*/ +class Solution { + // solution 1 + // public int hammingWeight(int n) { + // int ans = 0; + // while (n > 0) { + // ans += (n & 1); + // n >>= 1; + // } + // return ans; + // } + + // solution 2 + // public int hammingWeight(int n) { + // int ans = 0; + // while (n > 0) { + // n &= (n - 1); // 최하위 1비트를 제거 + // ans++; + // } + // return ans; + // } + + // solution 3. + // lookup table (8-bit 단위로) + // 이 아이디어는 시간이 부족해서 구현하지 못했습니다. + + + // solution 4. + // population count 알고리즘 + // https://blog.naver.com/jinhan814/222540111549 + // http://shumin.co.kr/algorithm-hamming-weight-bit-count/ + public int hammingWeight(int n) { + n = (n >> 1 & 0x55555555) + (n & 0x55555555); + n = (n >> 2 & 0x33333333) + (n & 0x33333333); + n = (n >> 4 & 0x0F0F0F0F) + (n & 0x0F0F0F0F); + n = (n >> 8 & 0x00FF00FF) + (n & 0x00FF00FF); + n = (n >> 16 & 0x0000FFFF) + (n & 0x0000FFFF); + return n; + } + + // solution 5. + // 자바 내장 함수 사용 O(logn) + // public int hammingWeight(int n) { + // return Integer.bitCount(n); + // } +} diff --git a/sum-of-two-integers/forest000014.java b/sum-of-two-integers/forest000014.java new file mode 100644 index 000000000..d9359aac2 --- /dev/null +++ b/sum-of-two-integers/forest000014.java @@ -0,0 +1,109 @@ +/* +# Time Complexity: O(n) +# Space Complexity: O(1) + +음...... bit manipulation을 최대한 활용해서 했습니다. +이진법의 덧셈/뺄셈을 손으로 계산한다면, carry/borrow 개념을 활용해서 수행할 텐데, 이 과정을 그대로 코드로 옮겨보았습니다. +++은 increment operator이고, -는 unary minus operator로만 썼으니, 문제의 조건인 +, - operator를 쓰지 말라는 제약사항은 지켰다고 주장하고 싶습니다. +그런데 이렇게 지저분하게 구현하는 것은 출제자의 의도에 부합하지 않는 것 같네요. Dale님의 풀이를 보니 훨씬 간결하던데, 좀 더 공부해봐야 할 것 같습니다. +*/ + +class Solution { + public int getSum(int a, int b) { + if (a >= 0 && b >= 0) { + return sum(a, b); + } else if (a <= 0 && b <= 0) { + return -sum(-a, -b); + } else if (a < 0) { + if (-a >= b) { + return -subtract(-a, b); + } else { + return subtract(b, -a); + } + } else { + if (a >= -b) { + return subtract(a, -b); + } else { + return -subtract(-b, a); + } + } + } + + public int sum(int a, int b) { + int sum = 0; + int carry = 0; + int bit = 0; + int digit = 0; + + while (a > 0 || b > 0) { + if (((a & 1) & (b & 1) & (carry & 1)) == 1) { + carry = 1; + bit = 1; + } else if (((a & 1) | (b & 1) | (carry & 1)) == 1) { + if (((a & 1) ^ (b & 1) ^ (carry & 1)) == 0) { + carry = 1; + bit = 0; + } else { + carry = 0; + bit = 1; + } + } else { + carry = 0; + bit = 0; + } + + sum |= (bit << digit); + + a >>= 1; + b >>= 1; + digit++; + } + + if (carry == 1) { + sum |= (1 << digit); + } + + return sum; + } + + public int subtract(int a, int b) { + int sub = 0; + int borrow = 0; + int bit = 0; + int digit = 0; + + while (a > 0) { + if (borrow == 1) { + if ((a & 1) == (b & 1)) { + borrow = 1; + bit = 1; + } else if ((a & 1) == 1) { + borrow = 0; + bit = 0; + } else { + borrow = 1; + bit = 0; + } + } else { + if ((a & 1) == (b & 1)) { + borrow = 0; + bit = 0; + } else if ((a & 1) == 1) { + borrow = 0; + bit = 1; + } else { + borrow = 1; + bit = 1; + } + } + + sub |= (bit << digit); + + digit++; + a >>= 1; + b >>= 1; + } + + return sub; + } +}