diff --git a/3sum/haklee.py b/3sum/haklee.py new file mode 100644 index 00000000..b3a862dd --- /dev/null +++ b/3sum/haklee.py @@ -0,0 +1,76 @@ +"""TC: O(n^2), SC: O(n^2) + +아이디어: +- 합이 0(즉, 고정값)이 되는 세 수를 찾는 것이 문제. +- 세 수 중 하나가 고정되어 있다면, 그리고 숫자들이 들어있는 리스트가 정렬되어 있다면 투 포인터 사용 가능. + - 투 포인터 테크닉은 검색하면 많이 나오므로 아주 자세한 설명은 생략하겠다. + - 이 문제에서는 리스트의 가장 작은 값과 가장 큰 값에 포인터를 둘 것이다. + - 그리고 이 두 값의 합이 + - 원하는 결과보다 작으면 작은 값의 포인터를 큰 쪽으로 옮기고, + - 원하는 결과보다 크면 큰 값의 포인터를 작은 쪽으로 옮긴다. + - 이 과정을 반복하면서 원하는 쌍을 찾는 것이 관건. +- 고정된 숫자를 정렬된 리스트의 가장 작은 값부터 큰 값으로 하나씩 옮겨가면 중복 없이 탐색이 가능하다. + - 이때 투 포인터를 쓸 구간은 고정된 숫자 뒤에 오는 숫자들로 둔다. + - 코드를 보는 것이 이해가 더 빠를 것이다.. + +SC: +- 자세한 설명은 TC 분석에서 확인 가능. +- 종합하면 O(n^2). + +TC: +- nums를 sort하는 과정에서 O(n * log(n)) +- 정렬된 nums를 모두 순회. + - 그리고 각 순회마다 n-1, n-2, ..., 2 크기의 구간에서 투 포인터 사용. + - 투 포인터를 사용할때 단순 사칙연산 및 비교연산만 사용하므로 O(1). + - 투 포인터 사용시 매 계산마다 포인터 사이의 거리가 1씩 줄어든다(s가 올라가든 e가 내려가든). + - (SC) 매 계산마다 최대 한 번 solution을 추가하는 연산을 한다. + - 그러므로 각 순회마다 C * (n-1), C * (n-2), ..., C * 1의 시간이 들어감. + - (SC) 비슷하게, 매 순회마다 위와 같은 꼴로 solution 개수가 더해질 수 있다. + - 종합하면 O(n^2) +- 총 O(n^2) +""" + +from collections import Counter + + +class Solution: + def threeSum(self, nums: List[int]) -> List[List[int]]: + # 커팅. 어차피 세 쌍의 숫자에 등장할 수 있는 같은 숫자 개수가 최대 3개이므로, + # 처음 주어진 nums에 같은 숫자가 네 번 이상 등장하면 세 번만 나오도록 바꿔준다. + # 이 처리를 하면 같은 숫자가 많이 반복되는 케이스에서 시간 개선이 있을 수 있다. + # Counter 쓰는 데에 O(n), 새로 tmp_nums 리스트를 만드는 데에 O(n)의 시간이 들어가므로 + # 최종적인 시간 복잡도에 영향을 주지는 않는다. + tmp_nums = [] + for k, v in Counter(nums).items(): + tmp_nums += [k] * min(v, 3) + + # 여기부터가 주된 구현. + sorted_nums = sorted(tmp_nums) # TC: O(n * log(n)) + nums_len = len(tmp_nums) + + sol = set() + for i in range(nums_len): # TC: O(n) + if i > 0 and sorted_nums[i] == sorted_nums[i - 1]: + # 커팅. 고정 값이 이미 한 번 사용되었던 값이면 스킵해도 괜찮다. + continue + s, e = i + 1, nums_len - 1 + while s < e: + # 이 while문 전체에서 TC O(n). + v = sorted_nums[s] + sorted_nums[e] + if v == -sorted_nums[i]: + # i < s < e 이므로, 이 순서대로 숫자는 정렬된 상태다. + # 즉, 같은 값을 사용한 순서만 다른 쌍을 걱정하지 않아도 된다. + sol.add((sorted_nums[i], sorted_nums[s], sorted_nums[e])) + if v < -sorted_nums[i]: + # s, e의 두 값을 더한 것이 원하는 값보다 작으면, 작은 쪽에 있는 포인터를 + # 더 큰 숫자가 있는 쪽으로 옮기면 된다. + # 여기서도 중복 값 커팅을 하려면 할 수 있겠지만, 이 커팅을 안 하려고 + # 맨 앞에서 같은 숫자들을 미리 최대한 제거해두었다. + s += 1 + else: + # s, e의 두 값을 더한 것이 원하는 값보다 크면, 큰 쪽에 있는 포인터를 + # 더 작은 숫자가 있는 쪽으로 옮기면 된다. + # 여기서도 중복 값 커팅을 하려면 할 수 있겠지만, 이 커팅을 안 하려고 + # 맨 앞에서 같은 숫자들을 미리 최대한 제거해두었다. + e -= 1 + return sol # 타입힌트를 따르지 않아도 제출은 된다... diff --git a/best-time-to-buy-and-sell-stock/haklee.py b/best-time-to-buy-and-sell-stock/haklee.py new file mode 100644 index 00000000..b62c0347 --- /dev/null +++ b/best-time-to-buy-and-sell-stock/haklee.py @@ -0,0 +1,23 @@ +"""TC: O(n), SC: O(1) + +아이디어: +- 특정 시점에서 stock을 팔았을때 최고 수익이 나려면 이전 가격 중 가장 낮은 가격에 stock을 사야 한다. +- 모든 시점에 대해 위의 값을 계산하면 전체 기간 중 최고 수익을 구할 수 있다. + - 이를 위해서 특정 시점 이전까지의 가격 중 가장 싼 가격인 minp값을 관리하고, + - 각 시점의 가격에서 minp값을 빼서 `현재 최고 수익`을 구한 다음에, + - `전체 최고 수익`을 max(`현재 최고 수익`, `전체 최고 수익`)으로 업데이트 한다. + +SC: +- minp, profit(`전체 최고 수익`) 값을 관리한다. O(1). + +TC: +- prices의 가격을 순회하면서 더하기, min, max 연산을 한다. O(n). +""" + + +class Solution: + def maxProfit(self, prices: List[int]) -> int: + minp, profit = prices[0], 0 + for p in prices: + profit = max(profit, p - (minp := min(minp, p))) + return profit diff --git a/group-anagrams/haklee.py b/group-anagrams/haklee.py new file mode 100644 index 00000000..f58417d0 --- /dev/null +++ b/group-anagrams/haklee.py @@ -0,0 +1,50 @@ +"""TC: O(n * l * log(l)), SC: O(n * l) + +전체 문자열 개수 n개, 문자열 최대 길이 l. + +아이디어: +- 모든 문자열에 대해 해당 문자열을 이루는 문자 조합으로 고유한 키를 만드는 것이 주된 아이디어. + - 문자열을 해체해서 sort한 다음 이를 바로 tuple로 만들어서 키로 사용했다. +- list를 리턴하라고 되어있는 것을 `dict_values`로 리턴했지만 문제가 생기지 않아서 그냥 제출했다. + +SC: +- dict 관리. + - 키값 최대 n개, 각각 최고 길이 l. O(n * l). + - 총 아이템 n개, 각각 최고 길이 l. O(n * l). +- 종합하면 O(n * l). + +TC: +- strs에 있는 각 아이템을 sort함. O(l * log(l)) +- 위의 과정을 n번 반복. +- 종합하면 O(n * l * log(l)). +""" + + +class Solution: + def groupAnagrams(self, strs: List[str]) -> List[List[str]]: + d = {} + for s in strs: + # k값 계산이 오른쪽에서 먼저 이루어지는군요?! + d[k] = d.get(k := tuple(sorted(s)), []) + [s] + return d.values() + + +"""TC: O(n * l * log(l)), SC: O(n * l) +각 단어를 sort하는 것보다 단어를 이루고 있는 문자를 카운터로 세어서 이 카운터를 키로 쓰는 것이 +시간복잡도에 더 좋을 수도 있다. Counter를 써서 알파벳 개수를 dict로 만든 다음 json.dumps로 str +로 만들어버리자. + +실제 이 솔루션을 제출하면 성능이 별로 좋지 않은데, l값이 작아서 위의 과정을 처리하는 데에 오버헤드가 +오히려 더 붙기 때문을 추정된다. +""" + +from collections import Counter +from json import dumps + + +class Solution: + def groupAnagrams(self, strs: List[str]) -> List[List[str]]: + d = {} + for s in strs: + d[k] = d.get(k := dumps(Counter(s), sort_keys=True), []) + [s] + return d.values() diff --git a/implement-trie-prefix-tree/haklee.py b/implement-trie-prefix-tree/haklee.py new file mode 100644 index 00000000..e178c5b7 --- /dev/null +++ b/implement-trie-prefix-tree/haklee.py @@ -0,0 +1,38 @@ +""" +단순한 trie 구현이므로 분석은 생략합니다. +""" + + +class Trie: + def __init__(self): + self.next: dict[str, Trie] = {} + self.end: bool = False + + def insert(self, word: str) -> None: + cur = self + + for c in word: + cur.next[c] = cur.next.get(c, Trie()) + cur = cur.next[c] + + cur.end = True + + def search(self, word: str) -> bool: + cur = self + + for c in word: + if c not in cur.next: + return False + cur = cur.next[c] + + return cur.end + + def startsWith(self, prefix: str) -> bool: + cur = self + + for c in prefix: + if c not in cur.next: + return False + cur = cur.next[c] + + return True diff --git a/word-break/haklee.py b/word-break/haklee.py new file mode 100644 index 00000000..26e1c3a1 --- /dev/null +++ b/word-break/haklee.py @@ -0,0 +1,105 @@ +"""TC: ?, SC: O(w * l + s^2) + +쪼개고자 하는 단어의 길이 s, wordDict에 들어가는 단어 개수 w, wordDict에 들어가는 단어 최대 길이 l + +아이디어: +- trie를 구현하는 문제에서 만든 클래스를 여기서 한 번 사용해보자. +- 주어진 단어들을 전부 trie에 집어넣는다. +- 쪼개려고 하는 단어를 trie를 통해서 매칭한다. (`Trie` 클래스의 `find_prefix_indices` 메소드) + - 단어를 앞에서부터 한 글자씩 매칭하면서 + - 중간에 end가 있는 노드를 만나면 `prefix_indices`에 값을 추가한다. "이 단어는 이 index + 에서 쪼개질 수 있어요!" 하는 의미를 담은 index라고 보면 된다. + - 글자 매칭이 실패하면 매칭 종료. + - 매칭이 끝나고 나서 `prefix_indices`를 리턴한다. + - e.g.) wordDict = ["leet", "le", "code"], s = "leetcode"일때 + - 첫 글자 l 매칭. 아무 일도 일어나지 않음. + - 다음 글자 e 매칭. 이 노드는 end가 true다. "le"에 대응되기 때문. prefix_indices에 2 추가. + - 다음 글자 e 매칭. 아무 일도 일어나지 않음. + - 다음 글자 t 매칭. 이 노드는 "leet"에 대응되어 end가 true다. prefix_indices에 4 추가. + - 다음 글자 c 매칭. 매칭 실패 후 종료. + - prefix_indices = [2, 4]를 리턴. +- 위의 매칭 과정이 끝나면 주어진 단어를 쪼갤 수 있는 방법들이 생긴다. + - 쪼개진 단어에서 뒷 부분을 취한다. + - e.g.) wordDict = ["leet", "le", "code"], s = "leetcode", prefix_indices = [2, 4] + - prefix_indices의 각 아이템을 돌면서 + - s[2:]를 통해서 "le/etcode" 중 뒷 부분 "etcode"를 취할 수 있다. + - s[4:]를 통해서 "leet/code" 중 뒷 부분 "code"를 취할 수 있다. + - 만약 뒷 부분이 빈 문자열("")이 될 경우 탐색에 성공한 것이다. + - 코드 상에서는 빈 문자열로 탐색을 시도할 경우 탐색 성공의 의미로 true 반환. + - 만약 prefix_indices가 빈 리스트로 온다면 쪼갤 수 있는 방법이 없다는 뜻이므로 탐색 실패다. + - 그 외에는 취한 뒷 부분들에 대해 각각 다시 쪼개는 것을 시도한다. +- 위의 과정에서 쪼개는 것을 이미 실패한 단어를 fail_list라는 set으로 관리하여 중복 연산을 막는다. + - 즉, memoization을 활용. + +SC: +- trie 생성. 최악의 경우 O(w * l) +- fail list에 들어갈 수 있는 단어 길이 + - 1, 2, ..., s + - O(s^2) + - 이걸 전체 단어를 저장하는 것 대신 맨 앞 글자의 index를 저장하는 식으로 구현하면 O(s)가 될 것이다. + 여기에서는 구현 생략. +- find함수의 호출 스택 최악의 경우 한 글자씩 앞에서 빼면서 탐색 시도, O(s) +- 종합하면 O(w * l) + O(s^2) + O(s) = O(w * l + s^2) + +TC: +- ??? +""" + + +class Trie: + def __init__(self): + self.next: dict[str, Trie] = {} + self.end: bool = False + + def insert(self, word: str) -> None: + cur = self + + for c in word: + cur.next[c] = cur.next.get(c, Trie()) + cur = cur.next[c] + + cur.end = True + + def find_prefix_indices(self, word: str) -> list[str]: + prefix_indices = [] + ind = 0 + cur = self + + for c in word: + ind += 1 + if c not in cur.next: + break + cur = cur.next[c] + if cur.end: + prefix_indices.append(ind) + + return prefix_indices + + +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + # init + trie = Trie() + for word in wordDict: + trie.insert(word) + + fail_list = set() + + # recursive find + def find(word: str) -> bool: + # 단어의 앞에서 쪼갤 수 있는 경우를 전부 찾아서 쪼개고 + # 뒤에 남은 단어를 다시 쪼개는 것을 반복한다. + if word == "": + return True + + if word in fail_list: + return False + + cut_indices = trie.find_prefix_indices(word) + result = any([find(word[i:]) for i in cut_indices]) + if not result: + fail_list.add(word) + + return result + + return find(s)