From 0703dff104690bed15286e22ee4422f78276c772 Mon Sep 17 00:00:00 2001 From: river20s Date: Mon, 28 Apr 2025 23:22:51 +0900 Subject: [PATCH 1/9] feat: Add Solution to #221 Best Time to Buy And Sell Stock --- best-time-to-buy-and-sell-stock/river20s.py | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 best-time-to-buy-and-sell-stock/river20s.py diff --git a/best-time-to-buy-and-sell-stock/river20s.py b/best-time-to-buy-and-sell-stock/river20s.py new file mode 100644 index 000000000..6c92d8eb3 --- /dev/null +++ b/best-time-to-buy-and-sell-stock/river20s.py @@ -0,0 +1,26 @@ +class Solution(object): + def maxProfit(self, prices): + """ + :type prices: List[int] + :rtype: int + 주어진 prices를 한 번만 순회하면서 + 지금까지의 최솟값을 추적하고, + '현재 가격 - 지금까지의 최솟값'으로 계산되는 이익이 + '지금까지의 최대 이익(초기 0)보다 크면 갱신하여 + 최종 최대 이익 구하기 문제 + Time Complexity: O(n) + Space Complexity: O(1) + """ + max_profit = 0 # 이익이 없을 때 0을 반환하게 초기화 + min_price = float('inf') # 최소 가격 저장, 무한대로 초기화해서 루프 첫 번째 가격부터 최소 가격에 저장 + + for price in prices: + # 최대 이익 갱신 + max_profit = max(max_profit, (price - min_price)) + # 최소 가격 갱신 + min_price = min(min_price, price) + # 최소 이익 갱신 이후 최소 가격 갱신해야 함 + # 최대 이익 자체는 이미 '산' 주식에 대해 계산해야 하므로 + # 사는 동시 팔 수 없음 + + return max_profit From 7262bb7c9a449a6be59dab98236225634859ef93 Mon Sep 17 00:00:00 2001 From: river20s Date: Tue, 29 Apr 2025 23:55:26 +0900 Subject: [PATCH 2/9] feat: Add Solution to Group Anagrams #236 --- group-anagrams/river20s.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 group-anagrams/river20s.py diff --git a/group-anagrams/river20s.py b/group-anagrams/river20s.py new file mode 100644 index 000000000..ab9b25cc8 --- /dev/null +++ b/group-anagrams/river20s.py @@ -0,0 +1,14 @@ +import collections +from typing import List + +class Solution(object): + def groupAnagrams(self, strs): + """ + :type strs: List[str] + :rtype: List[List[str]] + """ + anagram_groups = collections.defaultdict(list) + for s in strs: + key = tuple(sorted(s)) + anagram_groups[key].append(s) + return list(anagram_groups.values()) From 88ba4c1bc619daa3a7d7837eda12026675770195 Mon Sep 17 00:00:00 2001 From: river20s Date: Tue, 29 Apr 2025 23:56:35 +0900 Subject: [PATCH 3/9] feat: Add type hints to groupAnagrams method --- group-anagrams/river20s.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/group-anagrams/river20s.py b/group-anagrams/river20s.py index ab9b25cc8..926d976ea 100644 --- a/group-anagrams/river20s.py +++ b/group-anagrams/river20s.py @@ -2,7 +2,7 @@ from typing import List class Solution(object): - def groupAnagrams(self, strs): + def groupAnagrams(self, strs: List[str]) -> List[List[str]]: """ :type strs: List[str] :rtype: List[List[str]] From bd232b43ecc68a6bbd681f7576eae11429414e2e Mon Sep 17 00:00:00 2001 From: river20s Date: Wed, 30 Apr 2025 00:08:56 +0900 Subject: [PATCH 4/9] docs: Improve docstring and comments for groupAnagrams method --- group-anagrams/river20s.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/group-anagrams/river20s.py b/group-anagrams/river20s.py index 926d976ea..8128c773b 100644 --- a/group-anagrams/river20s.py +++ b/group-anagrams/river20s.py @@ -6,9 +6,27 @@ def groupAnagrams(self, strs: List[str]) -> List[List[str]]: """ :type strs: List[str] :rtype: List[List[str]] + 각 문자열을 순회하면서 알파벳 순으로 정렬한 결과를 tuple로 변환하여 Key로 하고, + 그 Key의 원래 문자열을 Value로 하는 딕셔너리를 통해 애너그램을 그룹화 하는 문제 + 딕셔너리의 Value만 모아서 반환함 + Time Complexity: O(N*K log K) (단, N은 입력 리스트 안의 문자열 개수(리스트 길이), K는 문자열 하나 당 길이) + -> 정렬에 O(K log K)시간 소요, 정렬 작업은 리스트 안의 문자열 개수 N만큼 반복 + Space Complexity: O(N*K) + -> 딕셔너리를 통해 원본 문자열 N개가 모두 저장됨 """ + # collections의 defaultdict 사용하여 Key 없을 때 자동으로 빈 리스트 생성함 + # Key: 정렬된 문자열, Value: 원래 문자열의 리스트 anagram_groups = collections.defaultdict(list) + + # s는 입력 strs의 각 문자열 + # 모든 s에 대해 순회 for s in strs: - key = tuple(sorted(s)) + # s를 알파벳 순으로 정렬하고, Key로 쓰기 위해 튜플로 변환함 + # sorted(s)는 각 문자를 요소로 갖는 리스트(예: ['h', 'i']) + # 튜플로 바꿔서 Key로 쓸 수 있게 하였음 + key = tuple(sorted(s)) + # defaultdict 사용하므로, Key 존재 여부 확인 불필요 + # 해당 Key의 Value에 원래 문자열 s 추가 anagram_groups[key].append(s) + # 딕셔너리 Value만 모아 반환 return list(anagram_groups.values()) From a070e1bd67d6e6198a41d3990466259ed4bb5a58 Mon Sep 17 00:00:00 2001 From: river20s Date: Thu, 1 May 2025 23:59:32 +0900 Subject: [PATCH 5/9] Add Solution --- encode-and-decode-strings/river20s.py | 0 implement-trie-prefix-tree/river20s.py | 53 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 encode-and-decode-strings/river20s.py create mode 100644 implement-trie-prefix-tree/river20s.py diff --git a/encode-and-decode-strings/river20s.py b/encode-and-decode-strings/river20s.py new file mode 100644 index 000000000..e69de29bb diff --git a/implement-trie-prefix-tree/river20s.py b/implement-trie-prefix-tree/river20s.py new file mode 100644 index 000000000..d3a98cbaa --- /dev/null +++ b/implement-trie-prefix-tree/river20s.py @@ -0,0 +1,53 @@ +class TrieNode: + # 트라이 노드 + def __init__(self): + self.children = {} + # 단어의 끝을 나타내기 위한 플래그 + self.isEndOfWord = False + +class Trie: + # 트라이 + def __init__(self): + self.root = TrieNode() + + def insert(self, word: str) -> None: + # 삽입 연산 + # Time Complexity: O(L) (L은 word의 길이) + # Space Complexity: O(M) (M은 트라이에 삽입된 모든 단어들의 총 문자 개수 합) + currentNode = self.root + + for char in word: + if char not in currentNode.children: + currentNode.children[char] = TrieNode() + currentNode = currentNode.children[char] + currentNode.isEndOfWord = True + + def search(self, word: str) -> bool: + # 완전 검색 연산 + # Time Complexity: O(L) (L은 word의 길이) + # Space Complexity: O(M) + currentNode = self.root + for char in word: + if char not in currentNode.children: + return False + currentNode = currentNode.children[char] + return currentNode.isEndOfWord + + def startsWith(self, prefix: str) -> bool: + # 접두사 일치 검사 연산 + # Time Complexity: O(P) (P는 Prefix의 길이) + # Space Complexity: O(M) + currentNode = self.root + for char in prefix: + if char not in currentNode.children: + return False + currentNode = currentNode.children[char] + return True + + + +# Your Trie object will be instantiated and called as such: +# obj = Trie() +# obj.insert(word) +# param_2 = obj.search(word) +# param_3 = obj.startsWith(prefix) From b7da02533909d5067088a003a0b77205b876eb1b Mon Sep 17 00:00:00 2001 From: river20s Date: Fri, 2 May 2025 00:03:51 +0900 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- encode-and-decode-strings/river20s.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 encode-and-decode-strings/river20s.py diff --git a/encode-and-decode-strings/river20s.py b/encode-and-decode-strings/river20s.py deleted file mode 100644 index e69de29bb..000000000 From ff51e87405bac73eaf3358922ec09ed2fba0219c Mon Sep 17 00:00:00 2001 From: river20s Date: Fri, 2 May 2025 00:11:36 +0900 Subject: [PATCH 7/9] =?UTF-8?q?docs:=20maxProfit=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EB=A1=9C=EC=A7=81=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=98=EC=98=81=ED=95=98=EC=97=AC,=20=EC=B5=9C?= =?UTF-8?q?=EB=8C=80=20=EC=9D=B4=EC=9D=B5(max=5Fprofit)=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=EA=B3=BC=20=EC=B5=9C=EC=86=8C=20=EA=B0=80=EA=B2=A9(mi?= =?UTF-8?q?n=5Fprice)=20=EA=B0=B1=EC=8B=A0=20=EC=88=9C=EC=84=9C=EB=A5=BC?= =?UTF-8?q?=20=EC=84=A4=EB=AA=85=ED=95=98=EB=8A=94=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=EC=9D=B4=20=EC=8B=A4=EC=A0=9C=20=EC=BD=94=EB=93=9C=EC=99=80=20?= =?UTF-8?q?=EB=B0=98=EB=8C=80=EB=A1=9C=20=EB=90=98=EC=96=B4=20=EC=9E=88?= =?UTF-8?q?=EB=8D=98=20=EC=98=A4=EB=A5=98=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=9C=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=AA=85=EC=9D=84=20=EC=A3=BC=EC=84=9D=EC=97=90=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98=20=ED=91=9C=EA=B8=B0=ED=95=B4=20=EB=8D=94=20?= =?UTF-8?q?=EC=89=BD=EA=B2=8C=20=EC=9D=BD=ED=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- best-time-to-buy-and-sell-stock/river20s.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/best-time-to-buy-and-sell-stock/river20s.py b/best-time-to-buy-and-sell-stock/river20s.py index 6c92d8eb3..ce18b6901 100644 --- a/best-time-to-buy-and-sell-stock/river20s.py +++ b/best-time-to-buy-and-sell-stock/river20s.py @@ -19,7 +19,7 @@ def maxProfit(self, prices): max_profit = max(max_profit, (price - min_price)) # 최소 가격 갱신 min_price = min(min_price, price) - # 최소 이익 갱신 이후 최소 가격 갱신해야 함 + # 최대 이익(max_profit) 갱신 이후 최소 가격(min_price) 갱신해야 함 # 최대 이익 자체는 이미 '산' 주식에 대해 계산해야 하므로 # 사는 동시 팔 수 없음 From 188ef5f9a756a7522b0beee34f1d4485162d951e Mon Sep 17 00:00:00 2001 From: river20s Date: Sat, 3 May 2025 00:27:33 +0900 Subject: [PATCH 8/9] feat: Add Initial implementation to Word Break #271 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - <139. Word Break> 문제에 대한 초기 접근 방식으로 재귀 호출과 트라이를 사용하여 구현함 - 트라이를 통해 딕셔너리 단어 검색 효율은 개선했으나 재귀 호출 과정에서 발생하는 중복 연산 문제로 인해 TLE 발생을 확인하였음 - 메모이제이션 또는 DP로 최적화 예정임 - 문제 이슈: #271 --- word-break/river20s.py | 66 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 word-break/river20s.py diff --git a/word-break/river20s.py b/word-break/river20s.py new file mode 100644 index 000000000..e405bba5e --- /dev/null +++ b/word-break/river20s.py @@ -0,0 +1,66 @@ +class TrieNode: + def __init__(self): + self.children = {} + self.isEndOfWord = False + +class Trie: + def __init__(self): + self.root = TrieNode() + + def insert(self, word: str) -> None: + currentNode = self.root + for char in word: + if char not in currentNode.children: + currentNode.children[char] = TrieNode() + currentNode = currentNode.children[char] + currentNode.isEndOfWord = True + +# <-- 여기까지 Trie 구현을 위한 TrieNode와 Trie 클래스 +# --> 여기부터 Word Break 문제 푸는 Solution 클래스 + +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + + # 1. 트라이 구축 + # wordDict 모든 단어 -> 트라이 넣기 + trie = Trie() + for word in wordDict: + trie.insert(word) + + n = len(s) # 문자열 s의 길이, 나중에 인덱스 끝까지 도달했는지 확인하기 위해 사용함 + + # 2. 재귀 함수 정의 + # canBreak(start_index): s[strat_index:] 부분을 분할할 수 있는지 확인 + def canBreak(start_index: int) -> bool: + + # 베이스 케이스 + # 시작 인덱스(start_index)가 문자열 끝에 도달했다면 성공 + if start_index == n: + return True + + # 현재 start_index부터 시작하는 가능한 모든 단어를 트라이를 이용해 찾고 + # 각 단어에 대해 나머지 부분이 분할 가능한지 재귀적으로 확인 + currentNode = trie.root + + for i in range(start_index, n): + char = s[i] + + # 현재 문자가 트라이 경로에 없다면 해당 트라이 탐색은 더이상 진행하지 않음 + if char not in currentNode.children: + break + + # 트라이의 다음 노드로 이동 + currentNode = currentNode.children[char] + + # 이동한 노드가 단어의 끝이라면 + if currentNode.isEndOfWord: + # 나머지 부분 s[i+1:]에 대해서도 분할 가능한지 재귀 호출 + if canBreak(i + 1): + # 나머지 부분 분할 성공 => 전체 분할 가능 + return True + # start_index부터 시작하는 모든 가능한 단어 분할을 시도했으나 + # 성공적인 경로를 찾지 못했다면 + return False + + # 3. 재귀 함수 호출 시작 + return canBreak(0) From fcf21f76e88b9993d27ef1ac0a44a91e54f8501a Mon Sep 17 00:00:00 2001 From: river20s Date: Sat, 3 May 2025 22:01:46 +0900 Subject: [PATCH 9/9] =?UTF-8?q?perf:=20Apply=20memoization=20to=20optimize?= =?UTF-8?q?=20performance=20#271=20-=20=EA=B8=B0=EC=A1=B4=20=EC=9E=AC?= =?UTF-8?q?=EA=B7=80=20+=20=ED=8A=B8=EB=9D=BC=EC=9D=B4=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=97=90=EC=84=9C=20=EB=B0=9C=EC=83=9D=ED=95=98?= =?UTF-8?q?=EB=8D=98=20TLE=20=EB=AC=B8=EC=A0=9C=EB=A5=BC=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=EB=A9=94?= =?UTF-8?q?=EB=AA=A8=EC=9D=B4=EC=A0=9C=EC=9D=B4=EC=85=98=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=98=80=EC=9D=8C=20-=20`canBreak(s?= =?UTF-8?q?tart=5Findex)`=20=ED=95=A8=EC=88=98=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EB=A5=BC=20`memo`=20=EB=94=95=EC=85=94=EB=84=88=EB=A6=AC?= =?UTF-8?q?=EC=97=90=20=EC=BA=90=EC=8B=B1=ED=95=98=EC=97=AC=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=ED=95=9C=20`start=5Findex`=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=A4=91=EB=B3=B5=20=EA=B3=84=EC=82=B0=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- word-break/river20s.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/word-break/river20s.py b/word-break/river20s.py index e405bba5e..d73a033f7 100644 --- a/word-break/river20s.py +++ b/word-break/river20s.py @@ -29,10 +29,19 @@ def wordBreak(self, s: str, wordDict: List[str]) -> bool: n = len(s) # 문자열 s의 길이, 나중에 인덱스 끝까지 도달했는지 확인하기 위해 사용함 + # <<<--- 메모이제이션 캐시 초기화 ---<<< + # key: start_index, value: s[start_index:] 분할 가능 여부 (True/False) + memo = {} + # 2. 재귀 함수 정의 # canBreak(start_index): s[strat_index:] 부분을 분할할 수 있는지 확인 def canBreak(start_index: int) -> bool: + # <<<--- 캐시 확인 ---<<< + # 이 start_index에 대한 결과가 이미 memo에 있으면 바로 반환 + if start_index in memo: + return memo[start_index] + # 베이스 케이스 # 시작 인덱스(start_index)가 문자열 끝에 도달했다면 성공 if start_index == n: @@ -41,7 +50,6 @@ def canBreak(start_index: int) -> bool: # 현재 start_index부터 시작하는 가능한 모든 단어를 트라이를 이용해 찾고 # 각 단어에 대해 나머지 부분이 분할 가능한지 재귀적으로 확인 currentNode = trie.root - for i in range(start_index, n): char = s[i] @@ -57,9 +65,13 @@ def canBreak(start_index: int) -> bool: # 나머지 부분 s[i+1:]에 대해서도 분할 가능한지 재귀 호출 if canBreak(i + 1): # 나머지 부분 분할 성공 => 전체 분할 가능 + # <<<--- 성공 결과 캐시에 저장 ---<<< + memo[start_index] = True return True # start_index부터 시작하는 모든 가능한 단어 분할을 시도했으나 # 성공적인 경로를 찾지 못했다면 + # <<<--- 실패 결과 캐시에 저장 ---<<< + memo[start_index] = False return False # 3. 재귀 함수 호출 시작