diff --git a/linked-list-cycle/seungriyou.py b/linked-list-cycle/seungriyou.py new file mode 100644 index 000000000..2c94a8aa3 --- /dev/null +++ b/linked-list-cycle/seungriyou.py @@ -0,0 +1,55 @@ +# https://leetcode.com/problems/linked-list-cycle/ + +from typing import Optional + +# Definition for singly-linked list. +class ListNode: + def __init__(self, x): + self.val = x + self.next = None + +class Solution: + def hasCycle_on(self, head: Optional[ListNode]) -> bool: + """ + [Complexity] + - TC: O(n) + - SC: O(n) + + [Approach] + hash table로 visited 노드를 기록한다. + """ + curr = head + visited = set() + + while curr: + if curr in visited: + return True + + visited.add(curr) + curr = curr.next + + return False + + def hasCycle(self, head: Optional[ListNode]) -> bool: + """ + [Complexity] + - TC: O(n) + - SC: O(1) + + [Approach] + Floyd’s Cycle Detection 알고리즘을 사용한다. + linked list에서 slow (1칸씩 전진) & fast (2칸씩 전진) runner를 이용하면, + - slow와 fast가 만난다면 cyclic + - 만나지 않은 채로 fast가 끝에 도달하면 not cyclic + 이다. + """ + slow = fast = head + + while fast and fast.next: + slow = slow.next + fast = fast.next.next + + if slow == fast: + return True + + return False diff --git a/maximum-product-subarray/seungriyou.py b/maximum-product-subarray/seungriyou.py new file mode 100644 index 000000000..980c4f303 --- /dev/null +++ b/maximum-product-subarray/seungriyou.py @@ -0,0 +1,38 @@ +# https://leetcode.com/problems/maximum-product-subarray/ + +from typing import List + +class Solution: + def maxProduct(self, nums: List[int]) -> int: + """ + [Complexity] + - TC: O(n) + - SC: O(1) + + [Approach] + nums를 순회하며 현재 보고 있는 값 num이 포함되는 max subarray product를 구하기 위해서는 + max(이전 값과 연속되는 subarray인 경우, 이전 값과 연속되지 않는 subarray인 경우)를 구해야 한다. + - num이 양수일 때: max(이전까지의 max subarray product * num, num) + - num이 음수일 때: max(이전까지의 min subarray product * num, num) + 따라서 매 단계마다 이전까지의 max subarray product(= max_p)와 min subarray product(= min_p)를 트래킹해야 한다. + """ + res = max_p = min_p = nums[0] + + for i in range(1, len(nums)): + num = nums[i] + + # 1. num이 음수라면, max_p와 min_p 바꿔치기 + # if num < 0: + # max_p, min_p = min_p, max_p + + # 2. max_p, min_p 업데이트 + # max_p = max(max_p * num, num) + # min_p = min(min_p * num, num) + + # 1 & 2번 대신, 다중 할당으로 처리 가능 + max_p, min_p = max(max_p * num, min_p * num, num), min(max_p * num, min_p * num, num) + + # 3. res 값 업데이트 + res = max(res, max_p) + + return res diff --git a/minimum-window-substring/seungriyou.py b/minimum-window-substring/seungriyou.py new file mode 100644 index 000000000..26a75842c --- /dev/null +++ b/minimum-window-substring/seungriyou.py @@ -0,0 +1,114 @@ +# https://leetcode.com/problems/minimum-window-substring/ + +class Solution: + def minWindow1(self, s: str, t: str) -> str: + """ + [Complexity] + - TC: O(m * k + n) (k = is_included() = len(set(t))) + - SC: O(k) (res 제외) + + [Approach] + 다음과 같은 counter를 유지하며, two pointer로 window를 이동하며 min window substring을 트래킹한다. + - 생성 시) t의 모든 문자에 대해 ++ + - pointer 이동 시) window에 포함되는 문자 중 t에 속하는 문자에 대해 -- + """ + from collections import Counter + + # early stop + m, n = len(s), len(t) + if m < n: + return "" + + # t에 대한 counter 생성 + counter = Counter(t) + + # t의 모든 문자가 window에 포함되는지 확인하는 함수 + def is_included(): + # counter의 모든 값이 0 이하이면, t의 모든 문자가 window에 포함되는 것 (중복 포함) + return all(c <= 0 for c in counter.values()) + + lo, min_len = 0, 1e6 + res = "" + + for hi in range(m): + # 현재 window에 포함된 문자를 counter에 반영 + if s[hi] in counter: + counter[s[hi]] -= 1 + + # t의 모든 문자가 window에 포함되어있다면(= counter의 모든 값이 <= 0이면), + # counter 값이 음수 ~ 0이 될 때까지 lo 증가시키면서 (min window를 구해야하므로) counter 업데이트 + if is_included(): + while True: + # window의 첫 번째 문자가 t에 속하는 문자라면 + if s[lo] in counter: + # counter에서의 값이 0이면, 더이상 lo를 오른쪽으로 줄일 수 없음 + if counter[s[lo]] == 0: + break + # coutner에서의 값이 음수라면, lo를 오른쪽으로 줄일 수 있으므로 counter 반영 + else: + counter[s[lo]] += 1 + # lo를 오른쪽으로 한 칸 이동 + lo += 1 + + # (1) t의 모든 문자가 window에 포함되어 있고 (2) 현재 window의 length가 min_len 보다 작다면 + # window substring 업데이트 + if is_included() and (new_len := hi - lo + 1) < min_len: + min_len = new_len + res = s[lo:hi + 1] + + return res + + def minWindow(self, s: str, t: str) -> str: + """ + [Complexity] + - TC: O(m + n) + - SC: O(k) + + [Approach] + 1) 위의 풀이에서 is_included()를 실행하는 데에 O(k)이 소요되므로, + (t에 속하는 문자 중, 아직 window에 모두 포함되지 않은 문자 종류 개수)를 트래킹하는 변수 remains를 이용하여 최적화 한다. + 즉, remains == 0이라면 is_included()인 것과 동일하다. + 2) 반복문 안에서 문자열 슬라이싱으로 res를 업데이트 할 때 추가적인 복잡도가 소요된다. + 따라서 min window에 대한 pointer인 min_lo, min_hi만 트래킹하고, return 문에서 문자열 슬라이싱으로 반환한다. + 단, min window substring이 존재하지 않는다면 결과가 빈 문자열이 될 수 있으므로, min_lo ~ min_hi - 1 범위를 반환하도록 한다. + """ + from collections import Counter + + # early stop + m, n = len(s), len(t) + if m < n: + return "" + + # t에 대한 counter 생성 + counter = Counter(t) + + # counter의 값을 모두 확인하는 is_include() 최적화 + remains = len(counter) # t에 속하는 문자 중, 아직 window에 모두 포함되지 않은 문자 종류 개수 + lo = min_lo = min_hi = 0 + min_len = m + 1 + + for hi in range(m): + # 현재 window에 포함된 문자를 counter에 반영 + if s[hi] in counter: + counter[s[hi]] -= 1 + # 현재 window에 해당 문자가 t에 존재하는 개수만큼 들어와있다면, remains-- + if counter[s[hi]] == 0: + remains -= 1 + + # t의 모든 문자가 window에 포함되어있는 동안 lo 이동 + while not remains: + # 최소 길이 window substring 갱신 + if (new_len := hi - lo + 1) < min_len: + min_len, min_lo, min_hi = new_len, lo, hi + 1 + + # lo 이동 전, counter 업데이트 + if s[lo] in counter: + counter[s[lo]] += 1 + # counter의 값이 0 초과가 된다면, 더이상 window에 해당 문자가 모두 들어있지 않다는 것이므로 remains++ + if counter[s[lo]] > 0: + remains += 1 + + # lo를 오른쪽으로 한 칸 이동 + lo += 1 + + return s[min_lo:min_hi] diff --git a/pacific-atlantic-water-flow/seungriyou.py b/pacific-atlantic-water-flow/seungriyou.py new file mode 100644 index 000000000..92bdc120d --- /dev/null +++ b/pacific-atlantic-water-flow/seungriyou.py @@ -0,0 +1,97 @@ +# https://leetcode.com/problems/pacific-atlantic-water-flow/ + +from typing import List + +class Solution: + def pacificAtlantic_bfs(self, heights: List[List[int]]) -> List[List[int]]: + """ + [Complexity] + - TC: O(m * n) (모든 칸은 최대 한 번씩 방문) + - SC: O(m * n) (queue) + + [Approach] + 거꾸로 각 바다에 맞닿아있는 edges에서부터 시작해서, BFS로 height 값이 gte인 칸(-> 바다에서부터 거꾸로 확인하므로)을 방문하며 기록한다. + pacific & atlantic 쪽 edges에서 시작해서 방문한 칸들의 교집합을 구하면 된다. + """ + from collections import deque + + m, n = len(heights), len(heights[0]) + dr, dc = [-1, 1, 0, 0], [0, 0, -1, 1] + + def bfs(start_cells): + q = deque(start_cells) + visited = set(start_cells) + + while q: + r, c = q.popleft() + + for i in range(4): + nr, nc = r + dr[i], c + dc[i] + + if ( + 0 <= nr < m and 0 <= nc < n # valid한 범위 확인 + and (nr, nc) not in visited # 방문 여부 확인 + and heights[nr][nc] >= heights[r][c] # height 값이 현재 칸보다 gte인지 확인 (** 거꾸로 확인하므로) + ): + q.append((nr, nc)) + visited.add((nr, nc)) + + return visited + + # pacific & atlantic 쪽 edges 모으기 + edges_p, edges_a = [], [] + for r in range(m): + edges_p.append((r, 0)) + edges_a.append((r, n - 1)) + for c in range(n): + edges_p.append((0, c)) + edges_a.append((m - 1, c)) + + # edges로부터 BFS로 방문 + visited_p = bfs(edges_p) + visited_a = bfs(edges_a) + + # pacific과 atlantic의 교집합 반환 + return [list(cell) for cell in visited_p & visited_a] + + def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]: + """ + [Complexity] + - TC: O(m * n) (모든 칸은 최대 한 번씩 방문) + - SC: O(m * n) (call stack) + + [Approach] + BFS 풀이를 DFS로도 접근 가능하다. + """ + m, n = len(heights), len(heights[0]) + dr, dc = [-1, 1, 0, 0], [0, 0, -1, 1] + visited_p, visited_a = set(), set() + + def dfs(r, c, visited): + # base condition + if (r, c) in visited: # 방문 여부 확인 + return + + # 방문 처리 + visited.add((r, c)) + + # recur + for i in range(4): + nr, nc = r + dr[i], c + dc[i] + + if ( + 0 <= nr < m and 0 <= nc < n # valid한 범위 확인 + and heights[nr][nc] >= heights[r][c] # height 값이 현재 칸보다 gte인지 확인 (** 거꾸로 확인하므로) + ): + dfs(nr, nc, visited) + + # edges로부터 DFS로 방문 + for r in range(m): + dfs(r, 0, visited_p) + dfs(r, n - 1, visited_a) + for c in range(n): + dfs(0, c, visited_p) + dfs(m - 1, c, visited_a) + + # pacific과 atlantic의 교집합 반환 + return [list(cell) for cell in visited_p & visited_a] diff --git a/sum-of-two-integers/seungriyou.py b/sum-of-two-integers/seungriyou.py new file mode 100644 index 000000000..9c3b4a6a0 --- /dev/null +++ b/sum-of-two-integers/seungriyou.py @@ -0,0 +1,43 @@ +# https://leetcode.com/problems/sum-of-two-integers/ + +class Solution: + def getSum(self, a: int, b: int) -> int: + """ + [Complexity] + - TC: O(1) + - SC: O(1) + + [Approach] + 덧셈 구현 시 주요한 요소는 carry와 value 값이다. + 예를 들어 이진수 11과 101의 덧셈을 생각해보면, 각 자리에서 발생하는 carry와 value는 다음과 같다. + a = 0 1 1 + b = 1 0 1 + --------------- + value) 1 1 0 (= a ^ b) + carry) 0 0 1 (= a & b) + 이때, 어떤 자리에서 발생한 carry는 한 칸 left shift 되어 다시 덧셈을 수행하듯 적용되어야 함에 주의한다. + value) 1 1 0 + carry) 0 0 1 0 (<<= 1) + + 이 과정을 carry가 0이 될 때까지 반복해나간다. + value) 0 1 0 0 (= a ^ b) + carry) 0 1 0 0 (= (a & b) << 1) + + value) 0 0 0 0 (= a ^ b) + carry) 1 0 0 0 (= (a & b) << 1) + + value) 1 0 0 0 (= a ^ b) -> RESULT + carry) 0 0 0 0 (= (a & b) << 1) -> END + + 하지만 파이썬에서는 int를 32bit 보다 큰 메모리에 저장하므로 음수가 들어올 경우에 대비해 다음의 작업을 추가로 수행해야 한다. + - 음수가 들어온다면 carry가 32bit를 넘어서도 계속 더해질 것이므로, while 문 조건식에의 carry에 32bit masking을 수행한다. + - (b & mask) == 0이지만 b > 0인 경우에는 a의 32bit 범위 밖에 overflow가 존재하는 것이므로, 결과 값에도 32bit masking을 수행한다. + """ + + mask = 0xffffffff # 32bit masking + + # carry가 0이 될 떄까지 반복 (음수가 들어오는 경우, 32bit masking 후 확인) + while (b & mask) != 0: + a, b = a ^ b, (a & b) << 1 # value, carry + + return (a & mask) if b > 0 else a # 음수 (overflow) 처리