Pattern cổ điển nhất, mạnh nhất. Biến O(n) thành O(log n) — và áp dụng được không chỉ trên array sort mà còn trên answer space, một kỹ thuật then chốt cho phỏng vấn senior.
n / 2^k. Khi vùng còn 1 phần tử, thuật toán dừng → tổng O(log n).
Với n = 10⁹, O(log n) = 30 bước. Đây là khác biệt giữa "1 giây" và "10 năm". Nhiều bài có
n ≤ 10⁹ bắt buộc phải dùng Binary Search hoặc công thức toán.
[left, right)Có rất nhiều phiên bản Binary Search. Khoá học này dùng 1 template duy nhất để tránh nhầm:
def binary_search(arr, target):
left, right = 0, len(arr) # half-open: right không truy cập
while left < right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid
return -1
right = len(arr) ban đầu — không có "off-by-one" với index cuối.left < right — vùng rỗng khi left == right.left = mid + 1 và right = mid — không bao giờ kẹt vô tận.left == right = vị trí "lower bound" của target.Template này dùng được cho cả "tìm chính xác" và "tìm boundary" — chỉ thay đổi điều kiện so sánh.
Hai biến thể quan trọng của BS — phải phân biệt rõ:
arr[i] ≥ targetdef lower_bound(arr, target):
left, right = 0, len(arr)
while left < right:
mid = (left + right) // 2
if arr[mid] < target:
left = mid + 1
else:
right = mid
return left
arr[i] > targetdef upper_bound(arr, target):
left, right = 0, len(arr)
while left < right:
mid = (left + right) // 2
if arr[mid] <= target:
left = mid + 1
else:
right = mid
return left
Khác biệt: so sánh với ≤ hay <.
upper_bound − lower_bound.
Đây là cách giải First and Last Position (#34) gọn nhất.
# SAI — kẹt khi left = mid:
while left < right:
mid = (left + right) // 2
if condition: left = mid # mid không đổi!
# ĐÚNG: left = mid + 1(left + right) / 2 tràn nếu cả hai gần INT_MAX. Dùng
left + (right − left) // 2. Python không có vấn đề này nhưng nên viết theo thói quen.left ≤ right với left < right.
Hai loại template tồn tại; trộn chúng dẫn đến bug. Khuyên dùng nhất quán half-open.Đề (LeetCode #704): Mảng nums sort tăng. Tìm index của target, -1 nếu không có.
def search(nums, target):
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid
return -1
nums = [-1, 0, 3, 5, 9, 12], target = 9| Bước | left | right | mid | nums[mid] | Hành động |
|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 5 | 5 < 9 → left=4 |
| 2 | 4 | 6 | 5 | 12 | 12 > 9 → right=5 |
| 3 | 4 | 5 | 4 | 9 | match → return 4 |
Time O(log n), Space O(1).
Đề (LeetCode #34): Mảng sort. Tìm index đầu và cuối của target. Trả [-1, -1] nếu không có. Yêu cầu O(log n).
def searchRange(nums, target):
def lower(t):
l, r = 0, len(nums)
while l < r:
m = (l + r) // 2
if nums[m] < t: l = m + 1
else: r = m
return l
first = lower(target)
if first == len(nums) or nums[first] != target:
return [-1, -1]
last = lower(target + 1) - 1 # mẹo: upper bound = lower(target+1)
return [first, last]
Time O(log n), Space O(1).
nums = [5,7,7,8,8,10], target = 8. Kỳ vọng: [3, 4].
lower(8)| Bước | l | r | m | nums[m] | Hành động |
|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 8 | không < 8 → r=3 |
| 2 | 0 | 3 | 1 | 7 | < 8 → l=2 |
| 3 | 2 | 3 | 2 | 7 | < 8 → l=3 |
| — | 3 | 3 | — | — | thoát, trả 3 |
lower(9) − 1 = upper bound của 8 trừ 1Tương tự, lower(9) = 5 → last = 4.
Kết quả: [3, 4]. Time O(log n).
Đề (LeetCode #35): Mảng sort, target. Trả về index nơi target nên được chèn vào.
Đây chính là lower_bound — vị trí đầu tiên ≥ target.
def searchInsert(nums, target):
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left
Time O(log n), Space O(1).
left, không phải right?left == right — trả cái nào cũng được. Quy ước trả left để code thống nhất.
Khi target lớn hơn mọi phần tử, left = len(nums) — đây là vị trí chèn cuối, đúng với yêu cầu.
Đề (LeetCode #69): Cho số nguyên không âm x. Trả floor(sqrt(x)). Không dùng built-in sqrt.
Đây là bài đầu tiên minh hoạ "BS không nhất thiết trên array". Tìm số r lớn nhất sao cho r² ≤ x.
def mySqrt(x):
if x < 2: return x
left, right = 1, x
while left < right:
mid = (left + right + 1) // 2 # ceil — tìm cận trên
if mid * mid <= x:
left = mid
else:
right = mid - 1
return left
Time O(log x), Space O(1).
left = mid (giữ mid là ứng viên), phải dùng (left + right + 1) // 2 (ceil) để tránh
vòng lặp vô tận khi left = right − 1. Đây là một trong các template "BS tìm cận trên".
right = mid chứ không phải right = mid − 1?mid đã không phải đáp án (hoặc cần loại), gán right = mid là loại đúng mid mà không loại nhầm các vị trí trước.left = mid, vì sao phải dùng mid = (l + r + 1) // 2?Rotated = mảng sort nhưng bị "cắt và ghép" tại điểm pivot. Ví dụ: [4,5,6,7,0,1,2] là [0,1,2,4,5,6,7] rotated tại pivot 4.
Với mid bất kỳ, một trong hai nửa luôn còn sort: nửa trái [l..mid] hoặc nửa phải [mid..r].
Quy trình: kiểm tra nửa nào sort → nếu target nằm trong khoảng sort đó, BS bên đó; ngược lại bên kia.
Đề (LeetCode #33): Mảng đã rotate. Tìm target. O(log n).
def search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# Xác định nửa nào sort
if nums[left] <= nums[mid]: # nửa trái sort
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else: # nửa phải sort
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
Time O(log n), Space O(1).
≤:nums[left] ≤ nums[mid] với "≤" để xử lý case mid = left (mảng size 2). Quên dấu = sẽ sai trên test nhỏ.
nums = [4,5,6,7,0,1,2], target = 0. Kỳ vọng: 4.
| Bước | l | r | mid | nums[mid] | Phân tích | Hành động |
|---|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 7 | nums[0]=4 ≤ 7 → trái sort. 0 không trong [4,7) → bên phải | l=4 |
| 2 | 4 | 6 | 5 | 1 | nums[4]=0 ≤ 1 → trái sort. 0 trong [0,1) → bên trái | r=4 |
| 3 | 4 | 4 | 4 | 0 | match | return 4 |
3 bước log₂(7) ≈ 3 — đúng O(log n).
Đề (LeetCode #153): Mảng đã rotate, không trùng. Tìm phần tử nhỏ nhất.
Min nằm ở "vết cắt" — vị trí mà nums[i] < nums[i-1]. So sánh nums[mid] với nums[right]:
nums[mid] > nums[right]: vết cắt ở bên phải mid → left = mid + 1.right = mid.def findMin(nums):
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
return nums[left]
Time O(log n).
Đề (LeetCode #162): Mảng nums. Một "peak" là nums[i] > nums[i-1] và nums[i] > nums[i+1].
Trả về index của một peak bất kỳ. Giả sử nums[-1] = nums[n] = -∞. Yêu cầu O(log n).
Mảng không cần sort. Nhưng vẫn áp dụng BS được vì có "monotonicity" cục bộ: nếu nums[mid] < nums[mid+1],
bên phải chắc chắn có peak (vì hàm đang đi lên và phải xuống lại sau hữu hạn bước).
def findPeakElement(nums):
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] < nums[mid + 1]:
left = mid + 1 # peak bên phải
else:
right = mid # peak bên trái hoặc tại mid
return left
Time O(log n).
canDo(X) phải monoton —
nếu canDo(X) = True thì canDo(X+1) cũng True (hoặc ngược lại).
canDo(X) dễ viết và chạy nhanh (thường O(n) hoặc O(n log n)).Khi cả ba có → BS trên answer space.
def parametric_bs(low, high, can_do):
"""Tìm X nhỏ nhất sao cho can_do(X) == True."""
while low < high:
mid = (low + high) // 2
if can_do(mid):
high = mid # mid khả thi → cố nhỏ hơn
else:
low = mid + 1 # mid không khả thi → phải lớn hơn
return low
low = giá trị nhỏ nhất khả dĩ; high = giá trị lớn nhất.can_do(X): cho X này, có làm được không? Kiểm tra monoton (vẽ trên giấy: với X tăng thì can_do từ False/True đến True/True).Đề (LeetCode #875): Koko có piles[i] chuối ở đống thứ i. Mỗi giờ ăn k quả từ một đống (nếu đống < k, chỉ ăn hết đống). Bảo vệ về sau h giờ. Tìm k nhỏ nhất để ăn hết trước khi bảo vệ về.
k ∈ [1, max(piles)] — không cần lớn hơn max.sum(ceil(p / k) for p in piles); can_do = (total ≤ h).import math
def minEatingSpeed(piles, h):
def can_do(k):
return sum(math.ceil(p / k) for p in piles) <= h
low, high = 1, max(piles)
while low < high:
mid = (low + high) // 2
if can_do(mid):
high = mid
else:
low = mid + 1
return low
Time O(n log(max(piles))).
piles = [3, 6, 7, 11], h = 8. Kỳ vọng: 4.
Answer space: [1, 11].
| Bước | low | high | mid | Tổng giờ với k=mid | can_do? | Hành động |
|---|---|---|---|---|---|---|
| 1 | 1 | 11 | 6 | 1+1+2+2 = 6 | 6 ≤ 8 ✓ | high=6 |
| 2 | 1 | 6 | 3 | 1+2+3+4 = 10 | 10 ≤ 8 ✗ | low=4 |
| 3 | 4 | 6 | 5 | 1+2+2+3 = 8 | 8 ≤ 8 ✓ | high=5 |
| 4 | 4 | 5 | 4 | 1+2+2+3 = 8 | 8 ≤ 8 ✓ | high=4 |
| — | 4 | 4 | — | — | — | thoát, trả 4 |
Kết quả: k = 4. Chỉ tốn ~log₂(11) ≈ 4 lần can_do call.
Đề (LeetCode #1011): Mảng weights, số ngày days. Tàu phải chở hàng theo thứ tự, mỗi ngày chở liên tiếp. Tìm capacity nhỏ nhất.
cap ∈ [max(weights), sum(weights)]. Cận dưới phải ≥ max(weights) (kẻo không chở được 1 món); cận trên = sum (chở hết trong 1 ngày).def shipWithinDays(weights, days):
def can_do(cap):
d = 1
cur = 0
for w in weights:
if cur + w > cap:
d += 1
cur = 0
cur += w
return d <= days
low, high = max(weights), sum(weights)
while low < high:
mid = (low + high) // 2
if can_do(mid):
high = mid
else:
low = mid + 1
return low
Time O(n log(sum)).
Đề (LeetCode #4, Hard): Hai mảng sort nums1, nums2 tổng size n+m. Tìm median.
Yêu cầu O(log(n+m)).
Đặt i là số phần tử lấy từ nums1 cho nửa trái. Khi đó j = (n+m+1)/2 − i phần tử từ nums2.
Cần tìm i sao cho nums1[i-1] ≤ nums2[j] và nums2[j-1] ≤ nums1[i] — đảm bảo nửa trái ≤ nửa phải.
def findMedianSortedArrays(A, B):
if len(A) > len(B): A, B = B, A # đảm bảo BS trên array nhỏ
n, m = len(A), len(B)
half = (n + m + 1) // 2
low, high = 0, n
while low <= high:
i = (low + high) // 2
j = half - i
Aleft = A[i-1] if i > 0 else float('-inf')
Aright = A[i] if i < n else float('inf')
Bleft = B[j-1] if j > 0 else float('-inf')
Bright = B[j] if j < m else float('inf')
if Aleft <= Bright and Bleft <= Aright:
if (n + m) % 2:
return max(Aleft, Bleft)
return (max(Aleft, Bleft) + min(Aright, Bright)) / 2
elif Aleft > Bright:
high = i - 1
else:
low = i + 1
Để đảm bảo j = half − i luôn hợp lệ (≥ 0). Nếu n < m, biên của i ∈ [0, n] hẹp hơn ⟹ j ≥ 0 luôn được đảm bảo.
Hình dung "đường thẳng" chia mảng tổng thành 2 nửa bằng nhau. Đường này đi qua nums1 ở vị trí i và nums2 ở vị trí j.
Median sẽ ở chính giữa khi:
(n + m + 1) // 2 đảm bảo nửa trái có ≥ nửa phải 0 hoặc 1 phần tử. Khi tổng lẻ, median = max nửa trái.
Khi tổng chẵn, median = average của max nửa trái và min nửa phải.
Time O(log min(n, m)). Đây là một trong những bài Hard kinh điển nhất — sinh viên hiểu được = đã làm chủ BS.
low = max(weights) chứ không phải 1?| Tín hiệu trong đề | Biến thể BS |
|---|---|
| "Mảng sort, tìm phần tử" | Classic BS |
| "Vị trí đầu/cuối", "đếm số lần" | Lower/Upper bound |
| "Vị trí chèn" | Lower bound |
| "Mảng rotated", "tìm peak" | BS trên monotonicity cục bộ |
| "Sqrt(x)", "căn bậc k" | BS trên answer space [0, x] |
| "Min X sao cho có thể Y" | Parametric BS |
| "Constraint n ≤ 10⁹" | BS trên answer space (gần như chắc chắn) |
| "Median of two sorted arrays" | BS trên đường chia |
n ≤ 10⁹ hoặc cụm "min X sao cho có thể Y" → BS.Recursion & Tree — DFS, Tree Traversal, Divide & Conquer
Cấu trúc cây và đệ quy đi đôi với nhau. Học ba phép duyệt (preorder, inorder, postorder), pattern "đệ quy trả về thông tin tổng hợp", áp dụng vào bài như Maximum Depth, Diameter, Lowest Common Ancestor, Path Sum. Kỹ thuật Divide & Conquer (Merge Sort, Quick Sort) cũng được giới thiệu để chuẩn bị cho Tuần 9 (Heap) và Tuần 12 (DP).
Nhấn T hoặc Esc để đóng