diff --git a/longest-substring-without-repeating-characters/seungriyou.py b/longest-substring-without-repeating-characters/seungriyou.py new file mode 100644 index 000000000..e939af08a --- /dev/null +++ b/longest-substring-without-repeating-characters/seungriyou.py @@ -0,0 +1,62 @@ +# https://leetcode.com/problems/longest-substring-without-repeating-characters/ + +class Solution: + def lengthOfLongestSubstring_slow(self, s: str) -> int: + """ + [Complexity] + - TC: O(n) + - SC: O(n) + + [Approach] + two pointer를 이동해가며 substring을 확인하는 과정에서, hash table을 이용하여 duplicate character를 O(1)에 lookup 할 수 있도록 한다. + p1, p2를 각각 왼쪽과 오른쪽 pointer라고 할 때, + - substring의 맨 오른쪽 문자가 duplicate이 아니면, seen에 추가하고 맨 오른쪽 범위를 한 칸 늘린다. 그리고 max_len을 업데이트 한다. + - substring의 맨 오른쪽 문자가 duplicate이면, seen에서 맨 왼쪽 문자를 삭제하고 맨 왼쪽 범위를 한 칸 줄인다. + """ + + p1 = p2 = max_len = 0 + seen = set() + + while p2 < len(s): + # p2가 중복이 아닌 문자를 가리키면, seen에 추가하고 p2 한 칸 옮기기 + if s[p2] not in seen: + seen.add(s[p2]) + p2 += 1 + # max_len 업데이트 + max_len = max(max_len, p2 - p1) + + # p2가 중복인 문자를 가리키면, seen에서 s[p1] 삭제하고 p1 한 칸 옮기기 + else: + seen.remove(s[p1]) + p1 += 1 + + return max_len + + def lengthOfLongestSubstring(self, s: str) -> int: + """ + [Complexity] + - TC: O(n) + - SC: O(n) + + [Approach] + lengthOfLongestSubstring_slow()의 경우, p2가 중복인 문자를 가리킬 때 p1을 한 칸씩만 이동한다. + 하지만 해당 문자가 가장 마지막으로 나온 인덱스를 기록해둔다면, 바로 그 다음 인덱스로 p1을 이동시킬 수 있다. + 이러한 인덱스를 lookup 하는 행위도 hash table을 이용해 O(1)으로 최적화 할 수 있다. + """ + + p1 = max_len = 0 + seen = dict() + + for p2, right in enumerate(s): + # right 문자가 p1 ~ p2 내 범위에서 이미 등장했다면, p1을 right 문자가 가장 마지막으로 등장한 인덱스의 다음 위치로 이동 + if seen.get(right, -1) >= p1: + p1 = seen[right] + 1 + + # 그렇지 않다면, p1을 그대로 두고 max_len 업데이트 + else: + max_len = max(max_len, p2 - p1 + 1) + + # right를 seen에 기록 + seen[right] = p2 + + return max_len diff --git a/number-of-islands/seungriyou.py b/number-of-islands/seungriyou.py new file mode 100644 index 000000000..6840d904f --- /dev/null +++ b/number-of-islands/seungriyou.py @@ -0,0 +1,76 @@ +# https://leetcode.com/problems/number-of-islands/ + +from typing import List + +class Solution: + def numIslands_bfs(self, grid: List[List[str]]) -> int: + """ + [Complexity] + - TC: O(m * n) + - SC: O(m * n) + + [Approach] + BFS를 사용한다. + """ + from collections import deque + + dr, dc = [-1, 1, 0, 0], [0, 0, -1, 1] + m, n = len(grid), len(grid[0]) + res = 0 + + def bfs(r, c): + q = deque([(r, c)]) + grid[r][c] = "0" + + while q: + r, c = q.popleft() + + for i in range(4): + nr, nc = r + dr[i], c + dc[i] + # nr, nc가 (1) 범위 내이면서 (2) 아직 방문하지 않은 곳이라면 + if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == "1": + q.append((nr, nc)) + grid[nr][nc] = "0" + + for r in range(m): + for c in range(n): + if grid[r][c] == "1": + bfs(r, c) + res += 1 + + return res + + def numIslands(self, grid: List[List[str]]) -> int: + """ + [Complexity] + - TC: O(m * n) + - SC: O(m * n) (call stack) + + [Approach] + DFS를 사용한다. + """ + dr, dc = [-1, 1, 0, 0], [0, 0, -1, 1] + m, n = len(grid), len(grid[0]) + res = 0 + + def dfs(r, c): + # base condition + if not (0 <= r < m and 0 <= c < n) or grid[r][c] == "0": + return + + # visited 처리 + grid[r][c] = "0" + + # recur + dfs(r - 1, c) + dfs(r + 1, c) + dfs(r, c - 1) + dfs(r, c + 1) + + for r in range(m): + for c in range(n): + if grid[r][c] == "1": + dfs(r, c) + res += 1 + + return res diff --git a/reverse-linked-list/seungriyou.py b/reverse-linked-list/seungriyou.py new file mode 100644 index 000000000..2412307cb --- /dev/null +++ b/reverse-linked-list/seungriyou.py @@ -0,0 +1,70 @@ +# https://leetcode.com/problems/reverse-linked-list/ + +from typing import Optional + +# Definition for singly-linked list. +class ListNode: + def __init__(self, val=0, next=None): + self.val = val + self.next = next + +class Solution: + def reverseList_iter(self, head: Optional[ListNode]) -> Optional[ListNode]: + """ + [Complexity] + - TC: O(n) + - SC: O(1) + + [Approach] + linked-list를 reverse 할 때의 핵심은 + (1) p1 -> p2를 p1 <- p2로 바꾸고 + (2) p1, p2를 모두 한 칸 앞으로 전진 + 하는 것이다. + 이때, (1)에서 p2.next = p1로 바꾼다면, p2를 한 칸 앞으로 전진할 수 없기 때문에 먼저 p2.next(= p3)를 기록해둬야 한다. + 이를 iterative 하게 구현하면 while문을 이용할 수 있다. + """ + p1, p2 = None, head + + while p2: + # 1. p3 얻어놓기 + p3 = p2.next + + # 2. p1 <- p2 전환 + p2.next = p1 + + # 3. p1 -> p2, p2 -> p3 이동 + p1, p2 = p2, p3 + + return p1 + + def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]: + """ + [Complexity] + - TC: O(n) + - SC: O(n) (call stack) + + [Approach] + linked-list를 reverse 할 때의 핵심은 + (1) p1 -> p2를 p1 <- p2로 바꾸고 + (2) p1, p2를 모두 한 칸 앞으로 전진 + 하는 것이다. + 이때, (1)에서 p2.next = p1로 바꾼다면, p2를 한 칸 앞으로 전진할 수 없기 때문에 먼저 p2.next(= p3)를 기록해둬야 한다. + 이를 recursive 하게 구현하려면 다음과 같이 base condition으로 종료조건을 추가해주면 된다. + """ + + def reverse_ll(p1, p2): + # base condition + if not p2: + return p1 + + # recur + # 1. p3 얻어놓기 + p3 = p2.next + + # 2. p1 <- p2 전환 + p2.next = p1 + + # 3. p1 -> p2, p2 -> p3 이동 + return reverse_ll(p2, p3) + + return reverse_ll(None, head) diff --git a/set-matrix-zeroes/seungriyou.py b/set-matrix-zeroes/seungriyou.py new file mode 100644 index 000000000..2f4e16b04 --- /dev/null +++ b/set-matrix-zeroes/seungriyou.py @@ -0,0 +1,91 @@ +# https://leetcode.com/problems/set-matrix-zeroes/ + +from typing import List + +class Solution: + def setZeroes_slow(self, matrix: List[List[int]]) -> None: + """ + Do not return anything, modify matrix in-place instead. + + [Complexity] + - TC: O(m * n * (m + n)) -> too slow... + - SC: O(1) + + [Approach] + 다음의 두 단계를 inplace로 수행한다. + 1. 모든 cell을 순회하며, 0을 발견하면 해당 row와 column의 값을 (0인 cell을 제외하고) 모두 #로 바꾼다. + 2. 다시 모든 cell을 순회하며, #을 모두 0으로 바꾼다. + """ + + m, n = len(matrix), len(matrix[0]) + + def set_row_col(r, c): + for _r in range(m): + # 0이 아닌 경우에만 "#"으로 바꿔야 함 ** + if matrix[_r][c] != 0: + matrix[_r][c] = "#" + + for _c in range(n): + if matrix[r][_c] != 0: + matrix[r][_c] = "#" + + # 1. 0을 발견하면 해당 row & column을 "#"으로 바꾸기 + for r in range(m): + for c in range(n): + if matrix[r][c] == 0: + # row & column을 "#"으로 바꾸기 + set_row_col(r, c) + + # 2. "#"을 모두 0으로 바꾸기 + for r in range(m): + for c in range(n): + if matrix[r][c] == "#": + matrix[r][c] = 0 + + def setZeroes(self, matrix: List[List[int]]) -> None: + """ + Do not return anything, modify matrix in-place instead. + + [Complexity] + - TC: O(m * n) + - SC: O(1) + + [Approach] + matrix의 첫 번째 row와 column을 flag 기록 용도로 사용한다면, space를 O(1)로 유지하면서 time도 최적화할 수 있다. + - 첫 번째 row: 각 column에 0이 있었다면, 해당 column의 칸에 0으로 기록 + - 첫 번째 column: 각 row에 0이 있었다면, 해당 row의 칸에 0으로 기록 + 이렇게 첫 번째 row와 column을 flag 기록용으로 쓰기 전에, 해당 row와 column에 0이 있었는지 여부를 미리 확인해야 함에 유의한다. + 전체 흐름은 다음과 같다: + 1. 첫 번째 row와 column에 0이 존재하는지 여부 확인 + 2. 두 번째 row와 column 부터 0이 존재하는 칸에 대해 첫 번째 row와 column에 flag 기록 + 3. 첫 번째 row와 column의 flag 값을 기반으로, 0 채우기 + 4. 1번에서 구해둔 값으로도 0 채우기 + """ + + m, n = len(matrix), len(matrix[0]) + + # 1. 첫 번째 row와 column에 0이 존재하는지 여부 확인 + has_zero_in_first_row = any(matrix[0][_c] == 0 for _c in range(n)) + has_zero_in_first_col = any(matrix[_r][0] == 0 for _r in range(m)) + + # 2. 두 번째 row와 column 부터 0이 존재하는 칸에 대해 첫 번째 row와 column에 flag 기록 + for r in range(1, m): + for c in range(1, n): + if matrix[r][c] == 0: + matrix[0][c] = 0 + matrix[r][0] = 0 + + # 3. 첫 번째 row와 column의 flag 값을 기반으로, 0 채우기 + for r in range(1, m): + for c in range(1, n): + if matrix[0][c] == 0 or matrix[r][0] == 0: + matrix[r][c] = 0 + + # 4. 1번에서 구해둔 값으로도 0 채우기 + if has_zero_in_first_row: + for _c in range(n): + matrix[0][_c] = 0 + + if has_zero_in_first_col: + for _r in range(m): + matrix[_r][0] = 0 diff --git a/unique-paths/seungriyou.py b/unique-paths/seungriyou.py new file mode 100644 index 000000000..e3d210164 --- /dev/null +++ b/unique-paths/seungriyou.py @@ -0,0 +1,49 @@ +# https://leetcode.com/problems/unique-paths/ + +class Solution: + def uniquePaths_2d(self, m: int, n: int) -> int: + """ + [Complexity] + - TC: O(m * n) + - SC: O(m * n) + + [Approach] + 로봇이 down or right로만 이동할 수 있으므로, 어떤 칸에 도달할 수 있는 unique path의 개수는 up 칸과 left 칸까지의 unique path의 개수를 더한 값이 된다. + 따라서 2D DP로 풀 수 있으며, dp table은 다음과 같다. + dp[r][c] = (up 칸까지의 unique path) + (left 칸까지의 unique path) + = dp[r - 1][c] + dp[r][c - 1] + 이때, dp table의 첫 번째 row와 column에 있는 칸들은 모두 도달할 수 있는 unique path의 개수가 1이므로 초기화해준다. + """ + # 첫 번째 row & column은 1로 초기화 + dp = [[1 for _ in range(n)] for _ in range(m)] + + # 두 번째 row & column 부터 순회 + for r in range(1, m): + for c in range(1, n): + dp[r][c] = dp[r - 1][c] + dp[r][c - 1] + + return dp[m - 1][n - 1] + + def uniquePaths(self, m: int, n: int) -> int: + """ + [Complexity] + - TC: O(m * n) + - SC: O(n) (1D DP로 space optimization) + + [Approach] + 2D DP에서 dp[r][c]를 구할 때 dp[r - 1][c]과 dp[r][c - 1]만 사용하므로, + rolling array 기법을 이용해 1D DP로 space optimization이 가능하다. + 따라서 길이가 n인 1D dp list를 이용해 + - c(= 1 ~ n - 1)를 순회하며 dp[c] += dp[c - 1]로 업데이트하는 것을 (2D DP에서의 dp[r - 1][c]가 dp[c] 값이므로!) + - row 개수인 m 번 반복 (단, 첫 번째 row를 1로 초기화 한 효과를 얻기 위해 dp를 1로 초기화 하고 m - 1번 반복) + 하면 된다. + """ + + # 첫 번째 row는 1로 초기화 한 효과 + dp = [1] * n + + for _ in range(1, m): # m - 1 번 반복 + for c in range(1, n): # 첫 번째 col은 1로 초기화 한 효과 + dp[c] += dp[c - 1] + + return dp[n - 1]