Skip to content

Commit 46d6ee8

Browse files
authored
[okyungjin] WEEK 04 Solutions (최적화) (#2759)
* 79. Word Search (시간 복잡도 최적화) * 322. Coin Change (시간 복잡도 최적화) * 21. Merge Two Sorted Lists (시간 복잡도 최적화) * 153. Find Minimum in Rotated Sorted Array (시간 복잡도 최적화) * 104. Maximum Depth of Binary Tree (BFS 풀이 추가) * 104. Maximum Depth of Binary Tree (재귀 함수 풀이 추가) * 104. Maximum Depth of Binary Tree
1 parent 6955953 commit 46d6ee8

5 files changed

Lines changed: 123 additions & 182 deletions

File tree

coin-change/okyungjin.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
# coins로 amount를 만들 수 없는 경우 -1을 반환
66
# 동전의 종류는 무한대라고 가정하고 문제를 풀 것
77

8-
# 처음에는 greedy로 접근하려고 했는데, 금액이 큰 동전을 먼저 선택하는게 최소 동전의 수가 아니라서 dp로 풀이했다.
9-
108
# [복잡도]
119
# C: 코인의 개수, # A: amount
1210
# 시간 복잡도: O(A * C)
@@ -19,21 +17,19 @@ def coinChange(self, coins: List[int], amount: int) -> int:
1917
# min_coins[i]: i원을 만드는데 필요한 최소 동전의 개수
2018
min_coins = [NOT_POSSIBLE] * (amount + 1)
2119
min_coins[0] = 0
22-
23-
# 최적화를 위해 정렬
24-
coins.sort()
2520

21+
# 1부터 목표 금액(amount)까지 상향식으로 최소 동전 개수를 계산
2622
for cur_amount in range(1, amount + 1):
27-
# coin: 선택한 동전의 금액
23+
# 모든 동전 종류를 확인하여 현재 금액을 만들 수 있는 조합 탐색
2824
for coin in coins:
29-
# coins가 정렬되어 있으므로, cur_amount 보다 동전의 금액이 더 크면 이후는 확인 불필요
30-
if coin > cur_amount:
31-
break
32-
33-
# 둘 중 더 작은 값으로 업데이트
34-
# 1. 기존에 구한 개수
35-
# 2. 현재 동전을 1개 추가해서 만드는 개수
36-
min_coins[cur_amount] = min(min_coins[cur_amount], min_coins[cur_amount - coin] + 1)
25+
# 동전 금액이 현재 금액보다 작거나 같을 때만 유효한 조합으로 간주
26+
if coin <= cur_amount:
27+
# '남은 금액을 만드는 최소 개수' + '현재 동전 1개'를 합산
28+
new_val = min_coins[cur_amount - coin] + 1
29+
30+
# 더 적은 개수의 동전 조합을 찾았다면 해당 금액의 최솟값을 갱신
31+
if new_val < min_coins[cur_amount]:
32+
min_coins[cur_amount] = new_val
3733

3834
# 초기화된 값 그대로이면 불가능한 조합이므로 -1 반환
3935
if min_coins[amount] == NOT_POSSIBLE:

find-minimum-in-rotated-sorted-array/okyungjin.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
# 공간 복잡도: O(1)
1313
class Solution:
1414
def findMin(self, nums: List[int]) -> int:
15-
16-
# 왼쪽/오른쪽 값의 인덱스 계산
1715
left = 0
1816
right = len(nums) - 1
1917

18+
# 이미 정렬된 상태인 경우 or n번 회전한 경우 분기 처리
19+
if nums[left] <= nums[right]:
20+
return nums[left]
21+
2022
while left < right:
2123
# 중간값 인덱스
2224
mid = (left + right) // 2
Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,81 @@
1-
# Definition for a binary tree node.
2-
# class TreeNode:
3-
# def __init__(self, val=0, left=None, right=None):
4-
# self.val = val
5-
# self.left = left
6-
# self.right = right
7-
8-
# 시간 복잡도: O(N), 모든 노드를 한번 씩 방문
9-
# 공간 복잡도: O(N), 최악의 경우 모든 노드가 큐에 담길 수 있음
1+
'''
2+
스택을 활용한 DFS
3+
4+
시간 복잡도: O(N)
5+
공간 복잡도: O(H), H: 트리의 높이
6+
'''
107
class Solution:
118
def maxDepth(self, root: Optional[TreeNode]) -> int:
9+
# 트리가 비어있으면 길이는 0
1210
if root is None:
1311
return 0
1412

1513
max_depth = 1
14+
15+
# 스택에 (현재 노드, 노드의 깊이) 튜플을 저장
1616
stack = [(root, 1)]
1717

1818
while stack:
1919
node, cur_depth = stack.pop()
20+
21+
# 현재 노드의 깊이와 저장된 최댓값을 비교하여 갱신
2022
max_depth = max(cur_depth, max_depth)
2123

24+
# 왼쪽 자식 노드가 있다면 현재 깊이 + 1을 하여 스택에 추가
2225
if node.left:
2326
stack.append((node.left, cur_depth + 1))
2427

28+
# 오른쪽 자식 노드가 있다면 현재 깊이 + 1을 하여 스택에 추가
2529
if node.right:
2630
stack.append((node.right, cur_depth + 1))
2731

2832
return max_depth
33+
34+
'''
35+
큐를 활용한 BFS
36+
37+
시간 복잡도: O(N)
38+
공간 복잡도: O(W), W: 트리의 최대 너비
39+
'''
40+
class Solution:
41+
def maxDepth(self, root: Optional[TreeNode]) -> int:
42+
# 트리가 비어있으면 길이는 0
43+
if root is None:
44+
return 0
45+
46+
queue = deque([root])
47+
depth = 0
48+
49+
while queue:
50+
# 현재 레벨에 있는 노드의 개수만큼 반복 처리
51+
level_size = len(queue)
52+
53+
for _ in range(level_size):
54+
node = queue.popleft()
55+
56+
if node.left:
57+
queue.append(node.left)
58+
if node.right:
59+
queue.append(node.right)
60+
61+
# 한 레벨의 탐색이 끝나면 깊이 증가
62+
depth += 1
63+
64+
return depth
65+
66+
'''
67+
재귀 함수 풀이
68+
69+
시간 복잡도: O(N)
70+
공간 복잡도: O(H), H: 스택의 높이
71+
'''
72+
class Solution:
73+
def maxDepth(self, root: Optional[TreeNode]) -> int:
74+
if root is None:
75+
return 0
76+
77+
left_depth = self.maxDepth(root.left)
78+
right_depth = self.maxDepth(root.right)
79+
80+
# 현재 자신의 노드 하나를 더해서 최대 깊이를 반환
81+
return max(left_depth, right_depth) + 1
Lines changed: 16 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,28 @@
1-
# from typing import Optional, List
2-
3-
# class ListNode:
4-
# def __init__(self, val=0, next=None):
5-
# self.val = val
6-
# self.next = next
7-
8-
# # 파이썬의 toString 역할을 하는 __repr__을 [1,3,4] 형태로 출력되게 수정
9-
# def __repr__(self):
10-
# nodes = []
11-
# curr = self
12-
# while curr:
13-
# nodes.append(str(curr.val))
14-
# curr = curr.next
15-
# return "[" + ",".join(nodes) + "]"
16-
17-
# # 파이썬 list를 ListNode 리스트로 변환해주는 헬퍼 함수
18-
# def make_linked_list(arr: List[int]) -> Optional[ListNode]:
19-
# if not arr:
20-
# return None
21-
# dummy = ListNode(0)
22-
# curr = dummy
23-
# for val in arr:
24-
# curr.next = ListNode(val)
25-
# curr = curr.next
26-
# return dummy.next
27-
28-
291
# list1의 노드 개수를 n, list2의 노드 개수를 m이라 할 때
302
# 시간 복잡도: O(n + m)
31-
# 공간 복잡도: O(n + m)
32-
class Solution_01:
3+
# 공간 복잡도: O(1) -> dummy만 사용, 추가 공간 복잡도는 O(1)
4+
class Solution:
335
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
34-
# 맨 앞에 더미 노드를 하나 추가한다
35-
# 없는 상태로 짰는데 if node: else: 구문 늘어나서 맨 앞에 가상의 노드를 하나 추가해줬다
6+
# dummy: 병합된 결과 리스트의 시작점을 보존하기 위한 가상의 헤드 노드
7+
# current: 현재 병합 위치를 추적하며 노드를 연결해 나가는 포인터
368
dummy = ListNode(None)
37-
node = dummy
9+
current = dummy
3810

39-
while True:
40-
# list1이 비어있으면 현재 노드 뒤로 list2를 이어준다.
41-
if not list1:
42-
node.next = list2
43-
break
44-
45-
# list2가 비어있으면 현재 노드 뒤로 list1을 이어준다.
46-
if not list2:
47-
node.next = list1
48-
break
49-
50-
# 값을 비교해서 작은 값을 next 노드에 추가한다.
11+
# 두 리스트가 모두 존재하는 동안 값을 비교하며 merge한다
12+
while list1 and list2:
13+
# 더 작은 값을 가진 노드를 결과 리스트에 연결
5114
if list1.val <= list2.val:
52-
node.next = ListNode(list1.val)
53-
# list1 하나 소비
15+
current.next = list1
5416
list1 = list1.next
5517
else:
56-
node.next = ListNode(list2.val)
57-
# list2 하나 소비
18+
current.next = list2
5819
list2 = list2.next
59-
60-
# 다음 노드를 현재 노드로 할당해준다
61-
node = node.next
62-
63-
return dummy.next
64-
6520

66-
# [접근법] Solution_01 의 공간 복잡도를 개선했습니다.
67-
# ListNode를 새로 생성하지 않고 기존 노드를 활용하도록 수정했습니다.
68-
69-
# list1의 노드 개수를 n, list2의 노드 개수를 m이라 할 때
70-
# 시간 복잡도: O(n + m)
71-
# 공간 복잡도: O(1) -> dummy만 사용, 추가 공간 복잡도는 O(1)
72-
class Solution:
73-
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
74-
# 맨 앞에 더미 노드를 하나 추가한다
75-
# 없는 상태로 짰는데 if node: else: 구문 늘어나서 맨 앞에 가상의 노드를 하나 추가해줬다
76-
dummy = ListNode(None)
77-
node = dummy
21+
# 병합 리스트의 포인터를 다음으로 이동
22+
current = current.next
7823

79-
while True:
80-
if not list1: # list1이 비어있으면 현재 노드 뒤로 list2를 이어준다.
81-
node.next = list2
82-
break
83-
84-
if not list2: # list2가 비어있으면 현재 노드 뒤로 list1을 이어준다.
85-
node.next = list1
86-
break
87-
88-
# 값을 비교해서 작은 값을 next 노드에 추가한다.
89-
if list1.val <= list2.val:
90-
node.next = list1 # 새로운 ListNode를 생성하지 않고 list1를 할당해준다.
91-
list1 = list1.next # list1 하나 소비
92-
else:
93-
node.next = list2 # 새로운 ListNode를 생성하지 않고 list2를 할당해준다.
94-
list2 = list2.next # list2 하나 소비
95-
96-
# 다음 노드를 현재 노드로 할당해준다
97-
node = node.next
98-
24+
# 한쪽 리스트가 소진되면 남은 노드들을 한 번에 연결
25+
current.next = list1 if list1 else list2
26+
27+
# dummy 노드의 다음부터가 실제 정렬된 리스트의 시작
9928
return dummy.next
100-
101-
102-
# 4. 예시 입력으로 호출 및 테스트
103-
# if __name__ == "__main__":
104-
# list1 = make_linked_list([1, 2, 4])
105-
# list2 = make_linked_list([1, 3, 4])
106-
107-
# solution = Solution()
108-
# merged_list = solution.mergeTwoLists(list1, list2)
109-
110-
# print(merged_list)

word-search/okyungjin.py

Lines changed: 31 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,61 @@
1+
# https://leetcode.com/problems/word-search/
2+
13
# board: 영문 대/소문자 배열, m * n
24
# board의 상하좌우로 인접한 문자열을 이어붙여서 word를 완성할 수 있는지 여부를 반환한다.
35
# 단, 셀은 한번만 사용 가능하다
46

5-
# dfs를 통해 백트랙킹으로 정답을 찾도록 구현했다.
7+
# [접근법]
8+
# dfs로 문자열을 상,하,좌,우로 탐색한다.
69

10+
# [복잡도]
711
# L: word 의 길이
8-
# 시간 복잡도: O(m * n * 4^L)
9-
# 공간 복잡도: O(m * n + L)
10-
class SolutionA:
12+
# 시간 복잡도: O(m * n * 3^L), 이전 문자에서 이동한 방향은 제외하므로 3^L
13+
# 공간 복잡도: O(L)
14+
15+
class Solution:
1116
def exist(self, board: List[List[str]], word: str) -> bool:
1217
row_size = len(board)
1318
col_size = len(board[0])
1419

15-
# 방문 여부를 기록하는 2차원 배열
16-
visited = [[False] * col_size for _ in range(row_size)]
17-
18-
def dfs(row, col, str_idx) -> bool:
19-
if str_idx == len(word):
20-
return True
21-
22-
# 종료조건1: 보드의 좌표를 넘어가는 경우
23-
if row < 0 or row >= row_size or col < 0 or col >= col_size:
20+
# board에 word를 만들 수 있는 문자열이 충분히 있는지 확인
21+
for char in set(word):
22+
# board 전체에서 해당 알파벳이 몇 개인지 카운트한다.
23+
board_char_count = sum(row.count(char) for row in board)
24+
if board_char_count < word.count(char):
2425
return False
25-
26-
# 종료조건 2: 이미 방문함
27-
# 종료조건 3: 문자열 불일치
28-
if visited[row][col] or board[row][col] != word[str_idx]:
29-
return False
30-
31-
32-
visited[row][col] = True
33-
34-
# 차례대로 상, 하, 좌, 우의 좌표를 탐색
35-
found = (dfs(row - 1, col, str_idx + 1) or \
36-
dfs(row + 1, col, str_idx + 1) or \
37-
dfs(row, col - 1, str_idx + 1) or \
38-
dfs(row, col + 1, str_idx + 1))
39-
40-
visited[row][col] = False # 백트래킹
41-
42-
return found
4326

27+
# 빈도수가 더 적은 글자부터 탐색하도록 단어를 뒤집는다
28+
count_first = sum(row.count(word[0]) for row in board)
29+
count_last = sum(row.count(word[-1]) for row in board)
30+
31+
if count_first > count_last:
32+
word = word[::-1]
4433

45-
# 시작점을 찾는다
46-
for r in range(row_size):
47-
for c in range(col_size):
48-
if board[r][c] == word[0] and dfs(r, c, 0):
49-
return True
50-
51-
return False
52-
53-
# SolutionA 의 공간복잡도 최적화 버전, visited 제거
54-
# L: word 의 길이
55-
# 시간 복잡도: O(m * n * 4^L)
56-
# 공간 복잡도: O(L)
57-
class Solution:
58-
def exist(self, board: List[List[str]], word: str) -> bool:
5934
VISITED_MARK = '#'
60-
61-
row_size = len(board)
62-
col_size = len(board[0])
63-
64-
def dfs(row, col, str_idx) -> bool:
35+
def dfs(row: int, col: int, str_idx: int) -> bool:
6536
if str_idx == len(word):
6637
return True
6738

6839
# 종료조건1: 보드의 좌표를 넘어가는 경우
69-
if row < 0 or row >= row_size or col < 0 or col >= col_size:
40+
if not (0 <= row < row_size and 0 <= col < col_size):
7041
return False
7142

72-
# 종료조건 2: 이미 방문함
43+
# 종료조건 2: 이미 방문함 (#: 방문했음을 표시하는 마크)
7344
# 종료조건 3: 문자열 불일치
7445
if board[row][col] == VISITED_MARK or board[row][col] != word[str_idx]:
7546
return False
76-
7747

78-
# VISITED_MARK로 교체하여 방문을 기록
48+
# 방문을 표시하는 마크(#)로 임시 교체
7949
origin_char = board[row][col]
8050
board[row][col] = VISITED_MARK
8151

82-
# 차례대로 상, 하, 좌, 우의 좌표를 탐색
83-
found = (dfs(row - 1, col, str_idx + 1) or \
84-
dfs(row + 1, col, str_idx + 1) or \
85-
dfs(row, col - 1, str_idx + 1) or \
86-
dfs(row, col + 1, str_idx + 1))
52+
# 좌표를 탐색한다
53+
found = (
54+
dfs(row - 1, col, str_idx + 1) # 상
55+
or dfs(row + 1, col, str_idx + 1) # 하
56+
or dfs(row, col - 1, str_idx + 1) # 좌
57+
or dfs(row, col + 1, str_idx + 1) # 우
58+
)
8759

8860
# 원본 문자열로 복원
8961
board[row][col] = origin_char

0 commit comments

Comments
 (0)