Trong phỏng vấn DSA, thuật toán chỉ là một nửa câu chuyện. Nửa còn lại là viết được nó ra Python nhanh và không sai cú pháp dưới áp lực thời gian. Bài này tập trung vào nửa thứ hai: những syntax, idiom, và cấu trúc dữ liệu mà bạn cần gọi ra trong đầu ngay khi đọc đề — thay vì loay hoay nhớ bisect viết thế nào hay tại sao [[0]*n]*m lại sai.
Phần lớn nội dung là syntax Python thuần (list, string, dict, heap, sorting…). Các phần cấu trúc dữ liệu và pattern đứng sau, gọn hơn, chủ yếu để bạn copy template khi cần.
Summary (Cách đọc bài này)
- Đọc kỹ phần Syntax — đây là phần dày nhất và đáng nhớ nhất. Mỗi đoạn code là một idiom thực dùng trong LeetCode.
- Cấu trúc dữ liệu & Pattern — lướt qua một lần để biết có gì, rồi quay lại copy template khi gặp bài.
- Đừng học thuộc lòng. Hiểu tại sao một idiom nhanh hơn cái khác thì sẽ nhớ lâu hơn.
Phần 1 — Syntax Python cần nhớ
Đây là phần quan trọng nhất. Nếu chỉ có 30 phút trước buổi phỏng vấn, hãy đọc phần này.
List — con dao đa năng
List là cấu trúc bạn dùng nhiều nhất: vừa làm mảng, vừa làm stack, vừa làm buffer. Điểm cần nhớ là chi phí thao tác: thêm/xóa ở cuối là O(1), nhưng ở đầu hay giữa là O(n) vì phải dịch toàn bộ phần tử.
nums = [1, 2, 3, 4, 5]
# --- Truy cập & cắt lát (slicing) ---nums[0] # 1 — phần tử đầunums[-1] # 5 — phần tử cuối, không cần len()nums[1:4] # [2,3,4] — từ index 1 đến trước 4nums[:3] # [1,2,3] — 3 phần tử đầunums[2:] # [3,4,5] — từ index 2 đến hếtnums[::-1] # [5,4,3,2,1] — đảo ngược (tạo bản copy)nums[::2] # [1,3,5] — lấy cách 1 phần tử (step=2)nums[:] # bản copy nông (shallow copy)
# --- Thêm / xóa ---nums.append(6) # thêm cuối — O(1)nums.pop() # lấy & xóa cuối — O(1)nums.pop(0) # lấy & xóa đầu — O(n), nên tránhnums.insert(0, 0) # chèn vào đầu — O(n)nums.extend([7,8]) # nối nhiều phần tửnums += [9] # tương đương extenddel nums[1] # xóa theo indexnums.remove(3) # xóa theo GIÁ TRỊ (lần đầu gặp)
# --- Tra cứu ---3 in nums # True/False — O(n) với listnums.index(3) # vị trí đầu tiên của 3nums.count(3) # số lần xuất hiệnKhi cần biến đổi tại chỗ thay vì tạo list mới, hãy dùng các method in-place — chúng tiết kiệm bộ nhớ và thường là điều interviewer mong đợi:
nums.sort() # sắp xếp tại chỗ, O(n log n)nums.sort(reverse=True) # giảm dầnnums.reverse() # đảo ngược tại chỗnums[::-1] = nums # cũng đảo ngược (gán slice)Tip (Ba cái bẫy hay gặp với list)
- Copy nhầm tham chiếu:
b = akhông copy, hai biến trỏ cùng list. Muốn copy thật:b = a[:]hoặcb = list(a). - Pop đầu liên tục:
list.pop(0)là O(n). Nếu cần FIFO, đổi sangcollections.deque. - Khởi tạo ma trận sai: xem callout đỏ ở phần list comprehension bên dưới.
Duyệt: enumerate, zip, unpacking
Đây là nhóm idiom giúp code ngắn và “Pythonic”. Interviewer đánh giá cao khi bạn dùng chúng thay vì quản lý index thủ công.
# enumerate — vừa lấy index vừa lấy giá trịfor i, val in enumerate(nums): print(i, val)for i, val in enumerate(nums, start=1): # đánh số từ 1 print(i, val)
# zip — duyệt nhiều list song songfor a, b in zip(list1, list2): ...for (r, c), name in zip(coords, names): # kết hợp unpacking ...
# zip(*matrix) — "xoay" hàng thành cột (transpose)rows = [[1, 2, 3], [4, 5, 6]]cols = list(zip(*rows)) # [(1,4), (2,5), (3,6)]
# Unpackingfirst, *middle, last = [1, 2, 3, 4, 5] # first=1, middle=[2,3,4], last=5a, b = b, a # hoán đổi không cần biến tạmreturn lo, hi # trả nhiều giá trị (là tuple)String — bất biến, nên cẩn thận khi “sửa”
String immutable: mọi thao tác “thay đổi” thực ra tạo string mới (O(n)). Vì thế nối chuỗi trong vòng lặp bằng += là cái bẫy O(n²) kinh điển — luôn gom vào list rồi join một lần.
s = "Hello World"
# Slicing & truy cập — giống hệt lists[0] # 'H's[-1] # 'd's[::-1] # đảo ngược chuỗi
# Tách & ghép"a,b,c".split(",") # ['a', 'b', 'c']"a b c".split() # ['a', 'b', 'c'] — tách theo mọi khoảng trắng"".join(['a', 'b', 'c']) # 'abc' — cách nối chuỗi ĐÚNG"-".join(['1', '2']) # '1-2'
# Kiểm tra & tìm kiếms.startswith("He"), s.endswith("ld")s.find("o") # 4 — index đầu tiên, -1 nếu không cós.count("o") # 2"abc".isalpha(), "123".isdigit(), "a1".isalnum()
# Biến đổi (trả chuỗi MỚI)s.lower(), s.upper()" hi ".strip() # 'hi' — bỏ khoảng trắng hai đầus.replace("o", "0")
# Ký tự ↔ mã số (cực hữu ích cho bài về bảng chữ cái)ord('a') # 97chr(97) # 'a'ord('c') - ord('a') # 2 — index trong bảng 26 chữWarning (Quy tắc vàng khi build string)
Đừng bao giờ result += char trong vòng lặp dài. Hãy gom vào list rồi nối một lần:
parts = []for ch in s: parts.append(transform(ch))result = "".join(parts) # O(n) thay vì O(n²)Dict (HashMap) — vũ khí chính để tăng tốc
Rất nhiều bài medium/hard chuyển từ O(n²) xuống O(n) nhờ một cái dict. Cần thuộc lòng các cách truy cập an toàn để không vướng KeyError.
d = {"a": 1, "b": 2}
# Truy cập AN TOÀN — không gây lỗi nếu thiếu keyd.get("c") # None — không lỗid.get("c", 0) # 0 — trả giá trị mặc địnhd.setdefault("c", []).append(1) # nếu chưa có thì tạo rồi thao tác
# Pattern đếm tần suất (rất hay dùng)freq = {}for x in nums: freq[x] = freq.get(x, 0) + 1
# Duyệtfor key in d: # duyệt key ...for key, val in d.items(): # duyệt cả cặp ...d.keys(), d.values(), d.items()
# Thao tác khácd.pop("a", None) # xóa & trả về, không lỗi nếu thiếu"a" in d # check key tồn tại — O(1){**d1, **d2} # gộp hai dict (d2 ghi đè trùng key)defaultdict & Counter — viết ngắn hơn nữa
defaultdict tự tạo giá trị mặc định nên bạn khỏi phải kiểm tra key. Counter là “dict đếm” có sẵn, lý tưởng cho bài tần suất, anagram, top-k.
from collections import defaultdict, Counter
# defaultdict — bỏ qua bước kiểm tra keygraph = defaultdict(list) # mặc định []graph[u].append(v)
groups = defaultdict(list)for word in words: groups[tuple(sorted(word))].append(word) # gom nhóm anagram
seen = defaultdict(int) # mặc định 0seen[x] += 1
# Counter — đếm trong 1 dòngcnt = Counter("aabbbc") # Counter({'b':3, 'a':2, 'c':1})cnt = Counter(nums)cnt.most_common(2) # [('b', 3), ('a', 2)] — top 2cnt['a'] # 2, trả 0 nếu thiếu (không lỗi)cnt['z'] += 1 # tự khởi tạolist(cnt.elements()) # bung ngược lại thành các phần tử
# Counter còn cộng/trừ được như multisetCounter("aab") - Counter("a") # Counter({'a':1, 'b':1})Set — kiểm tra tồn tại & phép toán tập hợp
seen = set()seen.add(x)seen.discard(x) # xóa, không lỗi nếu thiếux in seen # O(1) trung bình — lý do chính để dùng set
# Phép toán tập hợpa | b # hợp (union)a & b # giao (intersection)a - b # hiệu (difference)a ^ b # khác nhau (symmetric difference)
# Lưu tọa độ đã thăm trong ma trận/đồ thịvisited = set()visited.add((r, c)) # tuple hashable nên dùng làm phần tử set đượcTuple & “pair” — key bất biến
Tuple immutable và hashable, nên đây là lựa chọn để làm key cho dict/set hoặc làm “state” trong DP. Đó là khác biệt cốt lõi so với list (mutable, không hashable).
point = (x, y) # cặp tọa độvisited.add((r, c)) # tuple làm phần tử setdp[(i, mask)] = value # tuple làm key DP state
# Sắp xếp theo nhiều tiêu chí: tuple so sánh theo thứ tự từ tráipairs.sort(key=lambda p: (p[1], -p[0])) # tăng theo p[1], rồi giảm theo p[0]List comprehension & generator
Comprehension là cách tạo list/dict/set gọn và nhanh. Generator (dùng ()) thì “lười” — sinh từng phần tử khi cần, tiết kiệm bộ nhớ khi chỉ cần duyệt một lần.
squares = [x * x for x in range(10)]evens = [x for x in nums if x % 2 == 0]flat = [x for row in matrix for x in row] # làm phẳng ma trận
# Dict & set comprehensionidx = {v: i for i, v in enumerate(nums)} # value -> indexuniq = {x % k for x in nums}
# Generator — không tạo list trung giantotal = sum(x * x for x in nums)ok = any(x < 0 for x in nums) # dừng ngay khi gặp Trueallp = all(x > 0 for x in nums)Danger (Lỗi khởi tạo ma trận 2D — RẤT hay gặp)
# SAI — m hàng cùng trỏ về MỘT list, sửa 1 hàng là sửa tất cảgrid = [[0] * n] * m
# ĐÚNG — mỗi hàng là một list độc lậpgrid = [[0] * n for _ in range(m)]Số học & thao tác bit
Python có vài toán tử dễ quên nhưng cực hữu ích trong DSA: chia lấy nguyên //, lấy dư %, divmod, và nhóm toán tử bit cho các bài bitmask.
7 // 2 # 3 — chia lấy phần nguyên (làm tròn xuống)7 % 3 # 1 — số dưdivmod(7, 3) # (2, 1) — thương và dư cùng lúc2 ** 10 # 1024 — lũy thừaabs(-5), min(a, b), max(a, b)
# Số nguyên Python KHÔNG bị tràn (overflow) — thoải mái với số lớn
# --- Thao tác bit (cho bitmask DP, subset) ---a & b # ANDa | b # ORa ^ b # XOR — dùng tìm số xuất hiện lẻ lần~a # NOTa << 1 # dịch trái (= nhân 2)a >> 1 # dịch phải (= chia 2)
x & 1 # kiểm tra bit cuối (1 = lẻ)x & (x - 1) # xóa bit 1 thấp nhấtbin(13) # '0b1101'bin(13).count('1') # 3 — đếm số bit 1int('1101', 2) # 13 — đổi chuỗi nhị phân sang sốToán tử so sánh, ternary & walrus
# So sánh chuỗi (chaining) — Pythonic, dễ đọcif 0 <= i < n: # thay vì i >= 0 and i < n ...
# Biểu thức điều kiện (ternary)val = a if cond else b
# Toán tử walrus := — gán và dùng luôn trong điều kiệnwhile (line := read_next()): process(line)if (size := len(nums)) > 10: print(size)
# min/max với keylongest = max(words, key=len)closest = min(points, key=lambda p: p[0]**2 + p[1]**2)Sorting — sai một dòng là sai cả bài
Cần phân biệt sorted() (trả list mới, dùng được với mọi iterable) và .sort() (chỉ list, sửa tại chỗ, trả None). Sức mạnh thật nằm ở tham số key.
sorted(nums) # tăng dần, trả list mớisorted(nums, reverse=True) # giảm dầnnums.sort(key=lambda x: -x) # tại chỗ, theo key
# Sắp xếp theo nhiều tiêu chí bằng tuple keypeople.sort(key=lambda p: (p.age, p.name)) # tuổi tăng, rồi tênnums.sort(key=lambda x: (-freq[x], x)) # freq giảm, giá trị tăng
# Sắp theo index thay vì theo giá trịorder = sorted(range(len(nums)), key=lambda i: nums[i])
# Comparator tùy chỉnh (kiểu cũ, khi tiêu chí không gói thành key được)from functools import cmp_to_keydef cmp(a, b): return (a + b < b + a) - (a + b > b + a) # trả -1/0/1arr.sort(key=cmp_to_key(cmp))Note (Mẹo nhớ key=)
key nhận một hàm sinh ra “giá trị để so sánh”. Trả về tuple để sắp theo nhiều tầng. Thêm dấu - trước số để đảo chiều tầng đó mà không cần reverse.
heapq — chỉ có min-heap
Python chỉ có min-heap. Muốn max-heap thì đẩy giá trị âm -val, hoặc đẩy tuple (-priority, item). Heap là lựa chọn cho top-k, Dijkstra, và merge nhiều luồng đã sắp xếp.
import heapq
heap = []heapq.heappush(heap, 3)heapq.heappush(heap, 1)smallest = heap[0] # xem phần tử nhỏ nhất — O(1)val = heapq.heappop(heap) # lấy & xóa nhỏ nhất — O(log n)
heapq.heapify(nums) # biến list thành heap tại chỗ — O(n)
# Max-heap: đảo dấuheapq.heappush(heap, -x)largest = -heapq.heappop(heap)
# Heap chứa tuple — so sánh theo phần tử đầu, rồi phần tử sauheapq.heappush(heap, (dist, node))
# Top-k tiện lợiheapq.nlargest(k, nums)heapq.nsmallest(k, nums)heapq.heappushpop(heap, x) # push rồi pop trong 1 thao tác (giữ size k)bisect — binary search trên list đã sắp xếp
Khỏi tự viết binary search nếu chỉ cần tìm vị trí chèn. bisect_left trả vị trí đầu tiên có thể chèn, bisect_right trả vị trí sau mọi phần tử bằng nó.
import bisect
a = [1, 3, 3, 5, 7]bisect.bisect_left(a, 3) # 1 — vị trí số 3 đầu tiênbisect.bisect_right(a, 3) # 3 — ngay sau số 3 cuối cùngbisect.insort(a, 4) # chèn 4 vào đúng chỗ, giữ sorted
# Kiểm tra phần tử có tồn tại khôngi = bisect.bisect_left(a, x)exists = i < len(a) and a[i] == xdeque — hàng đợi hai đầu
collections.deque cho phép thêm/xóa ở cả hai đầu với O(1) — đúng thứ cần cho BFS và sliding-window. Đừng dùng list cho việc này.
from collections import deque
dq = deque([1, 2, 3])dq.append(4) # thêm cuốidq.appendleft(0) # thêm đầu — O(1) (list không làm được rẻ thế này)dq.pop() # xóa cuốidq.popleft() # xóa đầu — O(1)dq = deque(maxlen=k) # tự động bỏ phần tử cũ khi vượt size k| Thao tác | list | deque |
|---|---|---|
| Thêm/xóa cuối | O(1)* | O(1) |
| Thêm/xóa đầu | O(n) | O(1) |
| Truy cập theo index | O(1) | O(n) |
math & itertools
import mathfrom itertools import combinations, permutations, product, accumulate
math.inf, -math.inf # khởi tạo min/max trong DP, Dijkstramath.gcd(12, 8) # 4math.lcm(4, 6) # 12 (Python 3.9+)math.comb(5, 2) # 10 — C(n,k)math.isqrt(17) # 4 — căn bậc hai nguyên, không sai số float
# itertools — sinh tổ hợp cho brute force nhỏ / backtrackinglist(combinations([1,2,3], 2)) # [(1,2),(1,3),(2,3)]list(permutations([1,2,3], 2)) # các hoán vị độ dài 2list(product([0,1], repeat=3)) # mọi chuỗi nhị phân dài 3list(accumulate([1,2,3,4])) # [1,3,6,10] — prefix sum sẵnTip (Tổng kết phần Syntax)
Nếu phải nhớ đúng 5 thứ: (1) slicing [::-1], [a:b]; (2) dict.get(k, default) và Counter; (3) set cho lookup O(1); (4) sorted(key=lambda ...) với tuple key; (5) heapq chỉ là min-heap. Năm thứ này xuất hiện trong gần như mọi bài.
Phần 2 — Cấu trúc dữ liệu & template
Phần này gọn hơn: mỗi cấu trúc kèm template sẵn để copy. Trọng tâm là cách dựng nó bằng syntax Python ở Phần 1.
Stack & Queue
Stack chỉ là một list (LIFO). Queue thì dùng deque (FIFO) — không bao giờ dùng list.pop(0).
# Stack — LIFOstack = []stack.append(x)top = stack.pop()peek = stack[-1] if stack else None
# Queue — FIFOfrom collections import dequeq = deque()q.append(x) # vào cuốifront = q.popleft() # ra đầu — O(1)Linked List
LeetCode thường cho sẵn ListNode. Hai kỹ thuật phải thuộc: đảo danh sách và con trỏ nhanh/chậm (tìm giữa, phát hiện chu trình).
class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next
def reverse(head): # đảo danh sách prev = None while head: head.next, prev, head = prev, head, head.next return prev
slow = fast = head # fast/slow: tìm giữa & bắt chu trìnhwhile fast and fast.next: slow = slow.next fast = fast.next.nextTree — DFS & BFS
class TreeNode: def __init__(self, val=0, left=None, right=None): self.val, self.left, self.right = val, left, right
def dfs(node): # DFS đệ quy (đây là preorder) if not node: return # xử lý node dfs(node.left) dfs(node.right)
from collections import dequedef level_order(root): # BFS theo từng tầng if not root: return [] res, q = [], deque([root]) while q: level = [] for _ in range(len(q)): # cố định số node của tầng hiện tại node = q.popleft() level.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) res.append(level) return res| Thứ tự DFS | Trình tự thăm | Dùng khi |
|---|---|---|
| Preorder | gốc → trái → phải | copy cây, tiền tố đường đi |
| Inorder | trái → gốc → phải | BST cho ra dãy tăng dần |
| Postorder | trái → phải → gốc | xóa cây, tính từ dưới lên |
Graph — danh sách kề + duyệt
Cách biểu diễn phổ biến nhất là adjacency list bằng defaultdict(list). BFS cho đường đi ngắn nhất khi không trọng số; DFS để duyệt/đếm thành phần liên thông.
from collections import defaultdict, deque
graph = defaultdict(list)for u, v in edges: graph[u].append(v) graph[v].append(u) # bỏ dòng này nếu đồ thị có hướng
def bfs(start): # đường đi ngắn nhất (không trọng số) visited = {start} q = deque([start]) while q: u = q.popleft() for v in graph[u]: if v not in visited: visited.add(v) q.append(v)
def dfs(u, visited): # duyệt sâu visited.add(u) for v in graph[u]: if v not in visited: dfs(v, visited)Union-Find (Disjoint Set)
Template cho bài thành phần liên thông, phát hiện chu trình (vô hướng), Kruskal MST.
class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.count = n # số thành phần liên thông
def find(self, x): while self.parent[x] != x: self.parent[x] = self.parent[self.parent[x]] # nén đường (path compression) x = self.parent[x] return x
def union(self, x, y): rx, ry = self.find(x), self.find(y) if rx == ry: return False # đã cùng nhóm → có chu trình self.parent[rx] = ry self.count -= 1 return TrueTrie
Cho bài prefix search, autocomplete, word search II. Mỗi node là một dict con + cờ kết thúc từ.
class Trie: def __init__(self): self.root = {}
def insert(self, word): node = self.root for ch in word: node = node.setdefault(ch, {}) # tạo nhánh nếu chưa có node['#'] = True # đánh dấu hết từ
def search(self, word, prefix_only=False): node = self.root for ch in word: if ch not in node: return False node = node[ch] return prefix_only or '#' in nodeMonotonic Stack / Deque
Stack đơn điệu giải nhanh bài “phần tử lớn/nhỏ kế tiếp”; deque đơn điệu cho max/min của sliding window.
# Next Greater Element — stack giảm dần, lưu indexres = [-1] * len(nums)stack = []for i, x in enumerate(nums): while stack and nums[stack[-1]] < x: res[stack.pop()] = x stack.append(i)
# Sliding Window Maximum — deque chứa index, giá trị giảm dầnfrom collections import dequedef max_window(nums, k): dq, res = deque(), [] for i, x in enumerate(nums): while dq and nums[dq[-1]] <= x: dq.pop() dq.append(i) if dq[0] <= i - k: # phần tử rời khỏi cửa sổ dq.popleft() if i >= k - 1: res.append(nums[dq[0]]) return resPhần 3 — Pattern thường gặp
Nhận diện đúng pattern từ đề bài là kỹ năng quyết định. Mỗi pattern dưới đây kèm dấu hiệu nhận biết.
Two Pointers
Dấu hiệu: mảng đã sắp xếp, palindrome, tìm cặp, hoặc xóa phần tử tại chỗ.
# Hai đầu dồn vào giữa — tìm cặp có tổng = target (mảng đã sắp)lo, hi = 0, len(nums) - 1while lo < hi: s = nums[lo] + nums[hi] if s == target: return [lo, hi] elif s < target: lo += 1 else: hi -= 1
# Cùng chiều — ghi đè tại chỗ (giữ phần tử thỏa điều kiện)write = 0for read in range(len(nums)): if keep(nums[read]): nums[write] = nums[read] write += 1Sliding Window
Dấu hiệu: chuỗi/mảng con liên tiếp kèm ràng buộc (tối đa k phần tử khác nhau, tổng ≥ target, cửa sổ cố định k).
# Cửa sổ co giãn — chuỗi con dài nhất không lặp ký tựseen, left, best = {}, 0, 0for right, ch in enumerate(s): if ch in seen and seen[ch] >= left: left = seen[ch] + 1 seen[ch] = right best = max(best, right - left + 1)Prefix Sum
Dấu hiệu: truy vấn tổng đoạn nhiều lần, hoặc “đếm subarray có tổng = k” (kết hợp hashmap).
prefix = [0]for x in nums: prefix.append(prefix[-1] + x)# tổng nums[i..j] = prefix[j+1] - prefix[i]Binary Search (3 biến thể)
# 1) Tìm chính xác targetlo, hi = 0, len(nums) - 1while lo <= hi: mid = (lo + hi) // 2 if nums[mid] == target: return mid elif nums[mid] < target: lo = mid + 1 else: hi = mid - 1
# 2) Lower bound — index đầu tiên có nums[i] >= targetlo, hi = 0, len(nums)while lo < hi: mid = (lo + hi) // 2 if nums[mid] < target: lo = mid + 1 else: hi = mid
# 3) Binary search trên ĐÁP ÁN — "nhỏ nhất thỏa điều kiện"lo, hi = min_ans, max_answhile lo < hi: mid = (lo + hi) // 2 if feasible(mid): hi = mid else: lo = mid + 1return loBacktracking
def backtrack(start, path): res.append(path[:]) # LƯU Ý: copy path, đừng lưu tham chiếu for i in range(start, len(nums)): path.append(nums[i]) backtrack(i + 1, path) # i+1: tổ hợp; i: cho lặp lại path.pop() # hoàn tác (undo)Note (Checklist backtracking)
- Luôn
path[:]khi lưu kết quả (nếu không, mọi kết quả trỏ về cùng một list). - Tổ hợp dùng
startindex; hoán vị dùng tậpused. - Cắt nhánh (prune) sớm để khỏi nổ thời gian.
DP với memoization
@lru_cache là cách nhanh nhất để biến hàm đệ quy thành DP top-down — miễn là tham số hashable (int, str, tuple). State dạng list thì đổi sang tuple hoặc tự quản lý dict memo.
from functools import lru_cache
@lru_cache(maxsize=None)def dp(i, remaining): if remaining == 0: return 1 if i == n or remaining < 0: return 0 return dp(i + 1, remaining - coins[i]) + dp(i + 1, remaining)Bảng quyết định — gặp tình huống nào, dùng gì?
| Khi đề yêu cầu… | Dùng |
|---|---|
| Lookup O(1), check trùng | set, dict |
| Đếm tần suất | Counter, dict.get(k,0)+1 |
| Top-k / gộp luồng đã sắp | heapq |
| Duyệt theo tầng (FIFO) | deque (BFS) |
| Cấu trúc lồng nhau (LIFO) | list làm stack |
| Đã sắp + tìm vị trí chèn | bisect |
| Truy vấn tổng đoạn nhiều lần | prefix sum |
| Mảng con liên tiếp tối ưu | sliding window |
| Liệt kê mọi tổ hợp/hoán vị | backtracking |
| Bài toán con chồng lặp | DP + memo |
| Thành phần liên thông | Union-Find / DFS |
| Tìm theo tiền tố chuỗi | Trie |
| Phần tử lớn/nhỏ kế tiếp | monotonic stack |
| Đường ngắn nhất, không trọng số | BFS |
| Đường ngắn nhất, trọng số ≥ 0 | Dijkstra + heap |
Phần 4 — Tham khảo nhanh
Big-O của các thao tác
| Thao tác | list | dict/set | deque | heap |
|---|---|---|---|---|
| Truy cập index/key | O(1) | O(1)* | O(n) | — |
| Thêm cuối | O(1)* | O(1)* | O(1) | O(log n) |
| Xóa đầu | O(n) | — | O(1) | — |
| Tìm kiếm | O(n) | O(1)* | O(n) | O(n) |
LeetCode nên làm theo nhóm
Làm 2–3 bài đại diện mỗi nhóm là đủ để nắm pattern — không cần grind cả 500 bài.
| Nhóm | Bài tiêu biểu | Pattern |
|---|---|---|
| Two Pointers | 15, 11, 42 | hai đầu, container |
| Sliding Window | 3, 76, 239 | cửa sổ co giãn / mono deque |
| HashMap | 1, 49, 560 | complement, gom nhóm, prefix+hash |
| Stack | 20, 84, 739 | ngoặc, histogram, mono stack |
| Binary Search | 33, 153, 875 | mảng xoay, search trên đáp án |
| Linked List | 141, 19, 23 | fast/slow, đảo, merge |
| Tree | 102, 236, 124 | BFS, LCA, path sum |
| Graph | 200, 207, 127 | flood fill, topo sort, BFS |
| Heap | 215, 295, 23 | top-k, merge k list |
| Backtracking | 46, 78, 51 | hoán vị, tập con, N-Queens |
| DP | 70, 322, 1143 | 1D, coin change, LCS |
Mẹo trong buổi phỏng vấn
- Nói to suy nghĩ: diễn đạt lại đề (input/output/ràng buộc), nêu approach trước khi code, gọi tên độ phức tạp.
- Hỏi edge case ngay: mảng rỗng
[], một phần tử, giá trị trùng, số âm, đồ thị không liên thông, cây một node. - Test trước khi submit: chạy thử ví dụ trong đề bằng tay, rồi thử các edge case.
- Quản lý thời gian (~45 phút): ~5’ hiểu đề → ~10’ chốt approach → ~25’ code + test → ~5’ tối ưu / follow-up.
Solution (Khi chưa nghĩ ra lời giải tối ưu)
Cứ nói brute force trước (nếu nó đúng), nêu độ phức tạp, rồi tối ưu từng bước. Nhiều interviewer chấp nhận brute force rồi cùng bạn cải thiện — luôn tốt hơn ngồi im 30 phút.
Kết
Cheatsheet này không thay được luyện tập, nhưng nó giảm friction khi code: hết phải dừng lại nhớ cú pháp bisect, hết nhầm list với deque, và nhận ra pattern nhanh hơn từ đề bài. Trước mỗi buổi phỏng vấn, hãy lướt lại Phần 1 (Syntax) và bảng quyết định ở Phần 3 — chừng đó đủ để khởi động trong 15 phút.
Chúc bạn interview tốt.
Tham khảo thêm: