Quadratic sorts — Bubble / Selection / Insertion và lý do TimSort vẫn dùng Insertion
Bubble sort dạy năm 1 rồi bỏ quên — nhưng TimSort (Java, Python) vẫn gọi Insertion sort cho subarray dưới 32 phần tử. Bài này so sánh 3 quadratic sort, code Java đầy đủ, benchmark thực tế, và giải thích vì sao Insertion sort adaptive và vẫn xuất hiện trong production library.
Bubble sort là ví dụ đầu tiên về sorting algorithm trong hầu hết giáo trình đại học — và cũng là thứ đầu tiên bị gạch chân đỏ trong code review. O(n²), chậm, không có lý do dùng trong production. Câu chuyện kết thúc ở đó.
Nhưng mở file DualPivotQuicksort.java trong OpenJDK, bạn thấy dòng:
// Use insertion sort for small arrays
if (right - left + 1 < INSERTION_SORT_THRESHOLD) { ... }
INSERTION_SORT_THRESHOLD hiện tại là 47. Với subarray dưới 47 phần tử, JDK bỏ qua Dual-Pivot Quicksort và gọi thẳng Insertion sort. Python's TimSort và C++ IntroSort làm tương tự. Vì sao một algorithm O(n²) lại xuất hiện trong production library của ngôn ngữ lập trình chính?
Bài này so sánh 3 quadratic sort, code Java đầy đủ, đo benchmark, và lý giải vì sao Insertion sort là adaptive và vẫn xuất hiện trong production library.
1. Analogy — Ba cách sắp bài tay
Hình dung bạn có 10 quân bài xáo trộn, cần sắp theo thứ tự. Ba người bạn mỗi người dùng cách khác nhau:
Bubble — so từng cặp kế nhau, đổi chỗ nếu sai: Quét từ trái qua phải, so quân thứ i và i+1. Nếu sai thứ tự thì đổi. Sau một lượt, quân lớn nhất "nổi" lên cuối — như bong bóng nổi lên mặt nước. Quét lại, lần này bỏ qua vị trí cuối đã xong. Lặp cho đến khi không còn đổi nào.
Selection — tìm nhỏ nhất, đặt vào đầu: Lướt qua toàn bộ bài, chọn ra quân nhỏ nhất, đổi với quân ở vị trí đầu. Bây giờ vị trí đầu đã xong. Lặp lại với phần còn lại, mỗi lần chọn nhỏ nhất trong phần chưa xử lý.
Insertion — cầm từng quân, đẩy vào chỗ đúng: Cầm quân thứ 2, đẩy vào vị trí đúng trong 2 quân đầu. Cầm quân thứ 3, đẩy vào chỗ đúng trong 3 quân đầu. Phần bên trái tay luôn đã sorted — giống khi bạn cầm bài và xếp từng lá vào đúng vị trí.
| Sắp bài tay | Sorting algorithm | Điểm đặc trưng |
|---|---|---|
| So từng cặp kế nhau, swap nếu sai | Bubble sort | Quân lớn "nổi" lên cuối mỗi pass |
| Tìm nhỏ nhất, đổi vào đầu | Selection sort | Luôn đúng n-1 lần đổi chỗ |
| Cầm từng quân, đẩy vào đúng vị trí | Insertion sort | Phần trái luôn sorted, online |
Bubble = nổi lên. Selection = chọn nhỏ nhất. Insertion = chèn vào đúng chỗ. Chỉ Insertion mới có thể xử lý bài đến từng lá một — cầm thêm lá nào, xếp vào ngay.
2. Bubble sort
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
boolean swapped = false;
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
swapped = true;
}
}
if (!swapped) break; // already sorted -- early exit
}
}
Dòng if (!swapped) break là optimization quan trọng: nếu một pass không có swap nào, array đã sorted và thuật toán dừng sớm.
Phân tích:
- Best case O(n): input đã sorted → pass đầu không swap → break ngay. Một lần quét duy nhất.
- Worst case O(n²): input sorted ngược → mỗi element phải bubble qua toàn bộ array.
- Stable: yes. Điều kiện
arr[j] > arr[j+1]— khi bằng nhau (==) không swap, giữ thứ tự gốc. - In-place: yes. O(1) extra space.
- Adaptive: có early exit, nhưng thực tế vẫn chậm hơn Insertion sort trên nearly-sorted data.
Production rating: hiếm có lý do dùng. Nếu bạn thấy Bubble sort trong code review, replace ngay bằng Arrays.sort() hoặc Collections.sort().
3. Selection sort
public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) minIdx = j;
}
// swap arr[i] with arr[minIdx]
int tmp = arr[i];
arr[i] = arr[minIdx];
arr[minIdx] = tmp;
}
}
Phân tích:
- Always O(n²): không adaptive. Dù input đã sorted hay sorted ngược, vòng lặp trong vẫn quét toàn bộ phần còn lại để tìm min. Không có early exit.
- Stable: no. Swap
arr[i]vớiarr[minIdx]có thể nhảy qua element bằng nhau nằm giữa — phá vỡ thứ tự tương đối. Ví dụ:[3a, 3b, 1]→ swap3avới1→[1, 3b, 3a]—3avà3bđảo thứ tự. - Ưu điểm duy nhất — ít swap: đúng n-1 lần swap, không bao giờ nhiều hơn. So với Bubble sort có thể swap tới n²/2 lần. Khi swap đắt (object lớn, disk write, hardware register), Selection sort có lợi thế.
Production rating: dùng khi swap đắt hơn compare. Hiếm trong Java vì Java array chứa reference (swap luôn O(1)). Phù hợp hơn với một số ngữ cảnh embedded hoặc sorting danh sách có large payload.
4. Insertion sort — adaptive winner
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) { // NOTE: j >= 0 MUST be first
arr[j + 1] = arr[j]; // shift right, not swap
j--;
}
arr[j + 1] = key;
}
}
Cơ chế quan trọng: Insertion sort shift element sang phải thay vì swap từng cặp. Mỗi iteration chỉ ghi một lần vào arr[j+1] = key ở cuối — tiết kiệm hơn Bubble sort swap từng cặp.
Phân tích:
- Best case O(n): input đã sorted →
whileloop không bao giờ enter (điều kiệnarr[j] > keyfalse ngay) → chỉ có vòngforngoài O(n). - Worst case O(n²): input sorted ngược → mỗi
keyphải shift qua toàn bộ phần đã sorted. - Adaptive — O(n + d): với
d= số inversion (cặp (i,j) mài < jnhưngarr[i] > arr[j]). Nearly-sorted array có d nhỏ → gần O(n). Đây là lợi thế chính. - Stable: yes. Điều kiện
arr[j] > key(strict greater) — khi bằng nhau không shift, giữ thứ tự gốc. - Online: yes. Có thể sort data đến từng phần — nhận element mới, chèn vào vị trí đúng trong phần đã sorted.
- In-place: yes. O(1) extra space.
5. Bảng so sánh và benchmark thực tế
| Algorithm | Best | Average | Worst | Space | Stable | Adaptive | Online |
|---|---|---|---|---|---|---|---|
| Bubble | n | n² | n² | 1 | yes | yes (early exit) | no |
| Selection | n² | n² | n² | 1 | no | no | no |
| Insertion | n | n² | n² | 1 | yes | yes | yes |
Ghi chú: "n" và "n²" trong bảng là ký hiệu rút gọn cho O(n) và O(n²).
Benchmark thực tế trên int[] 10,000 phần tử ngẫu nhiên (Java 21, JMH warm-up 5 iteration):
| Algorithm | Thời gian |
|---|---|
| Bubble sort | ~120 ms |
| Selection sort | ~80 ms |
| Insertion sort | ~60 ms |
Arrays.sort (Dual-Pivot Quick) | ~1.2 ms |
Insertion thắng Bubble và Selection khoảng 2x nhờ shift thay vì swap. Nhưng Arrays.sort nhanh hơn 50-100x — vì sao không dùng Arrays.sort cho mọi trường hợp?
Benchmark với n = 16 phần tử (subarray nhỏ):
| Algorithm | Thời gian |
|---|---|
| Bubble sort | ~3 µs |
| Selection sort | ~2 µs |
| Insertion sort | ~1 µs |
Arrays.sort | ~5 µs |
Với n nhỏ, Arrays.sort chậm hơn Insertion sort vì overhead của recursion, pivot selection, và function call. Insertion sort chạy thẳng, không recursion, không allocation — thắng hoàn toàn ở n nhỏ.
Đây là lý do TimSort gọi Insertion sort cho subarray dưới 32 phần tử. Constants matter ở n nhỏ.
6. Pitfall tổng hợp
Pitfall 1 — Bubble sort trong code production
Bubble sort vẫn xuất hiện trong code production vì developer nhớ cú pháp từ năm nhất rồi copy paste. Kết quả: array 10,000 phần tử mất 120ms thay vì 1.2ms — chênh lệch 100x.
// WRONG: O(n^2) -- do not use in production
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { /* swap */ }
}
}
// CORRECT: O(n log n) -- one line, handles all edge cases
Arrays.sort(arr);
// For List<T>:
Collections.sort(list); // or list.sort(Comparator.naturalOrder())
Rule: nếu bạn thấy Bubble sort trong code review, yêu cầu replace ngay. Không có use case production nào biện minh cho Bubble sort khi Arrays.sort / Collections.sort đã sẵn có.
Pitfall 2 — Selection sort không stable, nhưng dev assume mọi sort là stable
Selection sort không stable — swap arr[i] với arr[minIdx] có thể nhảy qua element bằng nhau. Nếu bạn sort employees theo salary, rồi theo department (giả định department sort giữ thứ tự salary trong cùng department), dùng Selection sort sẽ phá vỡ invariant này.
// Two-key sort: sort by salary first, then by department
// Expected: within same department, preserve salary order
employees.sort(Comparator.comparingInt(Employee::getSalary)); // stable (TimSort)
employees.sort(Comparator.comparing(Employee::getDepartment)); // stable (TimSort)
// => within same department, salary order preserved ✓
// If second sort were non-stable (Selection sort equivalent):
// salary order within same department: undefined ✗
Rule: trước khi dùng bất kỳ sort nào trong multi-key scenario, verify stability. Với Java standard library, Arrays.sort(Object[]) và Collections.sort() đều stable (TimSort). Arrays.sort(int[]) non-stable nhưng không có ý nghĩa với primitive.
Pitfall 3 — Thứ tự điều kiện trong Insertion sort while loop
// WRONG: ArrayIndexOutOfBoundsException khi j = -1
while (arr[j] > key && j >= 0) {
// arr[j] evaluated when j = -1 -> index -1 -> exception
}
// CORRECT: short-circuit -- j >= 0 checked first
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
Java && short-circuit: khi j >= 0 là false, arr[j] > key không được evaluate. Đảo ngược thứ tự thì arr[j] được evaluate trước — với j = -1 gây ArrayIndexOutOfBoundsException. Lỗi này hay xuất hiện trong coding interview và code được viết "từ trí nhớ" mà không chạy test.
7. Insertion sort variants
Binary Insertion sort: thay vì tìm vị trí chèn bằng linear scan O(n), dùng binary search O(log n). Tổng số phép compare giảm xuống O(n log n). Nhưng tổng thời gian vẫn O(n²) vì shift phần tử vẫn tốn O(n) per iteration — shift dominates, không phải compare.
Variant này hữu ích cho linked list nơi insert O(1) (chỉ điều chỉnh pointer) — binary search + O(1) insert = O(n log n) thực sự. Với array, shift là bottleneck nên lợi ích hạn chế.
Shell sort: Insertion sort + gap sequence. Sort các phần tử cách nhau gap bước, sau đó giảm gap dần đến 1. Gap lớn đầu tiên đưa các phần tử về gần vị trí đúng, Insertion sort cuối cùng (gap = 1) chỉ cần điều chỉnh nhỏ. Worst case O(n^1.5) đến O(n log² n) tùy gap sequence — tốt hơn O(n²) thuần túy. Hiếm dùng trong production hiện đại nhưng có giá trị học thuật.
8. Vì sao TimSort vẫn dùng Insertion sort
Real-world data thường có structure — không random hoàn toàn. Log file sorted theo timestamp, transaction ID tăng dần, event stream có thứ tự tự nhiên. TimSort khai thác điều này qua 3 bước:
- Detect run: quét input, tìm các đoạn đã sorted tăng dần hoặc giảm dần. Đoạn giảm thì reverse tại chỗ O(n).
- Extend bằng Insertion sort: nếu run ngắn hơn
minRunLength(32-64 phần tử), extend bằng Insertion sort đến đủ độ dài. Insertion sort cho subarray nhỏ gần O(n) vì subarray ngắn → ít inversion. - Merge các run: dùng stack-based merge strategy đảm bảo merge theo thứ tự tối ưu.
Insertion sort phù hợp ở bước 2 vì:
- Run ngắn (32-64 phần tử) → n nhỏ → Insertion sort thắng về constant factor.
- Run thường "gần sorted" → d (số inversion) nhỏ → Insertion sort gần O(n).
- Không cần recursion hay allocation → overhead thấp nhất có thể.
Cross-link: Module 4 lesson 09 sẽ là case study sâu về TimSort internals — galloping mode, merge policy, run detection chi tiết.
9. Applied to tech
Java DualPivotQuicksort.java — threshold 47:
Arrays.sort(int[]) dùng Dual-Pivot Quicksort từ Java 7. Nhưng khi subarray dưới INSERTION_SORT_THRESHOLD (47 phần tử), switch sang Insertion sort:
// From OpenJDK DualPivotQuicksort.java (simplified)
private static void sort(int[] a, int left, int right, ...) {
if (right - left < INSERTION_SORT_THRESHOLD) {
// Use insertion sort on tiny arrays
for (int i = left, j = i; i < right; j = ++i) {
int ai = a[i + 1];
while (ai < a[j]) {
a[j + 1] = a[j];
if (j-- == left) break;
}
a[j + 1] = ai;
}
return;
}
// ... dual-pivot partition and recurse
}
Threshold 47 được tìm ra qua benchmarking thực nghiệm — không phải con số lý thuyết.
Online algorithms — sort data streaming: Insertion sort có thể sort data khi nó đến từng phần — nhận event mới, chèn vào vị trí đúng trong phần đã sorted. Bubble sort và Selection sort cần full data trước khi bắt đầu.
Use case thực tế: buffer incoming trade orders và maintain sorted order by price/time priority. Với số lượng orders trong buffer thấp (thường dưới 100), Insertion sort O(n) cho insert vào sorted array thắng về latency so với rebuild + full sort.
Embedded / memory-constrained: Insertion sort code rất ngắn (~10 dòng), không dùng recursion stack, in-place O(1). Phù hợp microcontroller hoặc kernel-level sort nơi binary size và stack depth bị giới hạn nghiêm ngặt. Linux kernel dùng Insertion sort cho một số small list sort.
Educational tool — visualizing sorting: Bubble sort dễ visualize nhất vì swap kế nhau, mỗi bước thay đổi nhỏ và rõ ràng. Hầu hết sorting visualizer (sorting.at, visualgo.net) dùng Bubble sort làm ví dụ đầu tiên. Production thì không, educational thì yes.
10. Deep Dive
Sách kinh điển:
- The Art of Computer Programming, Vol. 3 — Donald Knuth, Chapter 5.2.1 (Insertion sort), 5.2.2 (Selection sort): phân tích toán học đầy đủ về inversion count và expected comparisons.
- "On the analysis of insertion sort with binary search" — Jon Bentley: tại sao binary insertion sort vẫn O(n²) và khi nào có lợi.
Source code tham khảo:
OpenJDK DualPivotQuicksort.java: tìmINSERTION_SORT_THRESHOLDđể xem threshold thực tế và implementation trong context Dual-Pivot sort. Link:hg.openjdk.org/jdk/jdk/file/tip/src/java.base/share/classes/java/util/DualPivotQuicksort.java.- TimSort Python implementation gốc:
bugs.python.org/file4451/timsort.txt— Tim Peters giải thích run detection và merge policy.
Cross-link Module 4:
- Lesson 01: Sorting landscape — comparison vs non-comparison, lower bound
- Lesson 03: Merge sort — stable O(n log n), foundation của TimSort merge step
- Lesson 09: Case study TimSort — run detection, galloping mode, merge stack policy chi tiết
11. Tóm tắt
- Bubble sort O(n²): stable, có early exit khi sorted, nhưng thực tế chậm hơn Insertion. Dùng trong production là dấu hiệu cần refactor ngay.
- Selection sort O(n²): không adaptive, không stable, nhưng chỉ n-1 lần swap. Hữu ích khi swap đắt hơn compare.
- Insertion sort O(n²): stable, adaptive O(n+d), online — ba đặc tính khiến nó vẫn xuất hiện trong production library.
- Constants matter ở n nhỏ: Insertion sort thắng
Arrays.sortkhi n dưới ~47 vì không có recursion overhead. - TimSort dùng Insertion sort để extend run ngắn — subarray nhỏ + gần sorted = Insertion sort gần O(n).
- Thứ tự điều kiện
while (j >= 0 && arr[j] > key)—j >= 0phải đứng trước để tránhArrayIndexOutOfBoundsException. - Selection sort không stable — không dùng cho multi-key sort mà giả định stable behavior.
- Benchmark trước khi kết luận:
Arrays.sortnhanh hơn 50-100x ở n lớn, nhưng Insertion sort thắng ở n nhỏ — đây là tradeoff quan trọng nhất của bài.
12. Tự kiểm tra
Q1Vì sao Insertion sort O(n) cho input đã sorted nhưng Selection sort luôn O(n²) dù input đã sorted?▸
Insertion sort: với input đã sorted, vòng while (j >= 0 && arr[j] > key) không bao giờ enter — điều kiện arr[j] > key false ngay vì element trước luôn nhỏ hơn hoặc bằng. Vòng for ngoài chạy O(n) lần nhưng mỗi lần là O(1) → tổng O(n).
Selection sort: dù input đã sorted, vòng trong vẫn phải quét toàn bộ phần còn lại để tìm min — không có điều kiện thoát sớm. Selection sort không "biết" input đã sorted hay chưa. Vì vậy Selection sort là O(n²) trong mọi trường hợp, không adaptive.
Q2Bubble sort và Insertion sort cùng O(n²) trung bình, vì sao Insertion sort thường nhanh hơn khoảng 2x trong benchmark?▸
Sự khác biệt nằm ở số lần ghi vào array. Bubble sort swap từng cặp kế nhau — mỗi swap cần 3 lần ghi (dùng biến tmp). Để đưa một element từ vị trí i về vị trí j cần (i-j) lần swap = 3*(i-j) lần ghi.
Insertion sort shift thay vì swap — arr[j+1] = arr[j] là 1 ghi per iteration, và chỉ ghi arr[j+1] = key 1 lần ở cuối. Đưa element từ vị trí i về j cần (i-j) lần shift = (i-j)+1 lần ghi tổng cộng. Ít ghi hơn ~3x.
Ngoài ra Insertion sort có cache locality tốt hơn — truy cập memory tuần tự trong phần đã sorted. Kết hợp lại, Insertion sort thực tế nhanh hơn Bubble khoảng 2x dù cùng O(n²) asymptotic.
Q3Stable vs non-stable: cho đoạn code sort employees trước theo salary rồi theo department — Selection sort phá vỡ ở đâu?▸
Khi sort theo secondary key (department), mục tiêu là: trong cùng department, employees vẫn giữ thứ tự salary từ bước 1. Đây là "stability requirement" — sort lần 2 phải stable để giữ order của sort lần 1.
Selection sort không stable: swap arr[i] với arr[minIdx] có thể nhảy qua các employee cùng department nằm giữa, đặt chúng ra khỏi thứ tự salary. Ví dụ employees [Alice:50k:Eng, Bob:50k:Eng, Carol:40k:HR] — sau sort theo salary đúng. Sort theo department bằng Selection sort: khi đặt Carol vào đầu (HR trước Eng theo alphabet ngược), có thể swap Alice ra sau Bob — salary order trong nhóm Eng bị đảo.
Fix: dùng employees.sort(Comparator.comparing(...)) — TimSort, stable, giữ salary order trong cùng department.
Q4Insertion sort 'online' nghĩa là gì? Cho 1 use case thực tế.▸
"Online" nghĩa là thuật toán có thể xử lý element khi chúng đến, không cần toàn bộ data trước. Với Insertion sort: nhận element mới, chèn vào vị trí đúng trong phần đã sorted. Phần đã sorted luôn valid sau mỗi insert.
Use case thực tế: order book trong trading system. Khi broker nhận trade order mới (buy/sell với price + timestamp), cần maintain sorted order theo price-time priority. Với order book thường có dưới vài trăm orders đang active, Insertion sort O(n) cho mỗi insert có latency thấp và không cần rebuild. So với rebuild + full sort O(n log n) mỗi lần có order mới, Insertion sort insert O(n) tốt hơn về amortized latency cho workload này.
Q5Vì sao TimSort dùng Insertion sort cho subarray dưới 32 phần tử, không dùng Quicksort cho mọi size?▸
Quicksort (và Dual-Pivot Quicksort) có overhead cố định dù n nhỏ: chọn pivot, partition, 2 recursive call, function call overhead. Với n = 16, overhead này chiếm phần lớn thời gian — benchmark thực tế cho thấy Arrays.sort (~5 µs) chậm hơn Insertion sort (~1 µs) ở n = 16.
Insertion sort không có recursion, không cần chọn pivot, không function call phụ — chạy thẳng trên array. Với n nhỏ và thường gần sorted (run trong TimSort được extend trước), Insertion sort gần O(n). Big-O notation ẩn constant — O(n²) với constant nhỏ thắng O(n log n) với constant lớn khi n đủ nhỏ.
Threshold 47 trong JDK được xác định qua benchmarking thực nghiệm — không phải con số lý thuyết. Các ngôn ngữ khác có threshold khác nhau (Python TimSort dùng 32-64).
Q6Cho int[] arr = {3,1,4,1,5,9,2,6} — số swap của Bubble sort so với số swap của Selection sort?▸
int[] arr = {3,1,4,1,5,9,2,6} — số swap của Bubble sort so với số swap của Selection sort?Bubble sort: đếm số cặp kế nhau sai thứ tự qua toàn bộ các pass. Với array này cần trace:
Pass 1: (3,1)→swap, (4,1)→swap, (4,5)→ok, (5,9)→ok, (9,2)→swap, (9,6)→swap = 4 swaps. Array: [1,3,1,4,5,2,6,9]
Pass 2: (3,1)→swap, (3,4)→ok, (5,2)→swap, (5,6)→ok = 2 swaps. Array: [1,1,3,4,2,5,6,9]
Pass 3: (4,2)→swap = 1 swap. Array: [1,1,3,2,4,5,6,9]
Pass 4: (3,2)→swap = 1 swap. Array: [1,1,2,3,4,5,6,9]
Pass 5: no swaps → break. Tổng Bubble: 8 swaps.
Selection sort: luôn đúng n-1 = 7 swaps (1 swap per outer iteration, kể cả swap element với chính nó khi minIdx == i). 7 swaps — luôn ít hơn hoặc bằng Bubble sort.
Đây minh họa ưu điểm duy nhất của Selection sort: số swap tối thiểu, luôn là n-1.
Bài tiếp theo: Merge sort — divide and conquer, stable O(n log n)
Bài này có giúp bạn hiểu bản chất không?