Nền tảng ngôn ngữ và tư duy phân tích độ phức tạp — hai công cụ tiên quyết cho mọi bài toán LeetCode & Codility.
Sau 14 tuần, sinh viên sẽ có khả năng:
Đa số người mới mắc lỗi là cày bài lẻ tẻ — giải xong bài này, gặp bài tương tự vẫn không nhận ra. Khoá học áp dụng ba nguyên tắc khoa học học tập:
week01/two-sum.py để theo dõi tiến độ.solution.py, tests.py (3 test tự nghĩ),
và analysis.md (1 đoạn ngắn về complexity và lý do chọn approach).
Khoá học mặc định dùng Python. Bốn lý do:
dict, set, list, tuple đều hiệu năng tốt và cú pháp gọn.| Cấu trúc | Mô tả | Truy cập | Thay đổi được? |
|---|---|---|---|
list |
Mảng động, có thứ tự | O(1) theo index | Có |
tuple |
Mảng cố định, có thứ tự | O(1) theo index | Không |
dict |
Bảng băm key → value | O(1) trung bình theo key | Có |
set |
Tập hợp không trùng | O(1) trung bình kiểm tra thuộc | Có |
dict và set đều cài đặt bằng hash table.
Trong worst case (hash collision dày đặc), thao tác có thể là O(n) — nhưng thực tế cực hiếm xảy ra ngoài bài bịa nhằm phá hash.
| Thao tác | Cú pháp | Complexity |
|---|---|---|
| Truy cập index | a[i] | O(1) |
| Thêm vào cuối | a.append(x) | O(1) amortised |
| Thêm vào đầu | a.insert(0, x) | O(n) |
| Xoá ở cuối | a.pop() | O(1) |
| Xoá ở đầu | a.pop(0) | O(n) |
| Tìm phần tử | x in a | O(n) |
| Sort | a.sort() | O(n log n) |
| Slicing | a[i:j] | O(j − i) |
list.insert(0, x) hoặc list.pop(0) trong vòng lặp,
biến O(n) thành O(n²). Khi cần thêm/xoá ở đầu, dùng collections.deque (O(1) cả hai đầu).
# Đếm tần suất ký tự — O(n)
freq = {}
for ch in s:
freq[ch] = freq.get(ch, 0) + 1
# Cách viết Pythonic hơn
from collections import Counter
freq = Counter(s)
# Set: kiểm tra trùng — O(1)
seen = set()
for x in nums:
if x in seen: # O(1) trung bình
return True
seen.add(x)
return False
seen trên là pattern "hash để khử O(n²)". Đây là pattern xuất hiện
ở khoảng 25% số bài Easy/Medium đầu tiên trên LeetCode. Nhận diện được pattern này là thắng nửa trận.
String trong Python là immutable. Mỗi thao tác ghép tạo ra string mới:
# BẪY: O(n²) vì mỗi += copy toàn bộ string cũ
result = ""
for word in words:
result += word
# ĐÚNG: O(n) tổng
result = "".join(words)
s = "leetcode"
s[0:4] # "leet"
s[-3:] # "ode" (3 ký tự cuối)
s[::-1] # "edocteel" (đảo ngược)
s[::2] # "lecd" (mỗi 2 ký tự)
join hoặc list tạm rồi
"".join(list). Đừng += trong vòng lặp.
squares = [x*x for x in range(10)]
evens = [x for x in nums if x % 2 == 0]
matrix = [[0]*n for _ in range(m)] # ma trận m×n
# BẪY: KHÔNG dùng [[0]*n] * m — tất cả hàng là cùng 1 reference
a, b = b, a # swap không cần biến tạm
for i, x in enumerate(nums): # truy cập index + giá trị
...
for k, v in d.items(): # duyệt dict
...
for x, y in zip(arr1, arr2): # ghép cặp 2 mảng
...
prev, curr = curr, prev + curr # Fibonacci 1 dòng
| Hàm | Tác dụng | Ví dụ |
|---|---|---|
sorted | Sort không inplace, trả về list mới | sorted(nums, reverse=True) |
min/max | Min/max của iterable | max(nums, key=abs) |
sum | Tổng iterable | sum(nums) |
any/all | Có ít nhất 1 / tất cả True | any(x < 0 for x in nums) |
enumerate | Cặp (index, value) | for i,x in enumerate(a) |
zip | Ghép cặp nhiều iterable | zip(a, b) |
map/filter | Áp dụng / lọc | list(map(int, line.split())) |
reversed | Đảo iterable | for x in reversed(a) |
key= trong sorted/min/max cực mạnh.
sorted(words, key=len) sort theo độ dài; sorted(points, key=lambda p: p[0]**2 + p[1]**2) sort theo khoảng cách từ gốc toạ độ.
collections — ba bảo bốifrom collections import defaultdict
graph = defaultdict(list) # giá trị mặc định: list rỗng
graph[u].append(v) # không cần check key tồn tại
from collections import Counter
c = Counter("leetcode") # {'e':3, 'l':1, 't':1, 'c':1, 'o':1, 'd':1}
c.most_common(2) # [('e', 3), ('l', 1)]
from collections import deque
q = deque([1, 2, 3])
q.appendleft(0) # O(1)
q.popleft() # O(1) — ngược với list.pop(0) là O(n)
nums = [1,2,3]; nums.insert(0, 0) có complexity là gì?deque.appendleft.words thành một string với complexity tốt nhất?+= trong vòng lặp tạo ra string mới mỗi lần, dẫn tới O(n²). join tính tổng độ dài trước rồi cấp phát một lần, đạt O(n).Một bài LeetCode điển hình có time limit ~1 giây. Máy chạy được khoảng 10⁸ thao tác đơn giản/giây. Đối chiếu với input size:
| n tối đa | Complexity yêu cầu | Ví dụ thuật toán |
|---|---|---|
| ≤ 10 | O(n!) hoặc O(2ⁿ) | Permutation, brute backtrack |
| ≤ 20 | O(2ⁿ) | Subset enumeration |
| ≤ 500 | O(n³) | Floyd-Warshall, DP 3 chiều nhẹ |
| ≤ 5,000 | O(n²) | DP 2 chiều |
| ≤ 10⁶ | O(n log n) | Sort, segment tree |
| ≤ 10⁸ | O(n) hoặc O(n log log n) | Two pointers, sliding window |
Big-O đo tốc độ tăng, không đo thời gian thực. Hai thuật toán cùng O(n) có thể khác nhau 10 lần về thời gian thực — nhưng cùng tăng tuyến tính theo n.
Khoảng cách giữa O(n) và O(n²) khi n = 10⁵ là 100,000 lần. Đó là khác biệt giữa "chạy 1 giây" và "chạy 1 ngày".
Quy trình 3 bước:
# Ví dụ
def has_pair(nums, target):
for i in range(len(nums)): # vòng ngoài: n lần
for j in range(i+1, len(nums)): # vòng trong: trung bình n/2 lần
if nums[i] + nums[j] == target:
return True
return False
# Tổng: n × n/2 = n²/2 → O(n²)
x in lst
nếu lst là list thì O(n); nếu là set thì O(1). Luôn check kiểu dữ liệu.
def max_value(nums):
m = float('-inf')
for x in nums: # n lần
if x > m: # O(1)
m = x # O(1)
return m
Phân tích:
Space: chỉ dùng biến m — O(1).
for chạy qua mảng + công việc O(1) mỗi bước = O(n) thời gian, O(1) bộ nhớ.
Đây là cấu trúc ~30% bài Easy.
def count_inversions(nums):
count = 0
for i in range(len(nums)): # n lần
for j in range(i+1, len(nums)): # (n-1) + (n-2) + ... + 0
if nums[i] > nums[j]:
count += 1
return count
Phân tích:
Tổng số lần thao tác bên trong:
$$ \sum_{i=0}^{n-1} (n - 1 - i) = \frac{n(n-1)}{2} \approx \frac{n^2}{2} $$Bỏ hằng số ½ → O(n²). Space: O(1).
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
Phân tích: Mỗi lần gọi fib(n) sinh ra 2 lời gọi nhỏ hơn.
Cây gọi đệ quy có chiều cao ~n và phân nhánh 2 → tổng số nút ~\(2^n\).
Space: O(n) cho call stack tại bất kỳ thời điểm nào.
Phỏng vấn ngày càng quan tâm bộ nhớ, không chỉ thời gian. Hai loại space cần đếm:
Bộ nhớ thuật toán dùng thêm ngoài input. Đây là cái phỏng vấn thường hỏi.
Bao gồm cả input. Ít quan tâm hơn vì input là "miễn phí".
# Auxiliary O(n)
def double_array(nums):
return [x*2 for x in nums] # tạo list mới size n
# Auxiliary O(1)
def double_inplace(nums):
for i in range(len(nums)): # sửa trực tiếp
nums[i] *= 2
fib(n) đệ quy có space O(n) dù không tạo mảng nào.
Đây là kỹ năng quan trọng nhất của tuần này. Mở một bài LeetCode bất kỳ, mục "Constraints" cho biết:
Constraints:
1 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
Khi thấy n ≤ 10⁵, lập tức biết:
Khi thấy n ≤ 10⁹, biết:
fib(n) đệ quy naive (không memo) có space complexity bao nhiêu?fib(n) → fib(n-1) → ... → fib(0)). Time là O(2ⁿ) nhưng space chỉ O(n).n ≤ 20 gợi ý thuật toán bậc nào?Đề: Cho mảng nums và số target. Tìm hai chỉ số i, j sao cho
nums[i] + nums[j] == target. Giả sử có đúng một đáp án.
Constraints: 2 ≤ n ≤ 10⁴.
def twoSum(nums, target):
for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
Time O(n²), space O(1). Với n = 10⁴ thì ~5×10⁷ thao tác — chạy được nhưng rất sát giới hạn.
Quan sát: với mỗi nums[i], ta cần tìm complement = target - nums[i]
trong phần còn lại. Tìm trong list là O(n), nhưng tìm trong set/dict là O(1).
def twoSum(nums, target):
seen = {} # value → index
for i, x in enumerate(nums):
complement = target - x
if complement in seen: # O(1)
return [seen[complement], i]
seen[x] = i
return []
Time O(n), space O(n).
Đề: Cho hai string s và t. Trả về True nếu
t là một anagram (đảo chữ) của s.
def isAnagram(s, t):
return sorted(s) == sorted(t)
# Time: O(n log n), Space: O(n)
from collections import Counter
def isAnagram(s, t):
return Counter(s) == Counter(t)
# Time: O(n), Space: O(k) với k = số ký tự khác nhau
def isAnagram(s, t):
if len(s) != len(t): return False
cnt = [0] * 26
for ch in s: cnt[ord(ch) - ord('a')] += 1
for ch in t: cnt[ord(ch) - ord('a')] -= 1
return all(c == 0 for c in cnt)
# Time: O(n), Space: O(1) vì 26 là hằng số
Đề: Mảng prices[i] = giá cổ phiếu ngày i. Mua 1 ngày, bán 1 ngày sau đó.
Tính lợi nhuận tối đa (≥ 0).
Constraints: 1 ≤ n ≤ 10⁵ → cần O(n).
# O(n²) — sẽ vượt time limit
for i in range(n):
for j in range(i+1, n):
profit = max(profit, prices[j] - prices[i])
def maxProfit(prices):
min_so_far = float('inf')
best = 0
for p in prices:
min_so_far = min(min_so_far, p)
best = max(best, p - min_so_far)
return best
collections: defaultdict, Counter, deque.
Mỗi bài: nộp solution.py + tests.py (3 test tự nghĩ) +
analysis.md (complexity và lý do chọn approach).
Array & String — Two Pointers và Prefix Sum
Hai pattern cốt lõi giải quyết ~30% bài Easy/Medium đầu tiên trên LeetCode. Sẽ áp dụng trực tiếp Big-O đã học để chứng minh tại sao two pointers giảm O(n²) xuống O(n).
Nhấn T hoặc Esc để đóng