diff --git a/alien-dictionary/EGON.py b/alien-dictionary/EGON.py new file mode 100644 index 000000000..6ca647020 --- /dev/null +++ b/alien-dictionary/EGON.py @@ -0,0 +1,106 @@ +from collections import deque +from typing import List +from unittest import TestCase, main + + +class Solution: + def foreignDictionary(self, words: List[str]) -> str: + return self.solve_topological_sort(words) + + """ + LintCode 로그인이 안되어서 https://neetcode.io/problems/foreign-dictionary 에서 실행시키고 통과만 확인했습니다. + + Runtime: ? ms (Beats ?%) + Time Complexity: + #0. 복잡도 변수 정의 + - words 배열의 길이를 n + - words 배열을 이루는 단어들의 평균 길이를 l + - words 배열을 이루는 단어를 이루는 문자들의 총 갯수를 c (= n * l) + - words 배열을 이루는 단어를 이루는 문자들의 중복 제거 집합의 크기를 s라 하자 + + #1. 초기화 + - words 배열을 이루는 단어를 이루는 문자들을 조회하며 char_set을 초기화하는데 O(c) + - 위상정렬에 사용할 graph 딕셔너리 초기화를 위해 char_set의 크기만큼 조회하므로 O(s) + - 마찬가지로 위상정렬에 사용할 rank 딕셔너리 초기화에 O(s) + > O(c) + O(s) + O(s) ~= O(c + s) + + #2. 위상정렬 간선 초기화 + - words 배열을 조회하는데 O(n - 1) + - 단어 간 접두사 관계인 경우, 체크하는 startswith 메서드 사용에 * O(l) + - 단어 간 접두사 관계가 아닌 경우, first_char, second_char를 구하는데 + - zip 생성에 O(l) + - zip 조회에 * O(l) + > O(n - 1) * (O(l) + O(l) * O(l)) ~= O(n) * O(l ^ 2) ~= O(c * l) ~= O(c) + + #3. 위상정렬 실행 + - dq 초기화에 rank 딕셔너리의 모든 키를 조회하는데 O(s) + - dq를 이용해서 graph 딕셔너리의 모든 values를 순회하는데, #2에서 각 first_char, second_char마다 1회 value가 추가되었으므로, 중복이 없는 경우 최대 O(n), upper bound + > O(s) + O(n) ~= O(s + n), upper bound + + #4. 최종 계산 + > O(c + s) + O(c) + O(s + n) ~= O(c + s) + O(s + n) = O(n * l + s) + O(n + s) ~= O(n * l + s), upper bound + + Memory: ? MB (Beats ?%) + Space Complexity: O(s + c) + - char_set의 크기에서 O(s) + - graph의 keys는 최대 s개이고 values는 최대 c개이므로 O(s + c), upper bound + - rank의 keys의 크기에서 O(s) + - dq의 최대 크기는 rank의 크기와 같으므로 O(s) + > O(s) + O(s + c) + O(s) + O(s) ~= O(s + c) + """ + def solve_topological_sort(self, words: List[str]) -> str: + if not words: + return "" + + char_set = set([char for word in words for char in word]) + graph = {char: set() for char in char_set} + rank = {char: 0 for char in char_set} + for i in range(len(words) - 1): + first_word, second_word = words[i], words[i + 1] + + if len(first_word) > len(second_word) and first_word.startswith(second_word): + return "" + + first_char, second_char = next(((fc, sc) for fc, sc in zip(first_word, second_word) if fc != sc), ("", "")) + if (first_char and second_char) and second_char not in graph[first_char]: + graph[first_char].add(second_char) + rank[second_char] += 1 + + result = [] + dq = deque([char for char in rank if rank[char] == 0]) + while dq: + curr_char = dq.popleft() + result.append(curr_char) + for post_char in graph[curr_char]: + rank[post_char] -= 1 + if rank[post_char] == 0: + dq.append(post_char) + + if len(result) != len(rank): + return "" + else: + return "".join(result) + + +class _LeetCodeTestCases(TestCase): + def test_1(self): + words = ["z","o"] + output = "zo" + solution = Solution() + self.assertEqual(solution.foreignDictionary(words), output) + + def test_2(self): + words = ["hrn","hrf","er","enn","rfnn"] + output = "hernf" + solution = Solution() + self.assertEqual(solution.foreignDictionary(words), output) + + def test_3(self): + words = ["wrt","wrf","er","ett","rftt","te"] + output = "wertf" + solution = Solution() + self.assertEqual(solution.foreignDictionary(words), output) + + +if __name__ == '__main__': + main() diff --git a/longest-palindromic-substring/EGON.py b/longest-palindromic-substring/EGON.py new file mode 100644 index 000000000..a288e3966 --- /dev/null +++ b/longest-palindromic-substring/EGON.py @@ -0,0 +1,126 @@ +from unittest import TestCase, main + + +class Solution: + def longestPalindrome(self, s: str) -> str: + return self.solve_manacher_algorithm(s) + + """ + Runtime: 47 ms (Beats 96.97%) + Time Complexity: O(n ^ 3) + - s의 길이를 n이라 하면, s의 길이 - 1 만큼 조회하는데 O(n - 1) + - 각 문자마다 sliding_window를 2회 호출하는데, 각 호출마다 최대 s의 길이만큼 반복하므로, * 2 * O(n), upper bound + - 반복 후 s를 slicing하는데 최대 * O(n), upper bound + > O(n - 1) * (2 * O(n)) * O(n) ~= O(n ^ 3) + + Memory: 16.54 MB (Beats 88.85%) + Space Complexity: O(n) + - sliding_window의 결과로 생성되는 문자열의 최대 길이는 n이고, 조회마다 2회 생성되므로 2 * O(n), upper bound + > 2 * O(n) ~= O(n) + """ + def solve_sliding_window(self, s: str) -> str: + + def sliding_window(left: int, right: int) -> str: + while 0 <= left and right < len(s) and s[left] == s[right - 1]: + left -= 1 + right += 1 + + return s[left + 1: right - 1] + + if len(s) < 2 or s == s[::-1]: + return s + + result = '' + for i in range(len(s) - 1): + result = max(result, sliding_window(i, i + 1), sliding_window(i, i + 2), key=len) + + return result + + """ + Runtime: 36 ms (Beats 98.09%) + Time Complexity: O(n ^ 2) + - s의 길이를 n이라 하면, s의 길이 - 1 만큼 조회하는데 O(n - 1) + - 각 문자마다 two_pointer 2회 호출하는데, 각 호출마다 최대 s의 길이만큼 반복하므로, * 2 * O(n), upper bound + > O(n - 1) * (2 * O(n)) ~= O(n ^ 2) + + Memory: 16.85 MB (Beats 24.42%) + Space Complexity: O(1) + > 모든 변수는 result를 제외하고 인덱스를 위한 정수 변수만 사용하므로 O(1) + """ + def solve_two_pointer(self, s: str) -> str: + + if len(s) < 2 or s == s[::-1]: + return s + + def two_pointer(left: int, right: int) -> (int, int): + while left >= 0 and right < len(s) and s[left] == s[right]: + left -= 1 + right += 1 + + return left + 1, right - 1 + + start, end = 0, 0 + for i in range(len(s) - 1): + first_left, first_right = two_pointer(i, i) + second_left, second_right = two_pointer(i, i + 1) + + if first_right - first_left > end - start: + start, end = first_left, first_right + if second_right - second_left > end - start: + start, end = second_left, second_right + + return s[start: end + 1] + + """ + Time Complexity: O(n) + Space Complexity: O(n) + """ + def solve_manacher_algorithm(self, s: str) -> str: + SEPARATOR = '@' + # Step 1: Transform the string + t = SEPARATOR + SEPARATOR.join(s) + SEPARATOR + n = len(t) + p = [0] * n + center = right = 0 # Center and right boundary + max_length = 0 + max_center = 0 + + # Step 2: Calculate palindrome radius for each character + for c in range(n): + # Use previously calculated information (symmetry) + if c < right: + p[c] = min(p[2 * center - c], right - c) + + # Try to expand around i + while (0 <= c - p[c] - 1 and c + p[c] + 1 < n) and (t[c - p[c] - 1] == t[c + p[c] + 1]): + p[c] += 1 + + # Update center and right boundary if expanded beyond current right + if c + p[c] > right: + center = c + right = c + p[c] + + # Update max palindrome length and center + if p[c] > max_length: + max_length = p[c] + max_center = c + + # Step 3: Extract the original string's palindrome + start = (max_center - max_length) // 2 + return s[start:start + max_length] + + +class _LeetCodeTestCases(TestCase): + def test_1(self): + s = "babad" + output = "bab" + self.assertEqual(Solution().longestPalindrome(s), output) + + def test_2(self): + s = "cbbd" + output = "bb" + self.assertEqual(Solution().longestPalindrome(s), output) + + +if __name__ == '__main__': + main() diff --git a/rotate-image/EGON.py b/rotate-image/EGON.py new file mode 100644 index 000000000..a81f6faed --- /dev/null +++ b/rotate-image/EGON.py @@ -0,0 +1,47 @@ +from typing import List +from unittest import TestCase, main + + +class Solution: + def rotate(self, matrix: List[List[int]]) -> None: + return self.solve(matrix) + + """ + Runtime: 0 ms (Beats 100.00%) + Time Complexity: O(n ^ 2) + - 행렬의 행과 열을 교환하기 위해 이중 for문 사용에 O(n ^ 2) + - 행렬의 각 행을 뒤집기 위해, 행을 조회하는데 O(n) + - 각 행을 뒤집는데 * O(n) + > O(n ^ 2) + O(n) * O(n) ~= O(n ^ 2) + O(n ^ 2) ~= O(n ^ 2) + + Memory: 16.76 MB (Beats 14.84%) + Space Complexity: O(1) + > in-place 풀이이므로 상수 변수 할당을 제외한 메모리 사용 없음, O(1) + """ + def solve(self, matrix: List[List[int]]) -> None: + N = len(matrix) + + for i in range(N): + for j in range(i, N): + matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] + + for row in matrix: + row.reverse() + + +class _LeetCodeTestCases(TestCase): + def test_1(self): + matrix = [[1,2,3],[4,5,6],[7,8,9]] + output = [[7,4,1],[8,5,2],[9,6,3]] + Solution().rotate(matrix) + self.assertEqual(matrix, output) + + def test_2(self): + matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]] + output = [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] + Solution().rotate(matrix) + self.assertEqual(matrix, output) + + +if __name__ == '__main__': + main() diff --git a/subtree-of-another-tree/EGON.py b/subtree-of-another-tree/EGON.py new file mode 100644 index 000000000..bce47da53 --- /dev/null +++ b/subtree-of-another-tree/EGON.py @@ -0,0 +1,116 @@ +from typing import Optional +from unittest import TestCase, main + + +# Definition for a binary tree node. +class TreeNode: + def __init__(self, val=0, left=None, right=None): + self.val = val + self.left = left + self.right = right + + +class Solution: + def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool: + return self.solve_dfs(root, subRoot) + + """ + Runtime: 35 ms (Beats 90.24%) + Time Complexity: O(n) + - root 트리의 크기를 n이라 하면, root 트리의 모든 노드를 조회하는데 O(n) + - 각 노드마다 is_same_tree 실행하는데, subRoot 트리의 크기를 m이라 하면, 최대 subRoot의 노드의 크기만큼 조회하므로 * O(m) + > O(n) * O(m) ~= O(n * m) + + Memory: 17.09 (Beats 9.93%) + Space Complexity: O(n + m) + - stack의 최대 크기는 root 트리가 편향된 경우이며, 이는 root 트리의 노드의 총 갯수와 같으므로 O(n), upper bound + - is_same_tree 함수의 재귀 스택의 최대 깊이는 subRoot 트리가 편향된 경우이며, 이는 subRoot 트리의 노드의 총 갯수와 같으므로 O(m), upper bound + > O(n) + O(m) ~= O(n + m) + """ + def solve_dfs(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool: + + def is_same_tree(p: Optional[TreeNode], q: Optional[TreeNode]) -> bool: + if p is None and q is None: + return True + elif (p is not None and q is not None) and (p.val == q.val): + return is_same_tree(p.left, q.left) and is_same_tree(p.right, q.right) + else: + return False + + result = False + stack = [root] + while stack: + curr = stack.pop() + if (curr and subRoot) and curr.val == subRoot.val: + result = result or is_same_tree(curr, subRoot) + + if curr.left: + stack.append(curr.left) + + if curr.right: + stack.append(curr.right) + + return result + + +class _LeetCodeTestCases(TestCase): + def test_1(self): + root = TreeNode(3) + root_1 = TreeNode(4) + root_2 = TreeNode(5) + root.left = root_1 + root.right = root_2 + root_3 = TreeNode(1) + root_4 = TreeNode(2) + root.left.left = root_3 + root.left.right = root_4 + + subRoot = TreeNode(4) + sub_1 = TreeNode(1) + sub_2 = TreeNode(2) + subRoot.left = sub_1 + subRoot.right = sub_2 + + output = True + self.assertEqual(Solution.isSubtree(Solution(), root, subRoot), output) + + def test_2(self): + root = TreeNode(3) + root_1 = TreeNode(4) + root_2 = TreeNode(5) + root.left = root_1 + root.right = root_2 + root_3 = TreeNode(1) + root_4 = TreeNode(2) + root.left.left = root_3 + root.left.right = root_4 + root_5 = TreeNode(0) + root_4.left = root_5 + + subRoot = TreeNode(4) + sub_1 = TreeNode(1) + sub_2 = TreeNode(2) + subRoot.left = sub_1 + subRoot.right = sub_2 + + output = False + self.assertEqual(Solution.isSubtree(Solution(), root, subRoot), output) + + def test_3(self): + root = TreeNode(1) + root.right = TreeNode(1) + root.right.right = TreeNode(1) + root.right.right.right = TreeNode(1) + root.right.right.right.right = TreeNode(1) + root.right.right.right.right.right = TreeNode(2) + + subRoot = TreeNode(1) + subRoot.right = TreeNode(1) + subRoot.right.right = TreeNode(2) + + output = True + self.assertEqual(Solution.isSubtree(Solution(), root, subRoot), output) + + +if __name__ == '__main__': + main() diff --git a/validate-binary-search-tree/EGON.py b/validate-binary-search-tree/EGON.py new file mode 100644 index 000000000..1d646a47b --- /dev/null +++ b/validate-binary-search-tree/EGON.py @@ -0,0 +1,63 @@ +from typing import Optional +from unittest import TestCase, main + + +# Definition for a binary tree node. +class TreeNode: + def __init__(self, val=0, left=None, right=None): + self.val = val + self.left = left + self.right = right + + +class Solution: + def isValidBST(self, root: Optional[TreeNode]) -> bool: + return self.solve_dfs(root) + + """ + Runtime: 0 ms (Beats 100.00%) + Time Complexity: O(n) + > root 트리의 노드의 갯수를 n이라 하면, 모든 node를 조회하므로 O(n) + + Memory: 18.91 MB (Beats 5.70%) + Space Complexity: O(n) + > root 트리의 높이만큼 is_valid_bst 함수가 재귀 호출 스택에 쌓이나, 문제의 제약조건에서 이진트리라고만 했으므로 편향될 수 있으므로, O(n), upper bound + """ + def solve_dfs(self, root: Optional[TreeNode]) -> bool: + + def is_valid_bst(node: Optional[TreeNode], lower_bound=float('-inf'), upper_bound=float('inf')): + if not node: + return True + + if lower_bound < node.val < upper_bound: + return is_valid_bst(node.left, lower_bound, node.val) and is_valid_bst(node.right, node.val, upper_bound) + else: + return False + + return is_valid_bst(root) + + +class _LeetCodeTestCases(TestCase): + def test_1(self): + root = TreeNode(2) + root.left = TreeNode(1) + root.right = TreeNode(3) + + output = True + + self.assertEqual(Solution().isValidBST(root), output) + + def test_2(self): + root = TreeNode(5) + root.left = TreeNode(4) + root.right = TreeNode(6) + root.right.left = TreeNode(3) + root.right.right = TreeNode(7) + + output = False + + self.assertEqual(Solution().isValidBST(root), output) + + +if __name__ == '__main__': + main()