diff --git a/find-median-from-data-stream/liza0525.py b/find-median-from-data-stream/liza0525.py new file mode 100644 index 0000000000..54a3b8995e --- /dev/null +++ b/find-median-from-data-stream/liza0525.py @@ -0,0 +1,62 @@ +import heapq + +# 7기 풀이 +class MedianFinder: + # 공간 복잡도: O(n) + # - 각각의 힙은 추가되는 수의 개수의 공간을 사용 + def __init__(self): + self.pre_list = [] # 앞쪽 구간, 최대 힙으로 + self.post_list = [] # 뒤쪽 구간, 최소 힙으로 + + # 시간 복잡도: O(log n) + # - 힙을 사용하여 앞쪽 구간 및 뒤쪽 구간에 새로운 원소를 넣기 때문에, 메소드 호출 때마다 각 배열의 크기에 시간 복잡도가 정해진다. + # 공간 복잡도: O(1) + # - 해당 메소드에서는 클래스 멤버 변수 사용하는 것 이외에는 몇 개의 변수만 이용 + def addNum(self, num: int) -> None: + if not self.pre_list or num < -self.pre_list[0]: + # 앞쪽 구간에 값을 넣을 경우 + # num이 pre_list의 최댓값보다 작을 때 pre_list에 heappush를 한다. + # 최대힙 구현을 하기 위해 마이너스 값으로 넣는다. + heapq.heappush(self.pre_list, -num) + else: + # 뒤쪽 구간에 값을 넣을 경우 + # num이 post_list의 최솟값보다 클 때 post_list에 heappush를 한다. + heapq.heappush(self.post_list, num) + + pre_len, post_len = len(self.pre_list), len(self.post_list) + + # 힙의 크기 차이가 2 이상 날 때는 중간값 조정을 해주기 위해 요소를 다른 쪽으로 옮겨준다. + if pre_len == post_len + 2: + # pre_list에 요소가 몰린 경우이므로, + # pre_list의 최댓값을 pop한 후 post_list의 최솟값으로 push한다. + heapq.heappush( + self.post_list, + -heapq.heappop(self.pre_list) + ) + elif post_len == pre_len + 2: + # post_list에 요소가 몰린 경우이므로, + # post_list의 최솟값을 pop한 후 pre_list의 최댓값으로 push한다. + heapq.heappush( + self.pre_list, + -heapq.heappop(self.post_list) + ) + + # 시간 복잡도: O(1) + # - 각 힙에서의 최솟값 또는 최대값에 접근하는 정도 + 간단한 중간값 계산 + # 공간 복잡도: O(1) + # - 해당 메소드에서는 클래스 멤버 변수 사용하는 것 이외에는 몇 개의 변수만 이용 + def findMedian(self) -> float: + pre_len, post_len = len(self.pre_list), len(self.post_list) + + if pre_len > post_len: + # pre_list의 길이가 더 큰 경우에는 중간값이 pre_list에 있으므로 + # pre_list의 최댓값을 return + return -self.pre_list[0] + elif pre_len < post_len: + # post_list의 길이가 더 큰 경우에는 중간값이 post_list에 있으므로 + # post_list의 최솟값을 return + return self.post_list[0] + else: + # 두 힙의 길이가 같은 경우에는 + # pre_list의 최댓값과 post_list의 최솟값의 평균값을 return + return (-self.pre_list[0] + self.post_list[0]) / 2 diff --git a/insert-interval/liza0525.py b/insert-interval/liza0525.py new file mode 100644 index 0000000000..44afeae444 --- /dev/null +++ b/insert-interval/liza0525.py @@ -0,0 +1,34 @@ +# 7기 풀이 +# 시간 복잡도: O(n) +# - intervals의 모든 요소를 접근하므로 intervals의 길이(n)만큼 시간 소요 +# 공간 복잡도: O(1) +# - 결과 변수인 res를 제외하면, 몇 가지 변수 이외에 사용하지 않음 +class Solution: + def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]: + res = [newInterval] # newInterval을 맨 처음에 넣어줌 + + for curr_start, curr_end in intervals: # intervals를 돌며 현재의 구간을 curr_start, curr_end로 지정 + prev_start, prev_end = res[-1] # 이전 구간은 res의 마지막 요소인 prev_start, prev_end로 지정 + if curr_start <= prev_end: + # 1. curr_start가 prev_end와 작거나 같다는 것은 + # 다음의 두 케이스로 나뉘어진다.(자세한 설명은 각 조건 내에 상술) + if curr_end < prev_start: + # 1-1. curr 구간이 prev 구간보다 아예 앞서는 경우 + # 이 경우에는 구간의 순서를 변경해줘야 하므로 + # res[-1]을 curr 요소로 변경해준 후 + # prev 구간을 다시 append해준다. + res[-1][0], res[-1][1] = curr_start, curr_end + res.append([prev_start, prev_end]) + else: + # 1-2. curr 구간과 prev 구간이 겹치는 경우 + # 이 경우에는 start끼리 비교하여 작은 수를 res[-1]의 start로 + # end 끼리 비교하여 큰 수를 res[-1]의 end 값으로 재할당 + res[-1][0] = min(curr_start, prev_start) + res[-1][1] = max(curr_end, prev_end) + else: + # 2. curr_start가 prev_end보다 큰 경우에는 + # 구간의 순서를 바꾸거나 겹치는 구간을 merge할 필요가 없으므로 + # res에 현재 구간을 append해준다. + res.append([curr_start, curr_end]) + + return res diff --git a/kth-smallest-element-in-a-bst/liza0525.py b/kth-smallest-element-in-a-bst/liza0525.py new file mode 100644 index 0000000000..5e83ab238a --- /dev/null +++ b/kth-smallest-element-in-a-bst/liza0525.py @@ -0,0 +1,51 @@ +# 7기 풀이 +# 풀이 1, 2 모두 중위 순회를 하면 BST를 오름차순으로 순회할 수 있다는 특징을 이용해 풀이 +class Solution: + # 풀이 1 + # 시간 복잡도: O(n) + # - 최악의 경우에는 모든 노드를 탐색하는 경우이므로, 노드의 개수(n)만큼의 시간 소요 + # 공간 복잡도: O(h) + # - 재귀 스택에 트리의 깊이(h) 만큼 사용됨 + def kthSmallest(self, root: Optional[TreeNode], k: int) -> int: + res = -1 # 결과를 저장할 변수 + + def inorder(node): + # k와 res는 inorder 함수 외부의 변수들이므로, + # nonlocal 선언하여 접근할 수 있게 변경 + nonlocal k + nonlocal res + + if not node: + # 노드가 None인 경우는 재귀 탐색을 끝내고 return + return + + inorder(node.left) # 왼쪽 자식 노드를 재귀적으로 먼저 탐색 + k -= 1 # 현재 노드 탐색에서 k를 하나 줄임 + if k == 0: # k가 0이 되는 순간이 전체 노드 중에 k번째 값이 됨 + res = node.val # res에 node의 값을 할당 + return # 더이상 탐색할 필요가 없으므로 return + inorder(node.right) # 오른쪽 자식 노드를 재귀적으로 나중에 탐색 + + inorder(root) + return res + + # 풀이 2 + # 시간 복잡도: O(n) + # - 모든 노드를 탐색하기 때문에 노드의 개수(n)만큼의 시간 소요 + # 공간 복잡도: O(n) + # - 모든 노드의 값을 res_list에 저장하기 때문에 노드의 개수(n)만큼의 공간 차지 + def kthSmallest(self, root: Optional[TreeNode], k: int) -> int: + res_list = [] + + def inorder(node): + if not node: + # 노드가 없을 땐 return + return + + inorder(node.left) # 왼쪽 자식 노드를 재귀적으로 먼저 탐색 + res_list.append(node.val) # 현재 노드를 res_list에 삽입 + inorder(node.right) # 오른쪽 자식 노드를 재귀적으로 나중에 탐색 + + + inorder(root) # 중위 탐색 시작 + return res_list[k - 1] # 탐색한 결과 중 k번째 요소 return diff --git a/lowest-common-ancestor-of-a-binary-search-tree/liza0525.py b/lowest-common-ancestor-of-a-binary-search-tree/liza0525.py new file mode 100644 index 0000000000..e4d00346c4 --- /dev/null +++ b/lowest-common-ancestor-of-a-binary-search-tree/liza0525.py @@ -0,0 +1,37 @@ +# 7기 풀이 +# 시간 복잡도: O(n) +# - balanced tree라면 O(log n)이 되지만, 편향 트리가 worst case이므로 +# - 이때는 모든 노드 탐색, 노드 개수(n)에 따라 시간 복잡도가 결정된다. +# - balanced tree의 경우: 매 노드 탐색 때마다 한 쪽 서브 트리를 선택해 탐색하므로 노드 개수(n)의 log 값만큼의 시간 소요 +# 공간 복잡도: O(h) +# - tree의 높이인 h만큼의 재귀 스택이 쌓임 +class Solution: + def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': + node1 = p if p.val < q.val else q # node1은 val이 작은 노드를 가르키게 설정 + node2 = q if p.val < q.val else p # node2은 val이 큰 노드를 가르키게 설정 + + def find_lca(node): + # BST 특징에 의해 + # 자기 자신 값은 왼쪽 서브트리 노드의 값들보다 무조건 크고 + # 오른쪽 서브트리 노드들의 값보다 무조건 작다. + # 따라서 node1.val <= node.val <= node2.val이면 + # p와 q가 현재 노드를 기준으로 양쪽에 갈라지는 순간으로 판단, + # 그 노드가 LCA가 된다. + if node1.val <= node.val <= node2.val: + # 사이에 들어온 경우는 node 그 자체를 return + # 등호도 같이 써준다. + return node + elif node.val > node1.val and node.val > node2.val: + # node의 값이 node1의 값과 node2의 값보다 큰 경우, + # LCA는 현재 노드의 left subtree에 존재한다는 의미이므로 + # 다음 탐색은 왼쪽 자식 노드 쪽으로 재귀 탐색 + return find_lca(node.left) + elif node.val < node1.val and node.val < node2.val: + # node의 값이 node1의 값과 node2의 값보다 작은 경우, + # LCA는 현재 노드의 right subtree에 존재한다는 의미이므로 + # 다음 탐색은 오른쪽 자식 노드 쪽으로 재귀 탐색 + return find_lca(node.right) + + # 세 케이스가 exhaustive하므로 else 없음 + + return find_lca(root) diff --git a/meeting-rooms/liza0525.py b/meeting-rooms/liza0525.py new file mode 100644 index 0000000000..8d0aebc667 --- /dev/null +++ b/meeting-rooms/liza0525.py @@ -0,0 +1,27 @@ +from typing import List + + +# class Interval(object): +# def __init__(self, start, end): +# self.start = start +# self.end = end + +# 7기 풀이 +# 시간 복잡도: O(n log n) +# - python sorting을 이용하여 정렬하기 때문에 +# 공간 복잡도: O(1) +# - 메서드 파라미터 이외에 사용하는 변수 없음 +class Solution: + def can_attend_meetings(self, intervals: List[Interval]) -> bool: + # intervals를 정렬, 이때 key는 각 요소의 start 시간을 기준으로 오름차순 해준다. + intervals.sort(key=lambda x: x.start) + + for i in range(1, len(intervals)): + # 현재 interval 시작 시간이 이전 interval 종료 시간보다 작으면 + # 두 구간은 겹치는 구간이므로, 문제 요건에 따라 False를 early return + if intervals[i].start < intervals[i - 1].end: + return False + + # 모든 interval 구간 확인 후 겹치지 않으며 + # True를 return + return True