diff --git a/3sum/haklee.py b/3sum/haklee.py
new file mode 100644
index 000000000..b3a862dd9
--- /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 000000000..b62c03479
--- /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 000000000..f58417d04
--- /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 000000000..e178c5b7b
--- /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 000000000..26e1c3a12
--- /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)