diff --git a/find-minimum-in-rotated-sorted-array/liza0525.py b/find-minimum-in-rotated-sorted-array/liza0525.py new file mode 100644 index 0000000000..cc67e1adb8 --- /dev/null +++ b/find-minimum-in-rotated-sorted-array/liza0525.py @@ -0,0 +1,65 @@ +class Solution: + def findMin(self, nums: List[int]) -> int: + # 선형 탐색(linear scan) 버전 + + # 시간 복잡도: O(n) + # - 배열 전체를 뒤에서 앞으로 한 칸씩 순회하면서 + # "회전이 끊기는 지점"을 찾는다. + # - 최악의 경우 n-1번 비교. + + # 공간 복잡도: O(1) + # - 추가 변수만 사용. + + # 배열 길이가 1개면 그 값 자체가 최솟값 + min_num = 0 + if len(nums) == 1: + return nums[0] + + # 뒤에서 앞으로 탐색하면서 + # nums[i] < nums[i-1] 지점(회전이 일어난 지점)을 찾는다. + # 해당 지점의 nums[i]가 최솟값 + for i in range(len(nums) - 1, -1, -1): + if nums[i] < nums[i - 1]: + min_num = nums[i] + break + + return min_num + + def findMinBinarySearch(self, nums: List[int]) -> int: + # 이진 탐색(binary search) 버전 + + # 시간 복잡도: O(log n) + # - 정렬된 배열이 한 번 회전(rotated)된 형태를 이용해 + # 절반씩 탐색 범위를 줄인다. + # - mid 기준 왼쪽/오른쪽 어느 쪽이 정렬 상태인지에 따라 탐색 방향 결정 + + # 공간 복잡도: O(1) + # - 포인터(l, h, mid)만 사용 + + # l은 1부터 시작하는데, + # mid-1 비교를 안전하게 하기 위함이다. + l, h = 1, len(nums) - 1 + + while l <= h: + mid = (l + h) // 2 + + # mid가 최솟값이 되는 전형적인 조건: + # mid 바로 이전 요소가 mid보다 크면 회전 지점 + if nums[mid - 1] > nums[mid]: + return nums[mid] + + # nums[0] < nums[mid] -> 배열 시작점부터 mid까지는 정렬된 상태 + # → 회전 지점은 오른쪽에 있음 -> l을 오른쪽으로 이동 + if nums[0] < nums[mid]: + l = mid + 1 + else: + # 그 외의 경우 회전 지점은 왼쪽 구간에 존재 + h = mid - 1 + + # 회전이 아예 없는 경우(완전 정렬 상태) + return nums[0] + + # def findMin(self, nums: List[int]) -> int: + # # 파이썬의 내장함수인 min을 이용해도 문제 풀이 가능 + # # https://wiki.python.org/moin/TimeComplexity애 의하면 min 함수도 O(n)임 + # return min(nums) diff --git a/maximum-depth-of-binary-tree/liza0525.py b/maximum-depth-of-binary-tree/liza0525.py new file mode 100644 index 0000000000..423ca7731d --- /dev/null +++ b/maximum-depth-of-binary-tree/liza0525.py @@ -0,0 +1,38 @@ +# 시간 복잡도: O(n) +# - 트리의 모든 노드를 한 번씩 방문하면서 깊이 계산 +# - 각 노드는 정확히 한 번만 처리되므로 전체 노드 수 n에 비례 + +# 공간 복잡도: O(h) +# - 재귀 호출 스택의 최대 깊이는 트리의 높이(h)만큼 쌓임 +# - 균형 트리면 O(log n), 일자로 치우친 트리(skewed tree)면 O(n)까지 갈 수 있다. + + + +# 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: + # 현재 노드(cur_node)를 기준으로 왼쪽/오른쪽 서브트리의 최대 깊이를 구하고 + # 그 중 큰 값을 반환하는 재귀 함수을 사용 + def maxDepth(self, root: Optional[TreeNode]) -> int: + # depth는 지금까지 내려온 깊이를 의미 + def find_max_depth(cur_node, depth): + # 더 내려갈 노드가 없다면(leaf의 자식) 지금까지의 depth가 해당 경로의 깊이 + if not cur_node: + return depth + + # 왼쪽으로 한 단계 내려가면서 깊이를 증가시켜 탐색 + left_depth = find_max_depth(cur_node.left, depth + 1) + # 오른쪽도 동일하게 탐색 + right_depth = find_max_depth(cur_node.right, depth + 1) + + # 왼쪽/오른쪽 중 더 깊은 쪽이 이 노드 기준 최대 깊이 + return max(left_depth, right_depth) + + # 루트에서 시작하며 깊이는 0부터 시작 + max_depth = find_max_depth(root, 0) + + return max_depth diff --git a/merge-two-sorted-lists/liza0525.py b/merge-two-sorted-lists/liza0525.py new file mode 100644 index 0000000000..e47564eca5 --- /dev/null +++ b/merge-two-sorted-lists/liza0525.py @@ -0,0 +1,43 @@ +# 시간 복잡도: O(m + n) +# - 두 연결 리스트(list1, list2)를 각각 한 번씩만 순회하면 된다. +# - 각 노드를 정확히 한 번씩만 비교·이동하므로 전체 길이에 선형 시간. + +# 공간 복잡도: O(1) +# - 새로운 노드를 생성해 리스트를 복제하지 않고, +# 기존 리스트 노드들을 그대로 이어붙이기 때문에 추가 메모리는 거의 없다. +# - 연결 작업을 위한 dummy 노드 1개만 사용. + + + +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next +class Solution: + def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: + # 결과 리스트의 첫 지점을 쉽게 관리하기 위해 더미(Dummy) 노드를 만든다. + root_node = ListNode() + cur_node = root_node # 현재 결과 리스트를 이어가는 포인터 + + # 두 리스트가 모두 남아있는 동안 반복한다. + # - 각 리스트의 head를 비교해 더 작은 쪽을 결과 리스트에 붙인다. + while list1 and list2: + if list1.val < list2.val: + # list1의 노드를 결과 리스트에 연결하고 list1 포인터를 다음으로 이동 + cur_node.next = list1 + list1 = list1.next + else: + # list2의 노드를 결과 리스트에 연결하고 list2 포인터를 다음으로 이동 + cur_node.next = list2 + list2 = list2.next + + # 결과 리스트 포인터도 한 칸 전진 + cur_node = cur_node.next + + # 어느 한쪽이 끝났다면, 남아 있는 리스트 전체를 그대로 이어붙인다. + # - 이미 정렬되어 있으므로 추가 비교 없이 한 번에 연결 가능 + cur_node.next = list1 or list2 + + # dummy 노드 다음부터가 실제 머지된 리스트의 시작점 + return root_node.next diff --git a/word-search/liza0525.py b/word-search/liza0525.py new file mode 100644 index 0000000000..8db031f723 --- /dev/null +++ b/word-search/liza0525.py @@ -0,0 +1,67 @@ +# 시간 복잡도: O(m · n · 4^k) +# - 보드의 모든 칸(m·n)을 시작점으로 고려하고 +# - 각 칸에서 단어 길이 k만큼 DFS 탐색을 수행한다. +# - 각 단계마다 최대 4방향으로 뻗을 수 있기 때문에 지수적 탐색 구조를 가진다. +# (다만 visits로 중복 방문을 막아 실제 탐색량은 줄어든다.) + +# 공간 복잡도: O(m · n) +# - 방문 여부를 저장하는 2차원 배열 때문에 m·n의 공간이 필요하다. +# - DFS 재귀 깊이는 최대 단어 길이 k까지 깊어질 수 있어 O(k) 스택을 추가로 사용하지만, +# 전체 공간에서 보면 visits가 차지하는 비중이 더 크다. + + +class Solution: + def exist(self, board: List[List[str]], word: str) -> bool: + # 보드의 행(row)과 열(column) 개수를 가져온다. + m = len(board) + n = len(board[0]) + + # 전체 칸 수보다 단어 길이가 길다면 애초에 만들 수 없으니 바로 False + if len(word) > m * n: + return False + + # 방문 여부를 기록할 2차원 배열 생성 + # - 같은 칸은 다시 사용하지 못하므로 DFS에서 체크해야 한다. + visits = [[False for _ in range(n)] for _ in range(m)] + + # DFS 함수: (i, j)에서 word[str_index] 문자를 만족시키는지 재귀적으로 탐색한다. + def dfs(i, j, str_index): + # 범위 벗어나는지, 이미 방문한 칸인지, 현재 문자가 일치하는지 확인 + if not ( + 0 <= i < m + and 0 <= j < n + and str_index < len(word) + and not visits[i][j] + and board[i][j] == word[str_index] + ): + return + + # 현재 문자가 마지막 문자라면 단어를 모두 찾은 것 + if str_index == len(word) - 1: + return True + + # 현재 칸 방문 처리 + visits[i][j] = True + + # 상하좌우 네 방향으로 다음 문자를 찾으러 이동 + # - 하나라도 성공하면 그대로 True를 반환 + if ( + dfs(i + 1, j, str_index + 1) + or dfs(i - 1, j, str_index + 1) + or dfs(i, j + 1, str_index + 1) + or dfs(i, j - 1, str_index + 1) + ): + return True + + # 모든 방향 실패 시 백트래킹: 방문 기록을 원복해줘야 다른 경로에서 다시 사용할 수 있다. + visits[i][j] = False + return False + + # 모든 칸을 단어의 시작점으로 삼아 DFS 시도 + for start_i in range(m): + for start_j in range(n): + if dfs(start_i, start_j, 0): + return True + + # 모든 시도를 해도 못 찾으면 False + return False