diff --git a/longest-substring-without-repeating-characters/KwonNayeon.py b/longest-substring-without-repeating-characters/KwonNayeon.py index e01b46072..3588f5639 100644 --- a/longest-substring-without-repeating-characters/KwonNayeon.py +++ b/longest-substring-without-repeating-characters/KwonNayeon.py @@ -15,23 +15,34 @@ 3. 중복 문자를 만나면 윈도우의 시작점(current_start)을 중복 문자 다음 위치로 이동 4. 매 단계에서 현재 윈도우의 길이를 계산하고 최대 길이 갱신 5. 최종적으로 가장 긴 중복 없는 부분 문자열의 길이 반환 + +노트: +- 처음에 어려웠던 부분: 딕셔너리를 사용한다고 해서, 지금까지 캐릭터가 등장한 횟수를 저장해야 한다고 생각함 +- 실제로는 각 문자의 마지막 등장 위치(인덱스)를 저장하는 용도임 """ +# e.g., s = "abca" class Solution: def lengthOfLongestSubstring(self, s: str) -> int: - seen = {} + seen = {} # 각 문자가 마지막으로 등장한 인덱스를 저장하는 딕셔너리 current_start = 0 max_length = 0 for i in range(len(s)): char = s[i] + # 현재 문자가 이미 등장했고, 그 위치가 현재 고려 중인 부분 문자열 내에 있는 경우 if char in seen and seen[char] >= current_start: + + # 중복 발생, 윈도우 시작점을 이전 등장 위치 다음으로 이동 current_start = seen[char] + 1 seen[char] = i + # 현재 부분 문자열의 길이 계산 (현재 인덱스 - 시작 인덱스 + 1) current_length = i - current_start + 1 + + # 최대 길이 업데이트 max_length = max(current_length, max_length) return max_length diff --git a/number-of-islands/KwonNayeon.py b/number-of-islands/KwonNayeon.py index d532e7700..dea42cfbf 100644 --- a/number-of-islands/KwonNayeon.py +++ b/number-of-islands/KwonNayeon.py @@ -13,9 +13,11 @@ - 최악의 경우(모든 셀이 '1'일 때) m * n 만큼의 재귀 호출 스택 사용 풀이 방법: -1. 2중 for문으로 그리드의 모든 셀을 순회 -2. '1'을 발견하면 DFS로 연결된 모든 땅을 방문하고 '0'으로 표시 -3. '1'을 발견할 때마다 islands 카운트를 1씩 증가 +1. numIslands 메서드: 그리드를 순회하며 '1'을 찾고 섬의 개수를 세는 역할 +2. visit_island 메서드: 하나의 섬을 완전히 탐색하고 방문한 땅을 '0'으로 바꾸는 역할 +3. 두 함수가 작동하는 방식: +- numIslands는 섬의 시작점('1')을 찾아 카운트를 증가시킴 +- visit_island는 찾은 섬을 완전히 탐색하고 방문 표시('0'으로 변경) """ class Solution: @@ -29,14 +31,17 @@ def numIslands(self, grid: List[List[str]]) -> int: return islands def visit_island(self, grid, i, j): + # base case: 위치가 범위를 벗어나거나 '1'이 아니면 함수를 종료 if (i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0]) or grid[i][j] != '1'): return + # 방문한 땅을 '0'으로 표시하여 다시 방문하지 않도록 함 grid[i][j] = '0' - self.visit_island(grid, i+1, j) # 위 - self.visit_island(grid, i-1, j) # 아래 + # 네 방향(아래, 위, 오른쪽, 왼쪽)으로 DFS 재귀 호출 + self.visit_island(grid, i+1, j) # 아래 + self.visit_island(grid, i-1, j) # 위 self.visit_island(grid, i, j+1) # 오른쪽 self.visit_island(grid, i, j-1) # 왼쪽 diff --git a/reverse-linked-list/KwonNayeon.py b/reverse-linked-list/KwonNayeon.py index 2c3b31ed2..c105b53a8 100644 --- a/reverse-linked-list/KwonNayeon.py +++ b/reverse-linked-list/KwonNayeon.py @@ -3,13 +3,15 @@ - The number of nodes in the list is the range [0, 5000] - -5000 <= Node.val <= 5000 + + Time Complexity: O(n) - n은 linked list의 노드 수 -- 리스트를 한 번 순회하면서 각 노드를 한 번씩만 방문하기 때문 +- 리스트를 한 번 순회하면서 각 노드를 한 번씩만 방문 Space Complexity: O(1) - 추가 공간으로 prev, curr, temp 세 개의 포인터만 사용 -- 입력 크기와 관계없이 일정한 추가 공간만 사용 +- 입력 크기와 관계없이 일정한 추가 공간만 사용함 풀이 방법: 1. 세 개의 포인터를 사용하여 리스트를 순회하면서 뒤집기 @@ -45,3 +47,49 @@ def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: curr = temp return prev + +""" + + +Time Complexity: O(n) +- 스택에 모든 노드를 넣을 때/뺄 때 각 O(n) 시간을 소모함 + +Space Complexity: O(n) +- 스택에 모든 노드를 넣었다가 빼야 하므로 + +풀이방법: +- 스택의 LIFO 특성을 활용 + +노트: +- 풀이 1보다 공간 복잡도가 올라가지만, 더 이해하기 쉬운 풀이 +""" + +class Solution: + def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: + # 모든 노드를 순서대로 저장할 빈 리스트 생성 + nodes = [] + + # 현재 노드를 헤드로 초기화 + node = head + + # 링크드 리스트의 모든 노드를 순회하며 리스트에 저장 + while node: + nodes.append(node) + node = node.next + + # 새 링크드 리스트의 시작점이 될 더미 노드 + dummy = ListNode(-1) + + # 새 리스트를 만들기 위한 포인터 초기화 + node = dummy + + # nodes 리스트에서 역순으로 노드를 꺼내서 새 리스트 구성 + while nodes: + node.next = nodes.pop() # 리스트의 마지막 노드를 꺼내서 연결 + node = node.next # 노드 이동 + + # 마지막 노드의 next를 None으로 설정하여 리스트 종료 + node.next = None + + # 더미 노드의 next = 뒤집힌 리스트의 헤드 + return dummy.next diff --git a/set-matrix-zeroes/KwonNayeon.py b/set-matrix-zeroes/KwonNayeon.py index 552168bf0..caf63ecd7 100644 --- a/set-matrix-zeroes/KwonNayeon.py +++ b/set-matrix-zeroes/KwonNayeon.py @@ -5,20 +5,20 @@ - 1 <= m, n <= 200 - -2^31 <= matrix[i][j] <= 2^31 - 1 -Time Complexity: O(m*n) -- m은 행, n은 열을 의미 -- 0 찾기: O(m*n) -- 행과 열 변환: O(m*n) + -Space Complexity: O(m*n) -- zeros 배열이 최대 m*n 크기까지 저장 가능 +Time Complexity: O(m * n) +- m은 행, n은 열을 의미함 +- 0 찾기: O(m * n) +- 행과 열 변환: O(m * n) + +Space Complexity: O(m * n) +- zeros 배열이 최대 m * n 크기까지 저장 가능 풀이 방법: -1. 0 위치 저장 +1. 0 위치를 탐색/저장 2. 저장된 0의 행과 열을 모두 0으로 변환 -3. 주의점: 행렬 값 탐색과 변경을 동시에 수행하면 원래 어떤 값이 0이었는지 구분하기 어려워짐 """ - class Solution: def setZeroes(self, matrix: List[List[int]]) -> None: """ @@ -32,17 +32,21 @@ def setZeroes(self, matrix: List[List[int]]) -> None: zeros.append((r, c)) for r, c in zeros: + # 행(row)을 0으로 채움 for i in range(len(matrix[0])): matrix[r][i] = 0 + # 열(column)을 0으로 채움 for i in range(len(matrix)): matrix[i][c] = 0 """ -Time Complexity: O(m*n) -- 행렬 순회: O(m*n) -- 행과 열 변환: O(m*n) + + +Time Complexity: O(m * n) +- 행렬 순회: O(m * n) +- 행과 열 변환: O(m * n) -Space Complexity: O(m+n) +Space Complexity: O(m + n) - zero_rows: O(m) - zero_cols: O(n) @@ -51,7 +55,6 @@ def setZeroes(self, matrix: List[List[int]]) -> None: 2. 행과 열 정보를 분리 저장하여 메모리를 효율적으로 사용 3. 행과 열을 독립적으로 처리하여 불필요한 반복 연산 제거 """ - class Solution: def setZeroes(self, matrix: List[List[int]]) -> None: """ @@ -75,7 +78,9 @@ def setZeroes(self, matrix: List[List[int]]) -> None: matrix[i][c] = 0 """ -Time Complexity: O(m*n) + + +Time Complexity: O(m * n) Space Complexity: O(1) - 추가적인 메모리를 사용하지 않고 첫 행과 열을 마커로 활용하여 해결 diff --git a/unique-paths/KwonNayeon.py b/unique-paths/KwonNayeon.py index 6a31df3ef..b0039c9c6 100644 --- a/unique-paths/KwonNayeon.py +++ b/unique-paths/KwonNayeon.py @@ -2,6 +2,8 @@ Constraints: - 1 <= m, n <= 100 + + Time Complexity: O(1) - math.comb() 사용 @@ -13,11 +15,58 @@ 1. (m-1)번 아래로, (n-1)번 오른쪽으로 가야함 -> 총 (m+n-2)번 이동 2. 결국 (m+n-2)번의 이동 중 (n-1)번의 오른쪽 이동을 선택하는 조합의 수 3. Combination 공식 적용: (m+n-2)C(n-1) - -Further Consideration: -- DP로 풀어보기 + - C(m+n-2, n-1) = (m+n-2)! / ((n-1)! × (m-1)!) +- 풀이가 간결함, 하지만 큰 수에서 오버플로우 가능성 있음 """ class Solution: def uniquePaths(self, m: int, n: int) -> int: from math import comb return comb(m+n-2, n-1) + +""" + + +Time Complexity: O(m * n) +- m은 row, n은 column의 길이 +- 메모이제이션 활용, 모든 좌표에서 재귀 함수의 호출이 딱 한번만 일어나기 때문 + +Space Complexity: O(m * n) +- 함수의 호출 결과를 저장하는데 격자의 넓이에 비례하는 공간이 필요 + +풀이방법: +- 구현이 복잡함, 하지만 큰 수에서도 안정적임 +- 재귀와 메모이제이션을 활용한 Top-down DP 접근법 +- 현재 위치에서 목적지까지의 경로 수 = 아래로 이동 + 오른쪽으로 이동 + +2x3 그리드 예시: +각 위치에서 목적지까지 가는 경로 수: +(0,0)=3 (0,1)=2 (0,2)=1 +(1,0)=1 (1,1)=1 (1,2)=1 + +위치 (0,0)에서 시작: +dfs(0,0) = dfs(1,0) + dfs(0,1) = 1 + 2 = 3 + +경로 3개: +1. 오른쪽, 오른쪽, 아래 +2. 오른쪽, 아래, 오른쪽 +3. 아래, 오른쪽, 오른쪽 +""" +from functools import cache +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + @cache + def dfs(row, col): + if row == m - 1 and col == n - 1: + return 1 + + total = 0 + + if row + 1 < m: + total += dfs(row + 1, col) + + if col + 1 < n: + total += dfs(row, col + 1) + + return total + + return dfs(0, 0)