Sorting & Binary Search: Tìm Hiểu Chi Tiết Các Thuật Toán Thiết Yếu

Introduction
Sorting và searching là hai phép toán cơ bản nhất trong computer science. Mọi database query, mọi kết quả search engine, mọi autocomplete suggestion đều dựa vào các thuật toán sorting và searching hiệu quả phía dưới.
Trong coding interview, bài toán sorting kiểm tra khả năng hiểu divide and conquer, comparison-based logic, và stability của bạn. Bài toán binary search kiểm tra khả năng loại bỏ một nửa search space một cách có hệ thống — một trong những kỹ thuật thuật toán mạnh nhất mà bạn sẽ từng học.
Bài viết này bao gồm Phần 1: Sorting Algorithms và Phần 2: Binary Search từ series DSA của chúng ta. Kết thúc bài, bạn sẽ hiểu khi nào dùng mỗi thuật toán, tại sao chúng hoạt động, và cách implement chúng từ đầu.
What You'll Learn
✅ Hiểu comparison-based vs non-comparison sorting
✅ Implement Bubble Sort, Selection Sort, Insertion Sort, Merge Sort, Quick Sort
✅ Phân tích time và space complexity cho từng thuật toán sorting
✅ Nắm vững standard binary search và các biến thể
✅ Áp dụng binary search cho rotated array, search space, và answer validation
✅ Giải 8 bài interview kinh điển step-by-step
Prerequisites
- Hiểu Big O Notation & Complexity Analysis
- Quen thuộc với Problem-Solving Patterns
- Đã hoàn thành Arrays & Strings
- Kiến thức lập trình cơ bản với TypeScript hoặc Python
Phần 1: Sorting Algorithms
1. Tại Sao Sorting Quan Trọng
Sorting không chỉ đơn giản là sắp xếp các phần tử theo thứ tự. Nhiều thuật toán yêu cầu input đã sorted làm điều kiện tiên quyết:
| Phụ thuộc vào Sorting | Thuật toán |
|---|---|
| Binary search | Yêu cầu sorted array |
| Two pointers | Thường cần sorted input |
| Merge intervals | Sort theo start time trước |
| Closest pair | Sort rồi scan neighbors |
| Kruskal's MST | Sort edges theo weight |
| Median finding | Sorted order cho O(1) median |
"Nếu brute force của bạn là O(n²) và input chưa sorted, bản năng đầu tiên nên là: mình có thể sort nó trước không?"
Sort một array tốn O(n log n), gần như "miễn phí" so với O(n²) brute force. Nhiều bài O(n²) trở thành O(n log n) chỉ bằng cách sort trước.
2. Phân Loại Sorting Algorithm
Các thuộc tính chính để so sánh:
| Thuộc tính | Định nghĩa |
|---|---|
| Stable | Các phần tử bằng nhau giữ nguyên thứ tự tương đối ban đầu |
| In-place | Dùng O(1) bộ nhớ phụ (sort ngay trong array) |
| Adaptive | Chạy nhanh hơn trên input gần như đã sorted |
| Online | Có thể sort phần tử khi chúng đến (không cần tất cả data trước) |
3. Thuật Toán Sorting Cơ Bản — O(n²)
Đây là các thuật toán đơn giản nhưng chậm. Bạn nên biết chúng cho interview, nhưng hiếm khi dùng trong production.
3.1 Bubble Sort
Ý tưởng: Lặp đi lặp lại qua array, swap các phần tử kề nhau nếu chúng sai thứ tự. Phần tử lớn nhất chưa sorted sẽ "nổi lên" đúng vị trí sau mỗi pass.
// TypeScript - Bubble Sort
function bubbleSort(arr: number[]): number[] {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
let swapped = false;
for (let j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
swapped = true;
}
}
// Tối ưu: nếu không swap, array đã sorted
if (!swapped) break;
}
return arr;
}
console.log(bubbleSort([5, 3, 8, 1, 2])); // [1, 2, 3, 5, 8]# Python - Bubble Sort
def bubble_sort(arr: list[int]) -> list[int]:
n = len(arr)
for i in range(n - 1):
swapped = False
for j in range(n - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
# Tối ưu: nếu không swap, array đã sorted
if not swapped:
break
return arr
print(bubble_sort([5, 3, 8, 1, 2])) # [1, 2, 3, 5, 8]| Thuộc tính | Giá trị |
|---|---|
| Time (Best) | O(n) — đã sorted, với optimization |
| Time (Average/Worst) | O(n²) |
| Space | O(1) in-place |
| Stable | ✅ Có |
| Adaptive | ✅ Có (với cờ swapped) |
Khi nào dùng: Gần như không bao giờ trong thực tế. Tốt cho việc dạy và hiểu sorting cơ bản.
3.2 Selection Sort
Ý tưởng: Tìm phần tử nhỏ nhất trong phần chưa sorted và swap nó lên đầu. Lặp lại cho mỗi vị trí.
// TypeScript - Selection Sort
function selectionSort(arr: number[]): number[] {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
let minIdx = i;
for (let j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
if (minIdx !== i) {
[arr[i], arr[minIdx]] = [arr[minIdx], arr[i]];
}
}
return arr;
}# Python - Selection Sort
def selection_sort(arr: list[int]) -> list[int]:
n = len(arr)
for i in range(n - 1):
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
if min_idx != i:
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr| Thuộc tính | Giá trị |
|---|---|
| Time (Mọi trường hợp) | O(n²) — luôn quét toàn bộ phần chưa sorted |
| Space | O(1) in-place |
| Stable | ❌ Không (swap có thể thay đổi thứ tự tương đối) |
| Adaptive | ❌ Không |
Khi nào dùng: Khi memory write tốn kém (giảm thiểu swap — tối đa n swap).
3.3 Insertion Sort
Ý tưởng: Xây dựng sorted array từng phần tử một. Lấy mỗi phần tử và chèn nó vào đúng vị trí trong phần đã sorted phía trước.
Hãy nghĩ như xếp bài — bạn lấy từng lá bài và trượt nó vào đúng chỗ trong tay.
// TypeScript - Insertion Sort
function insertionSort(arr: number[]): number[] {
const n = arr.length;
for (let i = 1; i < n; i++) {
const key = arr[i];
let j = i - 1;
// Dịch các phần tử lớn hơn key sang phải
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
return arr;
}# Python - Insertion Sort
def insertion_sort(arr: list[int]) -> list[int]:
n = len(arr)
for i in range(1, n):
key = arr[i]
j = i - 1
# Dịch các phần tử lớn hơn key sang phải
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr| Thuộc tính | Giá trị |
|---|---|
| Time (Best) | O(n) — đã sorted |
| Time (Average/Worst) | O(n²) |
| Space | O(1) in-place |
| Stable | ✅ Có |
| Adaptive | ✅ Có |
| Online | ✅ Có |
Khi nào dùng: Array nhỏ (< 20 phần tử), data gần sorted, hoặc làm base case cho hybrid sort (TimSort dùng insertion sort cho partition nhỏ).
4. Thuật Toán Sorting Hiệu Quả — O(n log n)
Đây là những "ngựa chiến" của sorting thực tế. Hãy hiểu sâu các thuật toán này.
4.1 Merge Sort
Ý tưởng: Chia array làm đôi, đệ quy sort mỗi nửa, rồi merge hai nửa đã sorted lại. Đây là thuật toán divide and conquer kinh điển.
// TypeScript - Merge Sort
function mergeSort(arr: number[]): number[] {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left: number[], right: number[]): number[] {
const result: number[] = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i]);
i++;
} else {
result.push(right[j]);
j++;
}
}
// Thêm các phần tử còn lại
while (i < left.length) result.push(left[i++]);
while (j < right.length) result.push(right[j++]);
return result;
}
console.log(mergeSort([38, 27, 43, 3, 9, 82, 10]));
// [3, 9, 10, 27, 38, 43, 82]# Python - Merge Sort
def merge_sort(arr: list[int]) -> list[int]:
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left: list[int], right: list[int]) -> list[int]:
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
# Thêm các phần tử còn lại
result.extend(left[i:])
result.extend(right[j:])
return result
print(merge_sort([38, 27, 43, 3, 9, 82, 10]))
# [3, 9, 10, 27, 38, 43, 82]Bước merge hoạt động thế nào:
Left: [3, 27, 38, 43] Right: [9, 10, 82]
↑ i=0 ↑ j=0
Step 1: 3 < 9 → lấy 3 Result: [3]
Step 2: 27 > 9 → lấy 9 Result: [3, 9]
Step 3: 27 > 10 → lấy 10 Result: [3, 9, 10]
Step 4: 27 < 82 → lấy 27 Result: [3, 9, 10, 27]
Step 5: 38 < 82 → lấy 38 Result: [3, 9, 10, 27, 38]
Step 6: 43 < 82 → lấy 43 Result: [3, 9, 10, 27, 38, 43]
Step 7: thêm 82 Result: [3, 9, 10, 27, 38, 43, 82]| Thuộc tính | Giá trị |
|---|---|
| Time (Mọi trường hợp) | O(n log n) — luôn chia và merge |
| Space | O(n) — cần array tạm để merge |
| Stable | ✅ Có (phần tử bằng nhau giữ thứ tự) |
| Adaptive | ❌ Không (luôn O(n log n)) |
Khi nào dùng: Khi cần stability, sorting linked list, external sorting (data quá lớn cho memory), hoặc khi cần guarantee O(n log n).
Tại sao O(n log n)?
- log n level đệ quy (chia đôi array mỗi lần)
- O(n) công việc ở mỗi level (merge tất cả phần tử)
- Tổng: O(n) × O(log n) = O(n log n)
4.2 Quick Sort
Ý tưởng: Chọn một phần tử pivot, partition array sao cho tất cả phần tử nhỏ hơn pivot sang trái và lớn hơn sang phải, rồi đệ quy sort hai phần.
// TypeScript - Quick Sort (Lomuto partition)
function quickSort(arr: number[], low: number = 0, high: number = arr.length - 1): number[] {
if (low < high) {
const pivotIdx = partition(arr, low, high);
quickSort(arr, low, pivotIdx - 1);
quickSort(arr, pivotIdx + 1, high);
}
return arr;
}
function partition(arr: number[], low: number, high: number): number {
const pivot = arr[high]; // Chọn phần tử cuối làm pivot
let i = low - 1; // Ranh giới phần tử nhỏ hơn
for (let j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
// Đặt pivot vào đúng vị trí
[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
return i + 1;
}
console.log(quickSort([8, 3, 1, 7, 0, 10, 2]));
// [0, 1, 2, 3, 7, 8, 10]# Python - Quick Sort (Lomuto partition)
def quick_sort(arr: list[int], low: int = 0, high: int = None) -> list[int]:
if high is None:
high = len(arr) - 1
if low < high:
pivot_idx = partition(arr, low, high)
quick_sort(arr, low, pivot_idx - 1)
quick_sort(arr, pivot_idx + 1, high)
return arr
def partition(arr: list[int], low: int, high: int) -> int:
pivot = arr[high] # Chọn phần tử cuối làm pivot
i = low - 1 # Ranh giới phần tử nhỏ hơn
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
# Đặt pivot vào đúng vị trí
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
print(quick_sort([8, 3, 1, 7, 0, 10, 2]))
# [0, 1, 2, 3, 7, 8, 10]Trace partition cho [8, 3, 1, 7, 0, 10, 2], pivot = 2:
Ban đầu: [8, 3, 1, 7, 0, 10, 2] pivot=2, i=-1
j=0: 8 > 2 → bỏ qua
j=1: 3 > 2 → bỏ qua
j=2: 1 ≤ 2 → i=0, swap(0,2) → [1, 3, 8, 7, 0, 10, 2]
j=3: 7 > 2 → bỏ qua
j=4: 0 ≤ 2 → i=1, swap(1,4) → [1, 0, 8, 7, 3, 10, 2]
j=5: 10 > 2 → bỏ qua
Đặt pivot: swap(i+1=2, high=6) → [1, 0, 2, 7, 3, 10, 8]
↑ pivot tại index 2| Thuộc tính | Giá trị |
|---|---|
| Time (Best/Average) | O(n log n) |
| Time (Worst) | O(n²) — đã sorted + pivot tệ |
| Space | O(log n) — recursion stack |
| Stable | ❌ Không |
| Adaptive | ❌ Không |
Chiến lược chọn pivot:
| Chiến lược | Ưu điểm | Nhược điểm |
|---|---|---|
| Phần tử cuối | Đơn giản | O(n²) trên sorted input |
| Phần tử ngẫu nhiên | Tránh worst case | Thêm random call |
| Median-of-three | Performance thực tế tốt | Phức tạp hơn chút |
Khi nào dùng: Lựa chọn mặc định cho in-memory sorting. Hầu hết standard library sort dùng Quick Sort variant (hoặc hybrid sort như IntroSort).
4.3 Merge Sort vs Quick Sort
| Khía cạnh | Merge Sort | Quick Sort |
|---|---|---|
| Worst case | O(n log n) ✅ | O(n²) ❌ |
| Average case | O(n log n) | O(n log n) |
| Space | O(n) ❌ | O(log n) ✅ |
| Stable | ✅ Có | ❌ Không |
| Cache performance | Kém (không in-place) | Xuất sắc (in-place) |
| Tốt nhất cho | Linked list, external sort | Array, in-memory sort |
| Được dùng bởi | Python (Timsort), Java (objects) | C++ (IntroSort), Java (primitives) |
Interview tip: Nếu được hỏi "thuật toán sort nào tốt nhất?", câu trả lời luôn là "tùy trường hợp." Hãy đề cập các trade-off ở trên.
5. Tổng Hợp So Sánh Sorting Algorithm
| Thuật toán | Best | Average | Worst | Space | Stable | In-Place |
|---|---|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | ✅ | ✅ |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | ❌ | ✅ |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | ✅ | ✅ |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | ✅ | ❌ |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | ❌ | ✅ |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | ❌ | ✅ |
Điểm chính cần nhớ:
- O(n log n) là lower bound lý thuyết cho comparison-based sorting
- Merge Sort = guarantee O(n log n), stable, nhưng tốn O(n) space
- Quick Sort = nhanh nhất thực tế, in-place, nhưng worst case O(n²)
- Insertion Sort = tốt nhất cho array nhỏ hoặc gần sorted
- Sort thực tế là hybrid: TimSort (Python, Java) kết hợp Merge Sort + Insertion Sort
Built-in Sort: Các Ngôn Ngữ Dùng Gì
// TypeScript/JavaScript - TimSort (stable, O(n log n))
const arr = [3, 1, 4, 1, 5, 9];
arr.sort((a, b) => a - b); // Luôn cung cấp comparator cho số!
// Không có comparator, JS sort theo string: [1, 1, 3, 4, 5, 9]# Python - TimSort (stable, O(n log n))
arr = [3, 1, 4, 1, 5, 9]
arr.sort() # Sort in-place
sorted_arr = sorted(arr) # Trả về list mới đã sorted
# Custom sort
intervals = [(2, 3), (1, 5), (1, 2)]
intervals.sort(key=lambda x: (x[0], x[1]))
# [(1, 2), (1, 5), (2, 3)]Interview tip: Trong interview thật, dùng built-in sort trừ khi được yêu cầu implement từ đầu. Biết cách chúng hoạt động, nhưng đừng phát minh lại bánh xe.
Phần 2: Binary Search
6. Binary Search Cơ Bản
Binary search là một trong những kỹ thuật mạnh nhất trong computer science. Ý tưởng cốt lõi rất đơn giản: nếu search space của bạn đã sorted (hoặc có monotonic property), bạn có thể loại bỏ một nửa với mỗi lần so sánh.
Linear Search: [1, 3, 5, 7, 9, 11, 13, 15] → Kiểm tra từng cái: O(n)
Binary Search: [1, 3, 5, 7, 9, 11, 13, 15] → Kiểm tra giữa, loại nửa: O(log n)6.1 O(log n) Nhanh Cỡ Nào?
| Kích thước Array (n) | Linear Search O(n) | Binary Search O(log n) |
|---|---|---|
| 100 | 100 bước | 7 bước |
| 10,000 | 10,000 bước | 14 bước |
| 1,000,000 | 1,000,000 bước | 20 bước |
| 1,000,000,000 | 1 tỷ bước | 30 bước |
Binary search trên 1 tỷ phần tử chỉ mất tối đa 30 bước. Đó là sức mạnh của logarithmic time.
6.2 Standard Binary Search
// TypeScript - Standard Binary Search
function binarySearch(arr: number[], target: number): number {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2); // Tránh overflow
if (arr[mid] === target) {
return mid; // Tìm thấy
} else if (arr[mid] < target) {
left = mid + 1; // Target ở nửa phải
} else {
right = mid - 1; // Target ở nửa trái
}
}
return -1; // Không tìm thấy
}
console.log(binarySearch([1, 3, 5, 7, 9, 11], 7)); // 3
console.log(binarySearch([1, 3, 5, 7, 9, 11], 6)); // -1# Python - Standard Binary Search
def binary_search(arr: list[int], target: int) -> int:
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2 # Tránh overflow
if arr[mid] == target:
return mid # Tìm thấy
elif arr[mid] < target:
left = mid + 1 # Target ở nửa phải
else:
right = mid - 1 # Target ở nửa trái
return -1 # Không tìm thấy
print(binary_search([1, 3, 5, 7, 9, 11], 7)) # 3
print(binary_search([1, 3, 5, 7, 9, 11], 6)) # -1Ví dụ trace — tìm 7 trong [1, 3, 5, 7, 9, 11]:
Step 1: left=0, right=5, mid=2 → arr[2]=5 < 7 → left=3
Step 2: left=3, right=5, mid=4 → arr[4]=9 > 7 → right=3
Step 3: left=3, right=3, mid=3 → arr[3]=7 = 7 → return 3 ✓6.3 Các Bug Binary Search Thường Gặp
Binary search nổi tiếng là dễ viết sai. Đây là những lỗi phổ biến nhất:
| Bug | Code sai | Code đúng |
|---|---|---|
| Integer overflow | mid = (left + right) / 2 | mid = left + (right - left) / 2 |
| Vòng lặp vô hạn | left = mid hoặc right = mid | left = mid + 1 hoặc right = mid - 1 |
| Lệch 1 | while (left < right) vs while (left <= right) | Tùy biến thể (xem bên dưới) |
| Sai biên | Search vượt quá array bounds | Luôn kiểm tra left <= right |
7. Các Pattern Binary Search
Standard binary search chỉ là khởi đầu. Sức mạnh thực sự của binary search đến từ các biến thể. Hãy nắm vững ba pattern cốt lõi.
7.1 Pattern 1: Tìm Giá Trị Chính Xác
Đây là standard binary search đã thấy ở trên. Dùng while (left <= right) với left = mid + 1 và right = mid - 1.
Template: Trả về index khi arr[mid] === target, hoặc -1 nếu không tìm thấy.
7.2 Pattern 2: Tìm Vị Trí Đầu/Cuối (Boundary Search)
Đây là pattern binary search quan trọng nhất cho interview. Thay vì tìm bất kỳ occurrence nào, tìm vị trí đầu tiên hoặc cuối cùng thỏa mãn điều kiện.
Tìm Vị Trí Đầu Tiên ≥ Target (Lower Bound):
// TypeScript - Tìm vị trí đầu tiên arr[mid] >= target
function lowerBound(arr: number[], target: number): number {
let left = 0;
let right = arr.length; // Lưu ý: right = length (không phải length-1)
while (left < right) { // Lưu ý: < chặt (không phải <=)
const mid = Math.floor(left + (right - left) / 2);
if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid; // Lưu ý: right = mid (không phải mid-1)
}
}
return left; // Vị trí đầu tiên arr[left] >= target
}
// Ví dụ: [1, 3, 3, 5, 7]
// lowerBound(arr, 3) → 1 (index đầu tiên có giá trị >= 3)
// lowerBound(arr, 4) → 3 (index đầu tiên có giá trị >= 4, tức là 5)
// lowerBound(arr, 8) → 5 (vượt quá cuối, target > tất cả phần tử)# Python - Tìm vị trí đầu tiên arr[mid] >= target
def lower_bound(arr: list[int], target: int) -> int:
left, right = 0, len(arr) # right = len(arr), không phải len(arr)-1
while left < right: # < chặt (không phải <=)
mid = left + (right - left) // 2
if arr[mid] < target:
left = mid + 1
else:
right = mid # right = mid (không phải mid-1)
return left # Vị trí đầu tiên arr[left] >= target
# Python cũng có built-in:
import bisect
bisect.bisect_left([1, 3, 3, 5, 7], 3) # 1
bisect.bisect_right([1, 3, 3, 5, 7], 3) # 3Tìm Vị Trí Cuối Cùng ≤ Target (Upper Bound):
// TypeScript - Tìm vị trí cuối cùng arr[mid] <= target
function upperBound(arr: number[], target: number): number {
let left = 0;
let right = arr.length;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (arr[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left - 1; // Vị trí cuối cùng arr[left-1] <= target
}
// Ví dụ: [1, 3, 3, 5, 7]
// upperBound(arr, 3) → 2 (index cuối có giá trị <= 3)
// upperBound(arr, 4) → 2 (index cuối có giá trị <= 4, tức là 3)Khi nào dùng cái nào:
| Cần | Hàm | Trả về |
|---|---|---|
| Occurrence đầu tiên của target | lowerBound(arr, target) rồi kiểm tra arr[result] === target | |
| Occurrence cuối cùng của target | upperBound(arr, target) rồi kiểm tra arr[result] === target | |
| Đếm số lần xuất hiện target | upperBound(target) - lowerBound(target) + 1 | |
| Vị trí chèn | lowerBound(arr, target) |
7.3 Pattern 3: Binary Search on Answer (Search Space Reduction)
Đây là pattern cao cấp và mạnh nhất. Thay vì search trong array, bạn binary search trên answer space.
Insight chính: Nếu bạn có thể định nghĩa hàm canAchieve(x) trả về true cho mọi giá trị ≥ ngưỡng nào đó và false bên dưới (monotonic), bạn có thể binary search tìm ngưỡng đó.
Answer space: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
canAchieve: [F, F, F, F, T, T, T, T, T, T]
↑ Tìm boundary này// TypeScript - Binary Search on Answer template
function binarySearchOnAnswer(low: number, high: number): number {
while (low < high) {
const mid = Math.floor(low + (high - low) / 2);
if (canAchieve(mid)) {
high = mid; // mid có thể là answer, search bên trái
} else {
low = mid + 1; // mid quá nhỏ, search bên phải
}
}
return low;
}# Python - Binary Search on Answer template
def binary_search_on_answer(low: int, high: int) -> int:
while low < high:
mid = low + (high - low) // 2
if can_achieve(mid):
high = mid # mid có thể là answer, search bên trái
else:
low = mid + 1 # mid quá nhỏ, search bên phải
return lowChúng ta sẽ thấy pattern này trong các bài interview bên dưới.
8. Bài Interview: Sorting
Problem 1: Sort Colors (Dutch National Flag)
LeetCode 75: Cho một array với các phần tử 0, 1, 2, sort nó in-place trong một pass.
Insight chính: Kỹ thuật ba con trỏ — duy trì ranh giới cho 0s, 1s, và 2s.
// TypeScript - Sort Colors (One-pass, O(n) time, O(1) space)
function sortColors(nums: number[]): void {
let low = 0; // Ranh giới: mọi thứ trước low là 0
let mid = 0; // Phần tử hiện tại
let high = nums.length - 1; // Ranh giới: mọi thứ sau high là 2
while (mid <= high) {
if (nums[mid] === 0) {
[nums[low], nums[mid]] = [nums[mid], nums[low]];
low++;
mid++;
} else if (nums[mid] === 1) {
mid++;
} else {
// nums[mid] === 2
[nums[mid], nums[high]] = [nums[high], nums[mid]];
high--;
// Không tăng mid — phần tử swap cần kiểm tra lại
}
}
}
const colors = [2, 0, 2, 1, 1, 0];
sortColors(colors);
console.log(colors); // [0, 0, 1, 1, 2, 2]# Python - Sort Colors (One-pass, O(n) time, O(1) space)
def sort_colors(nums: list[int]) -> None:
low, mid, high = 0, 0, len(nums) - 1
while mid <= high:
if nums[mid] == 0:
nums[low], nums[mid] = nums[mid], nums[low]
low += 1
mid += 1
elif nums[mid] == 1:
mid += 1
else:
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1
# Không tăng mid — phần tử swap cần kiểm tra lại
colors = [2, 0, 2, 1, 1, 0]
sort_colors(colors)
print(colors) # [0, 0, 1, 1, 2, 2]Trace:
[2, 0, 2, 1, 1, 0] low=0, mid=0, high=5
↑mid ↑high
nums[0]=2 → swap(0,5) → [0, 0, 2, 1, 1, 2] high=4
[0, 0, 2, 1, 1, 2] low=0, mid=0, high=4
↑mid ↑high
nums[0]=0 → swap(0,0) → [0, 0, 2, 1, 1, 2] low=1, mid=1
nums[1]=0 → swap(1,1) → [0, 0, 2, 1, 1, 2] low=2, mid=2
nums[2]=2 → swap(2,4) → [0, 0, 1, 1, 2, 2] high=3
nums[2]=1 → mid=3
nums[3]=1 → mid=4 → mid > high → XONG ✓
Kết quả: [0, 0, 1, 1, 2, 2]Problem 2: Merge Intervals
LeetCode 56: Cho một tập hợp các intervals, merge tất cả intervals chồng nhau.
Insight chính: Sort theo start time, rồi merge các interval kề nhau mà overlap.
// TypeScript - Merge Intervals
function mergeIntervals(intervals: number[][]): number[][] {
if (intervals.length <= 1) return intervals;
// Sort theo start time
intervals.sort((a, b) => a[0] - b[0]);
const merged: number[][] = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const current = intervals[i];
const last = merged[merged.length - 1];
if (current[0] <= last[1]) {
// Chồng nhau — mở rộng điểm cuối
last[1] = Math.max(last[1], current[1]);
} else {
// Không chồng — thêm interval mới
merged.push(current);
}
}
return merged;
}
console.log(mergeIntervals([[1,3],[2,6],[8,10],[15,18]]));
// [[1,6],[8,10],[15,18]]# Python - Merge Intervals
def merge_intervals(intervals: list[list[int]]) -> list[list[int]]:
if len(intervals) <= 1:
return intervals
# Sort theo start time
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for current in intervals[1:]:
last = merged[-1]
if current[0] <= last[1]:
# Chồng nhau — mở rộng điểm cuối
last[1] = max(last[1], current[1])
else:
# Không chồng — thêm interval mới
merged.append(current)
return merged
print(merge_intervals([[1,3],[2,6],[8,10],[15,18]]))
# [[1,6],[8,10],[15,18]]Time: O(n log n) cho sorting + O(n) cho merging = O(n log n)
Space: O(n) cho kết quả
Problem 3: Kth Largest Element
LeetCode 215: Tìm phần tử lớn thứ k trong array chưa sorted.
Insight chính: Dùng Quick Select — biến thể của Quick Sort chỉ đệ quy vào một partition.
// TypeScript - Kth Largest Element (Quick Select)
function findKthLargest(nums: number[], k: number): number {
// Lớn thứ k = nhỏ thứ (n - k) (0-indexed)
const targetIdx = nums.length - k;
return quickSelect(nums, 0, nums.length - 1, targetIdx);
}
function quickSelect(arr: number[], low: number, high: number, target: number): number {
if (low === high) return arr[low];
// Random pivot để tránh worst case
const randomIdx = low + Math.floor(Math.random() * (high - low + 1));
[arr[randomIdx], arr[high]] = [arr[high], arr[randomIdx]];
const pivotIdx = partition(arr, low, high);
if (pivotIdx === target) {
return arr[pivotIdx];
} else if (pivotIdx < target) {
return quickSelect(arr, pivotIdx + 1, high, target);
} else {
return quickSelect(arr, low, pivotIdx - 1, target);
}
}
// Tái sử dụng hàm partition từ Quick Sort (Mục 4.2)
console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2)); // 5
console.log(findKthLargest([3, 2, 3, 1, 2, 4, 5, 5, 6], 4)); // 4# Python - Kth Largest Element (Quick Select)
import random
def find_kth_largest(nums: list[int], k: int) -> int:
target_idx = len(nums) - k # Lớn thứ k = nhỏ thứ (n-k)
return quick_select(nums, 0, len(nums) - 1, target_idx)
def quick_select(arr: list[int], low: int, high: int, target: int) -> int:
if low == high:
return arr[low]
# Random pivot để tránh worst case
random_idx = random.randint(low, high)
arr[random_idx], arr[high] = arr[high], arr[random_idx]
pivot_idx = qs_partition(arr, low, high)
if pivot_idx == target:
return arr[pivot_idx]
elif pivot_idx < target:
return quick_select(arr, pivot_idx + 1, high, target)
else:
return quick_select(arr, low, pivot_idx - 1, target)
def qs_partition(arr: list[int], low: int, high: int) -> int:
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
print(find_kth_largest([3, 2, 1, 5, 6, 4], 2)) # 5Time: O(n) trung bình, O(n²) worst case
Space: O(1) — in-place partitioning
Tại sao O(n)? Trung bình, mỗi lần đệ quy làm việc trên nửa số phần tử: n + n/2 + n/4 + ... = 2n = O(n).
9. Bài Interview: Binary Search
Problem 4: Search in Rotated Sorted Array
LeetCode 33: Tìm target trong array đã sorted rồi bị rotated tại một pivot.
Insight chính: Ít nhất một nửa của array luôn sorted. Xác định nửa nào, rồi kiểm tra xem target có nằm trong nửa đó không.
Sorted ban đầu: [0, 1, 2, 4, 5, 6, 7]
Rotated tại idx 4: [4, 5, 6, 7, 0, 1, 2]
↑ sorted ↑ ↑ sorted ↑// TypeScript - Search in Rotated Sorted Array
function searchRotated(nums: number[], target: number): number {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
if (nums[mid] === target) return mid;
// Xác định nửa nào sorted
if (nums[left] <= nums[mid]) {
// Nửa trái sorted
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1; // Target trong nửa trái sorted
} else {
left = mid + 1; // Target trong nửa phải
}
} else {
// Nửa phải sorted
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1; // Target trong nửa phải sorted
} else {
right = mid - 1; // Target trong nửa trái
}
}
}
return -1;
}
console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 0)); // 4
console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 3)); // -1# Python - Search in Rotated Sorted Array
def search_rotated(nums: list[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
# Xác định nửa nào sorted
if nums[left] <= nums[mid]:
# Nửa trái sorted
if nums[left] <= target < nums[mid]:
right = mid - 1 # Target trong nửa trái sorted
else:
left = mid + 1 # Target trong nửa phải
else:
# Nửa phải sorted
if nums[mid] < target <= nums[right]:
left = mid + 1 # Target trong nửa phải sorted
else:
right = mid - 1 # Target trong nửa trái
return -1
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0)) # 4Trace tìm target=0 trong [4, 5, 6, 7, 0, 1, 2]:
Step 1: left=0, right=6, mid=3 → nums[3]=7 ≠ 0
nums[0]=4 <= nums[3]=7 → nửa trái sorted [4,5,6,7]
0 không trong [4, 7) → left = 4
Step 2: left=4, right=6, mid=5 → nums[5]=1 ≠ 0
nums[4]=0 <= nums[5]=1 → nửa trái sorted [0,1]
0 trong [0, 1) → right = 4
Step 3: left=4, right=4, mid=4 → nums[4]=0 = 0 → return 4 ✓Problem 5: Find First and Last Position
LeetCode 34: Tìm vị trí bắt đầu và kết thúc của giá trị target trong sorted array.
Insight chính: Dùng hai binary search — một tìm occurrence đầu, một tìm occurrence cuối.
// TypeScript - Find First and Last Position
function searchRange(nums: number[], target: number): number[] {
return [findFirst(nums, target), findLast(nums, target)];
}
function findFirst(nums: number[], target: number): number {
let left = 0, right = nums.length - 1;
let result = -1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
if (nums[mid] === target) {
result = mid;
right = mid - 1; // Tiếp tục tìm bên trái cho occurrence sớm hơn
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
function findLast(nums: number[], target: number): number {
let left = 0, right = nums.length - 1;
let result = -1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2);
if (nums[mid] === target) {
result = mid;
left = mid + 1; // Tiếp tục tìm bên phải cho occurrence muộn hơn
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
console.log(searchRange([5,7,7,8,8,10], 8)); // [3, 4]
console.log(searchRange([5,7,7,8,8,10], 6)); // [-1, -1]# Python - Find First and Last Position
def search_range(nums: list[int], target: int) -> list[int]:
return [find_first(nums, target), find_last(nums, target)]
def find_first(nums: list[int], target: int) -> int:
left, right = 0, len(nums) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
result = mid
right = mid - 1 # Tiếp tục tìm bên trái
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
def find_last(nums: list[int], target: int) -> int:
left, right = 0, len(nums) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
result = mid
left = mid + 1 # Tiếp tục tìm bên phải
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
print(search_range([5,7,7,8,8,10], 8)) # [3, 4]Time: O(log n) — hai binary search
Space: O(1)
Problem 6: Find Peak Element
LeetCode 162: Tìm peak element (phần tử lớn hơn neighbors) trong O(log n).
Insight chính: Nếu arr[mid] < arr[mid+1], peak chắc chắn tồn tại bên phải. Nếu arr[mid] > arr[mid+1], peak tồn tại bên trái (hoặc tại mid).
// TypeScript - Find Peak Element
function findPeakElement(nums: number[]): number {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (nums[mid] < nums[mid + 1]) {
left = mid + 1; // Peak ở bên phải
} else {
right = mid; // Peak ở mid hoặc bên trái
}
}
return left; // left === right, trỏ đến peak
}
console.log(findPeakElement([1, 2, 3, 1])); // 2 (giá trị 3)
console.log(findPeakElement([1, 2, 1, 3, 5, 6, 4])); // 5 (giá trị 6)# Python - Find Peak Element
def find_peak_element(nums: list[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] < nums[mid + 1]:
left = mid + 1 # Peak ở bên phải
else:
right = mid # Peak ở mid hoặc bên trái
return left # left == right, trỏ đến peak
print(find_peak_element([1, 2, 3, 1])) # 2
print(find_peak_element([1, 2, 1, 3, 5, 6, 4])) # 5Time: O(log n)
Space: O(1)
Tại sao hoạt động: Biên array là -∞, nên nếu ta đang đi "lên dốc" (tăng dần), ta chắc chắn sẽ gặp peak trước khi rơi xuống -∞ ở biên.
Problem 7: Minimum in Rotated Sorted Array
LeetCode 153: Tìm phần tử nhỏ nhất trong rotated sorted array (không có duplicate).
Insight chính: So sánh mid với right. Nếu arr[mid] > arr[right], minimum ở nửa phải. Ngược lại, nó ở nửa trái (bao gồm mid).
// TypeScript - Find Minimum in Rotated Sorted Array
function findMin(nums: number[]): number {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (nums[mid] > nums[right]) {
left = mid + 1; // Minimum ở nửa phải
} else {
right = mid; // Minimum ở mid hoặc nửa trái
}
}
return nums[left];
}
console.log(findMin([3, 4, 5, 1, 2])); // 1
console.log(findMin([4, 5, 6, 7, 0, 1, 2])); // 0
console.log(findMin([11, 13, 15, 17])); // 11 (không rotated)# Python - Find Minimum in Rotated Sorted Array
def find_min(nums: list[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[right]:
left = mid + 1 # Minimum ở nửa phải
else:
right = mid # Minimum ở mid hoặc nửa trái
return nums[left]
print(find_min([3, 4, 5, 1, 2])) # 1
print(find_min([4, 5, 6, 7, 0, 1, 2])) # 0Problem 8: Koko Eating Bananas (Binary Search on Answer)
LeetCode 875: Koko có thể ăn với tốc độ
kchuối/giờ. Cho các đống chuối vàhgiờ, tìmktối thiểu.
Insight chính: Binary search trên answer k. Với mỗi candidate speed, kiểm tra xem Koko có thể ăn hết tất cả đống trong h giờ không.
// TypeScript - Koko Eating Bananas
function minEatingSpeed(piles: number[], h: number): number {
let left = 1;
let right = Math.max(...piles);
while (left < right) {
const mid = Math.floor(left + (right - left) / 2);
if (canFinish(piles, mid, h)) {
right = mid; // Thử tốc độ chậm hơn
} else {
left = mid + 1; // Cần tốc độ nhanh hơn
}
}
return left;
}
function canFinish(piles: number[], speed: number, hours: number): boolean {
let totalHours = 0;
for (const pile of piles) {
totalHours += Math.ceil(pile / speed);
}
return totalHours <= hours;
}
console.log(minEatingSpeed([3, 6, 7, 11], 8)); // 4
console.log(minEatingSpeed([30, 11, 23, 4, 20], 5)); // 30
console.log(minEatingSpeed([30, 11, 23, 4, 20], 6)); // 23# Python - Koko Eating Bananas
import math
def min_eating_speed(piles: list[int], h: int) -> int:
left, right = 1, max(piles)
while left < right:
mid = left + (right - left) // 2
if can_finish(piles, mid, h):
right = mid # Thử tốc độ chậm hơn
else:
left = mid + 1 # Cần tốc độ nhanh hơn
return left
def can_finish(piles: list[int], speed: int, hours: int) -> bool:
total_hours = sum(math.ceil(pile / speed) for pile in piles)
return total_hours <= hours
print(min_eating_speed([3, 6, 7, 11], 8)) # 4
print(min_eating_speed([30, 11, 23, 4, 20], 5)) # 30Trace cho piles=[3,6,7,11], h=8:
Search space: [1, 11]
mid=6: ceil(3/6)+ceil(6/6)+ceil(7/6)+ceil(11/6) = 1+1+2+2 = 6 ≤ 8 ✓ → right=6
mid=3: ceil(3/3)+ceil(6/3)+ceil(7/3)+ceil(11/3) = 1+2+3+4 = 10 > 8 ✗ → left=4
mid=5: ceil(3/5)+ceil(6/5)+ceil(7/5)+ceil(11/5) = 1+2+2+3 = 8 ≤ 8 ✓ → right=5
mid=4: ceil(3/4)+ceil(6/4)+ceil(7/4)+ceil(11/4) = 1+2+2+3 = 8 ≤ 8 ✓ → right=4
left=4, right=4 → return 4Time: O(n × log(max_pile)) — binary search × feasibility check
Space: O(1)
10. Binary Search Decision Framework
Dùng decision tree này để xác định pattern binary search nào cần áp dụng:
Từ khóa nhận biết trong interview:
| Signal | Pattern |
|---|---|
| "tìm target trong sorted array" | Pattern 1: Standard |
| "first occurrence", "leftmost", "minimum thỏa mãn" | Pattern 2: Lower Bound |
| "last occurrence", "rightmost", "maximum thỏa mãn" | Pattern 2: Upper Bound |
| "minimum speed/capacity/time để đạt X" | Pattern 3: Search on Answer |
| "rotated sorted array" | Modified Pattern 1 |
| "peak element" | Biến thể Pattern 2 |
11. Bài Tập Luyện Tập
Bài Sorting
Easy:
| # | Bài | Concept chính |
|---|---|---|
| 1 | Sort Colors | Dutch National Flag |
| 2 | Merge Sorted Array | Two pointers từ cuối |
| 3 | Valid Anagram | Counting sort variant |
| 4 | Contains Duplicate | Sort + kiểm tra kề |
Medium:
| # | Bài | Concept chính |
|---|---|---|
| 5 | Merge Intervals | Sort + merge |
| 6 | Kth Largest Element | Quick Select |
| 7 | Top K Frequent Elements | Bucket sort |
| 8 | Sort List | Merge sort trên linked list |
| 9 | Largest Number | Custom comparator |
Hard:
| # | Bài | Concept chính |
|---|---|---|
| 10 | Count of Smaller Numbers | Merge sort + counting |
Bài Binary Search
Easy:
| # | Bài | Concept chính |
|---|---|---|
| 1 | Binary Search | Standard binary search |
| 2 | First Bad Version | Lower bound |
| 3 | Search Insert Position | Lower bound |
| 4 | Sqrt(x) | Binary search on answer |
Medium:
| # | Bài | Concept chính |
|---|---|---|
| 5 | Search in Rotated Array | Modified binary search |
| 6 | Find First and Last Position | Boundary search |
| 7 | Find Peak Element | Binary search trên peak |
| 8 | Find Minimum in Rotated Array | Binary search trên rotation |
| 9 | Koko Eating Bananas | Search on answer |
| 10 | Capacity to Ship Packages | Search on answer |
| 11 | Search a 2D Matrix | Flatten + binary search |
Hard:
| # | Bài | Concept chính |
|---|---|---|
| 12 | Median of Two Sorted Arrays | Binary search trên partition |
| 13 | Split Array Largest Sum | Search on answer |
| 14 | Find in Rotated Array II (có duplicate) | Xử lý duplicate |
Cách tiếp cận khuyên dùng: Giải hết bài Easy trước, rồi Medium. Chỉ thử Hard khi bạn đã thoải mái với các pattern.
Summary and Key Takeaways
Sorting Algorithms:
✅ O(n log n) là lower bound cho comparison sort — Merge Sort và Quick Sort đạt được điều này
✅ Merge Sort: stable, guarantee O(n log n), nhưng tốn O(n) space
✅ Quick Sort: nhanh nhất thực tế, in-place O(log n) space, nhưng worst case O(n²)
✅ Insertion Sort: tốt nhất cho array nhỏ và data gần sorted
Binary Search Cơ Bản:
✅ Loại bỏ một nửa search space mỗi bước — O(log n) time
✅ Yêu cầu sorted hoặc monotonic input
✅ Luôn dùng mid = left + (right - left) / 2 để tránh overflow
Binary Search Patterns:
✅ Pattern 1 (Exact match): while (left <= right), return mid khi tìm thấy
✅ Pattern 2 (Boundary): while (left < right), tìm first/last thỏa điều kiện
✅ Pattern 3 (Search on answer): Binary search trên answer space với feasibility check
Interview Tips:
✅ "Sort trước" thường giảm bài O(n²) xuống O(n log n)
✅ Binary search on answer là pattern bị đánh giá thấp nhất — tìm "minimum X để đạt Y"
✅ Biết khi nào dùng left <= right vs left < right — tùy pattern
✅ Quick Select giải bài "kth element" trong O(n) trung bình
What's Next?
Bạn đã nắm vững hai công cụ thuật toán cơ bản nhất: sorting và binary search. Các kỹ thuật này xuất hiện khắp nơi — trong bài array, bài tree, graph algorithm, và system design.
Tiếp tục series DSA:
Next: Dynamic Programming & Greedy Algorithms — Optimal substructure, memoization, và greedy choice
Previous: Heaps & Priority Queues — Các phép toán min/max hiệu quả
Series Overview: Data Structures & Algorithms Roadmap
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.