ductai05 avatar
Overview
Python DSA Interview Cheatsheet — Syntax, Data Structures & Tricks

Python DSA Interview Cheatsheet — Syntax, Data Structures & Tricks

June 1, 2026
19 min read

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ử.

list_core.py
nums = [1, 2, 3, 4, 5]
# --- Truy cập & cắt lát (slicing) ---
nums[0] # 1 — phần tử đầu
nums[-1] # 5 — phần tử cuối, không cần len()
nums[1:4] # [2,3,4] — từ index 1 đến trước 4
nums[:3] # [1,2,3] — 3 phần tử đầu
nums[2:] # [3,4,5] — từ index 2 đến hết
nums[::-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ánh
nums.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 extend
del nums[1] # xóa theo index
nums.remove(3) # xóa theo GIÁ TRỊ (lần đầu gặp)
# --- Tra cứu ---
3 in nums # True/False — O(n) với list
nums.index(3) # vị trí đầu tiên của 3
nums.count(3) # số lần xuất hiện

Khi 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:

list_inplace.py
nums.sort() # sắp xếp tại chỗ, O(n log n)
nums.sort(reverse=True) # giảm dần
nums.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 = a không copy, hai biến trỏ cùng list. Muốn copy thật: b = a[:] hoặc b = list(a).
  • Pop đầu liên tục: list.pop(0) là O(n). Nếu cần FIFO, đổi sang collections.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.

iterate.py
# 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 song
for 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)]
# Unpacking
first, *middle, last = [1, 2, 3, 4, 5] # first=1, middle=[2,3,4], last=5
a, b = b, a # hoán đổi không cần biến tạm
return 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.

string_core.py
s = "Hello World"
# Slicing & truy cập — giống hệt list
s[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ếm
s.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 đầu
s.replace("o", "0")
# Ký tự ↔ mã số (cực hữu ích cho bài về bảng chữ cái)
ord('a') # 97
chr(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:

string_build.py
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.

dict_core.py
d = {"a": 1, "b": 2}
# Truy cập AN TOÀN — không gây lỗi nếu thiếu key
d.get("c") # None — không lỗi
d.get("c", 0) # 0 — trả giá trị mặc định
d.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ệt
for 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ác
d.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.

collections_dict.py
from collections import defaultdict, Counter
# defaultdict — bỏ qua bước kiểm tra key
graph = 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 0
seen[x] += 1
# Counter — đếm trong 1 dòng
cnt = Counter("aabbbc") # Counter({'b':3, 'a':2, 'c':1})
cnt = Counter(nums)
cnt.most_common(2) # [('b', 3), ('a', 2)] — top 2
cnt['a'] # 2, trả 0 nếu thiếu (không lỗi)
cnt['z'] += 1 # tự khởi tạo
list(cnt.elements()) # bung ngược lại thành các phần tử
# Counter còn cộng/trừ được như multiset
Counter("aab") - Counter("a") # Counter({'a':1, 'b':1})

Set — kiểm tra tồn tại & phép toán tập hợp

set_core.py
seen = set()
seen.add(x)
seen.discard(x) # xóa, không lỗi nếu thiếu
x in seen # O(1) trung bình — lý do chính để dùng set
# Phép toán tập hợp
a | 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 được

Tuple & “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).

tuple_core.py
point = (x, y) # cặp tọa độ
visited.add((r, c)) # tuple làm phần tử set
dp[(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ái
pairs.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.

comprehension.py
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 comprehension
idx = {v: i for i, v in enumerate(nums)} # value -> index
uniq = {x % k for x in nums}
# Generator — không tạo list trung gian
total = sum(x * x for x in nums)
ok = any(x < 0 for x in nums) # dừng ngay khi gặp True
allp = all(x > 0 for x in nums)
Danger (Lỗi khởi tạo ma trận 2D — RẤT hay gặp)
matrix_init.py
# 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ập
grid = [[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.

numbers_bits.py
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úc
2 ** 10 # 1024 — lũy thừa
abs(-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 # AND
a | b # OR
a ^ b # XOR — dùng tìm số xuất hiện lẻ lần
~a # NOT
a << 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ất
bin(13) # '0b1101'
bin(13).count('1') # 3 — đếm số bit 1
int('1101', 2) # 13 — đổi chuỗi nhị phân sang số

Toán tử so sánh, ternary & walrus

operators.py
# So sánh chuỗi (chaining) — Pythonic, dễ đọc
if 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ện
while (line := read_next()):
process(line)
if (size := len(nums)) > 10:
print(size)
# min/max với key
longest = 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.

sorting.py
sorted(nums) # tăng dần, trả list mới
sorted(nums, reverse=True) # giảm dần
nums.sort(key=lambda x: -x) # tại chỗ, theo key
# Sắp xếp theo nhiều tiêu chí bằng tuple key
people.sort(key=lambda p: (p.age, p.name)) # tuổi tăng, rồi tên
nums.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_key
def cmp(a, b):
return (a + b < b + a) - (a + b > b + a) # trả -1/0/1
arr.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.

heapq_core.py
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ấu
heapq.heappush(heap, -x)
largest = -heapq.heappop(heap)
# Heap chứa tuple — so sánh theo phần tử đầu, rồi phần tử sau
heapq.heappush(heap, (dist, node))
# Top-k tiện lợi
heapq.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ó.

bisect_core.py
import bisect
a = [1, 3, 3, 5, 7]
bisect.bisect_left(a, 3) # 1 — vị trí số 3 đầu tiên
bisect.bisect_right(a, 3) # 3 — ngay sau số 3 cuối cùng
bisect.insort(a, 4) # chèn 4 vào đúng chỗ, giữ sorted
# Kiểm tra phần tử có tồn tại không
i = bisect.bisect_left(a, x)
exists = i < len(a) and a[i] == x

deque — 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.

deque_core.py
from collections import deque
dq = deque([1, 2, 3])
dq.append(4) # thêm cuối
dq.appendleft(0) # thêm đầu — O(1) (list không làm được rẻ thế này)
dq.pop() # xóa cuối
dq.popleft() # xóa đầu — O(1)
dq = deque(maxlen=k) # tự động bỏ phần tử cũ khi vượt size k
Thao táclistdeque
Thêm/xóa cuốiO(1)*O(1)
Thêm/xóa đầuO(n)O(1)
Truy cập theo indexO(1)O(n)
* amortized

math & itertools

math_itertools.py
import math
from itertools import combinations, permutations, product, accumulate
math.inf, -math.inf # khởi tạo min/max trong DP, Dijkstra
math.gcd(12, 8) # 4
math.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ỏ / backtracking
list(combinations([1,2,3], 2)) # [(1,2),(1,3),(2,3)]
list(permutations([1,2,3], 2)) # các hoán vị độ dài 2
list(product([0,1], repeat=3)) # mọi chuỗi nhị phân dài 3
list(accumulate([1,2,3,4])) # [1,3,6,10] — prefix sum sẵn
Tip (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)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_queue.py
# Stack — LIFO
stack = []
stack.append(x)
top = stack.pop()
peek = stack[-1] if stack else None
# Queue — FIFO
from collections import deque
q = deque()
q.append(x) # vào cuối
front = 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áchcon trỏ nhanh/chậm (tìm giữa, phát hiện chu trình).

linked_list.py
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ình
while fast and fast.next:
slow = slow.next
fast = fast.next.next

Tree — DFS & BFS

tree.py
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 deque
def 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ự DFSTrình tự thămDùng khi
Preordergốc → trái → phảicopy cây, tiền tố đường đi
Inordertrái → gốc → phảiBST cho ra dãy tăng dần
Postordertrái → phải → gốcxó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.

graph.py
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.

union_find.py
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 True

Trie

Cho bài prefix search, autocomplete, word search II. Mỗi node là một dict con + cờ kết thúc từ.

trie.py
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 node

Monotonic 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.

monotonic.py
# Next Greater Element — stack giảm dần, lưu index
res = [-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ần
from collections import deque
def 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 res

Phầ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ỗ.

two_pointers.py
# 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) - 1
while 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 = 0
for read in range(len(nums)):
if keep(nums[read]):
nums[write] = nums[read]
write += 1

Sliding 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).

sliding_window.py
# Cửa sổ co giãn — chuỗi con dài nhất không lặp ký tự
seen, left, best = {}, 0, 0
for 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_sum.py
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ể)

binary_search.py
# 1) Tìm chính xác target
lo, hi = 0, len(nums) - 1
while 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] >= target
lo, 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_ans
while lo < hi:
mid = (lo + hi) // 2
if feasible(mid): hi = mid
else: lo = mid + 1
return lo

Backtracking

backtracking.py
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 start index; hoán vị dùng tập used.
  • 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.

dp_memo.py
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ùngset, dict
Đếm tần suấtCounter, dict.get(k,0)+1
Top-k / gộp luồng đã sắpheapq
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ènbisect
Truy vấn tổng đoạn nhiều lầnprefix sum
Mảng con liên tiếp tối ưusliding window
Liệt kê mọi tổ hợp/hoán vịbacktracking
Bài toán con chồng lặpDP + memo
Thành phần liên thôngUnion-Find / DFS
Tìm theo tiền tố chuỗiTrie
Phần tử lớn/nhỏ kế tiếpmonotonic stack
Đường ngắn nhất, không trọng sốBFS
Đường ngắn nhất, trọng số ≥ 0Dijkstra + heap

Phần 4 — Tham khảo nhanh

Big-O của các thao tác

Thao táclistdict/setdequeheap
Truy cập index/keyO(1)O(1)*O(n)
Thêm cuốiO(1)*O(1)*O(1)O(log n)
Xóa đầuO(n)O(1)
Tìm kiếmO(n)O(1)*O(n)O(n)
* trung bình (amortized / hash)

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ómBài tiêu biểuPattern
Two Pointers15, 11, 42hai đầu, container
Sliding Window3, 76, 239cửa sổ co giãn / mono deque
HashMap1, 49, 560complement, gom nhóm, prefix+hash
Stack20, 84, 739ngoặc, histogram, mono stack
Binary Search33, 153, 875mảng xoay, search trên đáp án
Linked List141, 19, 23fast/slow, đảo, merge
Tree102, 236, 124BFS, LCA, path sum
Graph200, 207, 127flood fill, topo sort, BFS
Heap215, 295, 23top-k, merge k list
Backtracking46, 78, 51hoán vị, tập con, N-Queens
DP70, 322, 11431D, 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)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: