Counting / Radix / Bucket sort — Non-comparison O(n) khi key range nhỏ
Comparison sort bị chặn bởi lower bound n log n — nhưng Counting sort đạt O(n) mà không vi phạm. Bài này dạy 3 non-comparison sort: counting (range nhỏ), radix LSD/MSD (fixed-width key), bucket (uniform distribution), khi nào pick từng loại, và cảnh báo về memory cost.
Lesson 01 của Module 4 đã chứng minh lower bound n log n cho mọi comparison sort — không có cách nào tốt hơn khi chỉ dùng phép so sánh. Nhưng nếu cần sort 100 triệu bản ghi int trong range 0 đến 1000 thì sao? Counting sort chạy O(n + k) với k = 1001 — tức là O(n), không phụ thuộc n log n.
Điểm mấu chốt: lower bound đó chỉ áp dụng cho comparison-based sort. Khi biết thêm thông tin về key — range nhỏ, số digit cố định, hay distribution đồng đều — có thể bypass phép so sánh hoàn toàn và đạt tốc độ nhanh hơn.
Bài này dạy 3 non-comparison sort: Counting sort (range nhỏ), Radix sort LSD/MSD (fixed-width key), và Bucket sort (uniform distribution). Bao gồm khi nào pick từng loại và cảnh báo về memory cost.
1. Analogy — Sắp bài theo suit
Hình dung một bộ bài 52 lá cần sắp theo suit: cơ, rô, nhép, bích.
Nếu dùng comparison sort: cầm hai lá, so sánh, đặt lá nhỏ hơn sang trái — lặp lại đến khi xong. Mỗi bước tốn 1 phép so sánh.
Nếu dùng non-comparison sort: đặt sẵn 4 ô trên bàn, mỗi ô là 1 suit. Nhặt từng lá, nhìn suit, bỏ thẳng vào ô đúng — không cần so sánh lá nào với lá nào. Hết bài thì đọc theo thứ tự ô. Nhanh hơn vì biết trước chỉ có 4 suit.
| Sắp bài theo suit | Counting sort |
|---|---|
| 4 ô suit đặt sẵn trên bàn | Counting array kích thước k (key range) |
| Nhìn suit của lá, bỏ vào ô tương ứng | Đọc key, tăng count[key]++ |
| Không so sánh lá nào với lá nào | Không dùng phép so sánh element |
| Biết trước chỉ có 4 suit | Biết trước key range [0, k) |
| Đọc ô theo thứ tự để lấy kết quả | Prefix sum + place = output sorted |
Non-comparison sort = đặt sẵn chỗ, bỏ thẳng vào. Không cần hỏi "lá này lớn hơn lá kia không?" — chỉ cần hỏi "lá này thuộc ô nào?". Đổi lại, phải biết trước có bao nhiêu ô.
2. Counting sort
2.1 Cơ chế
Cho array các số nguyên trong range [0, k). Ba bước:
- Count: đếm tần suất mỗi giá trị vào
count[0..k-1]. - Prefix sum: biến
count[i]thành số lượng element có giá trị nhỏ hơn hoặc bằngi— đây là vị trí cuối của giá trịitrong output. - Place (right-to-left): duyệt array gốc từ phải sang trái, đặt từng element vào đúng vị trí trong output.
public static void countingSort(int[] arr, int k) {
// arr values in [0, k)
int[] count = new int[k];
for (int x : arr) count[x]++;
// prefix sum -> count[i] = position in output (exclusive upper bound)
for (int i = 1; i < k; i++) count[i] += count[i - 1];
// place from right to left for stability
int[] output = new int[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
output[--count[arr[i]]] = arr[i];
}
System.arraycopy(output, 0, arr, 0, arr.length);
}
2.2 Trace ví dụ
Sort [4, 2, 2, 8, 3, 3, 1] với k = 10:
Input: [4, 2, 2, 8, 3, 3, 1]
Count step:
count[0]=0, count[1]=1, count[2]=2, count[3]=2
count[4]=1, count[5..7]=0, count[8]=1
Prefix sum:
count[0]=0, count[1]=1, count[2]=3, count[3]=5
count[4]=6, count[5..7]=6, count[8]=7
Place right-to-left:
i=6: arr[6]=1 -> output[--count[1]] = output[0] = 1
i=5: arr[5]=3 -> output[--count[3]] = output[4] = 3
i=4: arr[4]=3 -> output[--count[3]] = output[3] = 3
i=3: arr[3]=8 -> output[--count[8]] = output[6] = 8
i=2: arr[2]=2 -> output[--count[2]] = output[2] = 2
i=1: arr[1]=2 -> output[--count[2]] = output[1] = 2
i=0: arr[0]=4 -> output[--count[4]] = output[5] = 4
Output: [1, 2, 2, 3, 3, 4, 8]
2.3 Properties
| Thuộc tính | Giá trị |
|---|---|
| Time | O(n + k) |
| Space | O(n + k) |
| Stable | Yes (place right-to-left giữ thứ tự gốc) |
| In-place | No |
| Khi nào dùng | Integer key trong range nhỏ: 0-100, 0-1000 |
Vì sao right-to-left đảm bảo stability? Khi duyệt từ phải sang trái, element xuất hiện sau trong input được đặt vào ô cuối cùng còn trống cho giá trị đó. Element xuất hiện trước được đặt vào ô trước đó. Thứ tự tương đối của các element bằng nhau được bảo toàn.
3. Radix sort (LSD — Least Significant Digit first)
3.1 Ý tưởng
Counting sort đòi hỏi k nhỏ. Với số nguyên 32-bit, k có thể lên tới hàng tỉ — không thực tế. Radix sort xử lý từng chữ số (digit) riêng biệt, mỗi lần Counting sort theo digit đó.
LSD (Least Significant Digit): bắt đầu từ chữ số ít quan trọng nhất (hàng đơn vị), sort theo đó, rồi tiến dần lên hàng chục, hàng trăm... LSD đòi hỏi sub-sort stable để các chữ số trước không bị phá vỡ khi sort chữ số sau.
public static void radixSortLSD(int[] arr) {
int max = Arrays.stream(arr).max().getAsInt();
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSortByDigit(arr, exp);
}
}
private static void countingSortByDigit(int[] arr, int exp) {
int[] count = new int[10];
for (int x : arr) count[(x / exp) % 10]++;
for (int i = 1; i < 10; i++) count[i] += count[i - 1];
int[] output = new int[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
int d = (arr[i] / exp) % 10;
output[--count[d]] = arr[i];
}
System.arraycopy(output, 0, arr, 0, arr.length);
}
3.2 Trace ví dụ
Sort [170, 45, 75, 90, 802, 24, 2, 66]:
exp=1 (units digit):
Input: [170, 45, 75, 90, 802, 24, 2, 66]
Digits: [ 0, 5, 5, 0, 2, 4, 2, 6]
After: [170, 90, 802, 2, 24, 45, 75, 66]
exp=10 (tens digit):
Input: [170, 90, 802, 2, 24, 45, 75, 66]
Digits: [ 7, 9, 0, 0, 2, 4, 7, 6]
After: [802, 2, 24, 45, 66, 170, 75, 90]
exp=100 (hundreds digit):
Input: [802, 2, 24, 45, 66, 170, 75, 90]
Digits: [ 8, 0, 0, 0, 0, 1, 0, 0]
After: [2, 24, 45, 66, 75, 90, 170, 802]
3.3 Properties và variant
| Thuộc tính | Giá trị |
|---|---|
| Time | O(n × d), d = số digit |
| Space | O(n + k), k = base (10 hoặc 256) |
| Stable | Yes (dùng stable sub-sort) |
| In-place | No |
| Khi nào dùng | Fixed-width key: 32-bit int, 64-bit long, fixed-length string |
MSD (Most Significant Digit first): bắt đầu từ chữ số quan trọng nhất. Xử lý đệ quy theo bucket — phù hợp cho variable-length string và khi muốn early termination. Phức tạp hơn LSD vì cần quản lý recursive bucket riêng cho từng prefix.
Base lớn hơn = ít pass hơn: thay vì base 10 (10 digits, d = 10 cho 10-tỉ), dùng base 256 (1 byte = 1 digit, d = 4 pass cho 32-bit int). Mỗi pass tốn O(n + 256) thay O(n + 10) — tổng nhanh hơn về constant.
4. Bucket sort
4.1 Ý tưởng
Bucket sort phù hợp khi data có uniform distribution trên một range đã biết. Chia range thành k bucket đều nhau, distribute từng element vào bucket tương ứng, sort từng bucket bằng comparison sort, rồi concat.
public static void bucketSort(double[] arr, int bucketCount) {
// arr values in [0.0, 1.0)
@SuppressWarnings("unchecked")
List<Double>[] buckets = new List[bucketCount];
for (int i = 0; i < bucketCount; i++) {
buckets[i] = new ArrayList<>();
}
// distribute: element x goes into bucket floor(x * bucketCount)
for (double x : arr) {
int idx = (int) (x * bucketCount);
buckets[idx].add(x);
}
// sort each bucket (insertion sort wins for small buckets)
int pos = 0;
for (List<Double> bucket : buckets) {
Collections.sort(bucket);
for (double x : bucket) arr[pos++] = x;
}
}
4.2 Phân tích
Best case (uniform distribution): mỗi bucket nhận xấp xỉ n/k element. Sort từng bucket tốn O((n/k) log(n/k)). Tổng: O(n + k × (n/k) log(n/k)) = O(n + n log(n/k)). Khi k = n, O(n) — tuyến tính.
Worst case (skewed distribution): toàn bộ element rơi vào 1 bucket. Sort bucket đó tốn O(n log n) — degenerate về comparison sort.
| Phân phối | Complexity |
|---|---|
| Uniform | O(n + k) expected |
| Skewed / clustered | O(n log n) hoặc O(n squared) |
5. Bảng so sánh đầy đủ
Cột Time dùng ký hiệu ngắn (không có backtick) để tránh vỡ bảng:
| Sort | Time | Space | Stable | Khi nào dùng |
|---|---|---|---|---|
| Counting | n+k | n+k | Yes | Integer key range nhỏ |
| Radix LSD | n×d | n+k | Yes | Fixed-width key (int, long, fixed string) |
| Bucket | n+k avg | n+k | Yes (với stable inner sort) | Uniform float distribution |
| Quick | n log n avg | log n | No | Generic random data |
| Merge | n log n | n | Yes | Generic, stable cần thiết |
| Heap | n log n | 1 | No | In-place, no stable requirement |
6. Vì sao non-comparison sort không phá lower bound n log n
Lower bound n log n chỉ áp dụng cho comparison-based sort — chứng minh qua decision tree với n! permutation (xem Module 4 lesson 01).
Non-comparison sort không tạo decision tree so sánh — thay vào đó dùng assumption thêm về input:
- Counting sort: key nằm trong range nhỏ
[0, k)— dùng key làm index trực tiếp. - Radix sort: key có số digit d cố định — xử lý từng digit độc lập.
- Bucket sort: distribution gần uniform — phân bổ đều vào bucket.
Khi assumption đó sai (k lớn, d lớn, distribution skewed), non-comparison sort không còn hiệu quả hơn — hoặc thậm chí tệ hơn về space và worst case.
Trade-off: thêm space + giả định về input, đổi lấy tốc độ tốt hơn. Đây không phải "phá" lower bound — mà là thay đổi bài toán: từ "sort bất kỳ data" thành "sort data có cấu trúc đặc biệt".
Decision tree argument: mỗi phép so sánh là một nhánh nhị phân, toàn bộ cây phải có n! leaf. Counting sort không có "nhánh so sánh" — nó dùng index arithmetic: count[arr[i]]++ là O(1) không phân nhánh theo giá trị. Vì vậy argument không áp dụng, lower bound không bị phá, chỉ là bài toán khác.
7. Pitfall tổng hợp
Pitfall 1 — Counting sort với range lớn: OOM
// WRONG: k = Integer.MAX_VALUE -> allocate 2GB array -> OOM
int[] arr = {Integer.MAX_VALUE - 1, 0, 5};
int k = Integer.MAX_VALUE;
int[] count = new int[k]; // OutOfMemoryError
Counting sort chỉ thực tế khi k = O(n) hoặc nhỏ hơn. Với array 1 triệu phần tử, k hợp lý là dưới vài triệu. Nếu range lớn, dùng Radix sort (chia nhỏ key thành digit) thay Counting sort trực tiếp.
// Check before allocating: if k >> n, consider radix sort instead
int min = Arrays.stream(arr).min().getAsInt();
int max = Arrays.stream(arr).max().getAsInt();
long range = (long) max - min + 1;
if (range > 10L * arr.length) {
// range too large for counting sort -- use radix or comparison sort
Arrays.sort(arr);
} else {
countingSort(arr, (int) range, min);
}
Pitfall 2 — Radix sort cho variable-length string: thứ tự sai
LSD Radix sort giả định mọi key có cùng độ dài. Với string có độ dài khác nhau, cần padding bằng null character hay space — nhưng padding gây ra vấn đề: lexicographic order với null padding khác natural order.
Input strings: ["car", "cat", "a", "be"]
Pad to length 3: ["car", "cat", "a ", "be "] <- space padding
LSD result might differ from natural sort depending on how spaces compare
Safe approach: use MSD radix sort (handles variable length natively)
or use comparison sort with standard String.compareTo()
Với variable-length string trong production: ưu tiên MSD radix hoặc Counting sort theo ký tự từng level. Hoặc đơn giản là dùng Arrays.sort(String[]) (TimSort) nếu không có bottleneck thực sự đo được.
Pitfall 3 — Bucket sort với non-uniform distribution
// If all values cluster near 0.01, they all fall into bucket 0
double[] arr = {0.001, 0.005, 0.003, 0.007, 0.002, 0.8, 0.9};
// Buckets of size 0.1: almost all elements in bucket 0
// -> O(n log n) for that one bucket -> no gain over comparison sort
Đo distribution trước khi dùng Bucket sort. Nếu data có pattern (log-normal, Zipfian, power-law), cần thiết kế bucket boundary theo distribution thực tế thay vì chia đều. Hoặc chuyển sang Counting/Radix sort nếu key có thể map sang integer.
8. Java standard library và production note
Arrays.sort(int[]) Java không dùng Counting hay Radix sort mặc định — dùng Dual-Pivot Quicksort. Lý do: Arrays.sort nhận mảng tùy ý, không biết trước key range. Non-comparison sort đòi hỏi programmer cung cấp thêm context (k, digit count, distribution) — JDK không thể đoán.
// JDK default: Dual-Pivot Quicksort
int[] arr = {5, 3, 8, 1, 9};
Arrays.sort(arr); // O(n log n), không phải O(n+k)
// If you know range is small, implement counting sort manually:
// countingSort(arr, maxValue + 1);
Ngoài JDK:
- Apache Commons Primitives / Eclipse Collections: cung cấp
SortedListsvới Radix sort option choint[]vàlong[]. - fastutil library:
IntArrays.radixSort(int[])— LSD radix sort cho primitive int, thực tế nhanh hơnArrays.sortkhoảng 2-3x trên large random int array. - Chrome V8 engine: sort array số nguyên nhỏ trong JavaScript dùng Counting sort variant khi detect range phù hợp.
9. Ứng dụng thực tế
Database aggregation — GROUP BY với integer key: khi aggregate COUNT(*) theo http_status_code (range 100-599), database engine hiệu quả nhất là giữ counting array kích thước 600 — đây chính là Counting sort structure. Tổng O(n) thay vì O(n log n) cho sort-then-group.
Histograms — log buckets HTTP status: monitoring system thống kê phân phối HTTP response code (2xx/3xx/4xx/5xx) từ hàng triệu request log. Range chỉ 600 giá trị → Counting sort O(n) để sort + aggregate trong một pass, không cần comparison sort nào.
Networking — packet sort theo IP prefix: router sort gói tin theo destination IP để longest-prefix-match. Địa chỉ IP 32-bit gồm 4 octet → Radix sort 4 phase, mỗi phase Counting sort theo 1 byte (base 256, k=256). Tổng: O(4n) = O(n), nhanh hơn comparison sort với n vài triệu gói tin/giây.
Graphics rendering — depth sort (painters algorithm): để render overlapping object theo thứ tự front-to-back, Bucket sort chia z-range thành k bucket. Với uniform object distribution, mỗi bucket nhận vài object, sort từng bucket bằng Insertion sort, concat → O(n + k) expected. Dùng cho game engine offline rendering pass.
10. Deep Dive
Sách kinh điển:
- The Art of Computer Programming, Vol. 3 — Donald Knuth, Section 5.2.5 (Radix Sort) và Section 5.2.4 (Distribution Sort / Counting Sort): phân tích toán học đầy đủ. Knuth là nguồn gốc của tên "radix sort" và phân tích counting sort như "distribution sort".
- Introduction to Algorithms (CLRS), Chapter 8 (Sorting in Linear Time): Counting sort, Radix sort, Bucket sort — proof O(n) và điều kiện cần.
Bài viết kỹ thuật:
- "Engineering Radix Sort" — McIlroy, Bostic & McIlroy (1993): Radix sort cho string với trie-based optimization. Nguồn gốc của nhiều string sort production library. Computing Systems, Vol. 6(1).
- "Burstsort" — Sinha & Zobel (2004): string sort dùng trie + bucket — vượt qua MSD radix sort về cache performance trên modern hardware. Cross-link: Module 7 lesson 06 (string matching) sẽ cover trie structure.
Cross-link trong khóa học:
- Module 4 lesson 01: Lower bound n log n cho comparison sort — nền tảng để hiểu tại sao non-comparison sort có thể faster.
- Module 4 lesson 04: Quick sort — algorithm được Java dùng mặc định thay Radix sort và lý do.
- Module 7: String matching — MSD radix sort cho string, trie structure, Burstsort.
11. Tóm tắt
- Lower bound n log n áp dụng cho comparison sort — non-comparison sort bypass bằng cách dùng thêm assumption về input (range, digit count, distribution).
- Counting sort O(n + k): đếm tần suất, prefix sum, place right-to-left. Stable. Chỉ dùng khi k = O(n) hoặc nhỏ hơn — k lớn gây OOM.
- Radix LSD O(n × d): sort từng digit từ phải sang trái bằng Counting sort stable. Hiệu quả cho fixed-width key: 32-bit int (d=4 với base 256), fixed-length string.
- MSD Radix: sort từ digit quan trọng nhất, xử lý đệ quy — phù hợp variable-length string, cho phép early termination.
- Bucket sort O(n + k) expected: distribute vào bucket, sort từng bucket. Tốt cho uniform float distribution. Worst case O(n squared) khi data skewed.
- Java
Arrays.sort(int[])dùng Dual-Pivot Quick, không Radix — vì không biết trước range. Muốn Radix sort thì dùngfastutilhoặc tự implement. - Trade-off cốt lõi: non-comparison sort nhanh hơn nhưng tốn thêm O(n + k) space và yêu cầu biết trước key structure. Không có "free lunch" — chỉ là shift assumption từ "không biết gì về data" sang "biết range/digit/distribution".
12. Tự kiểm tra
Q1Vì sao Counting sort O(n) không phá lower bound n log n của comparison sort?▸
Lower bound n log n được chứng minh qua decision tree argument: mọi comparison sort phải distinguish được n! permutation, cây nhị phân có n! leaf phải có chiều cao tối thiểu log₂(n!) = Θ(n log n). Argument này chỉ áp dụng cho algorithm dựa trên phép so sánh.
Counting sort không so sánh element với nhau — nó dùng key làm index trực tiếp: count[arr[i]]++ là một phép ghi vào array theo index, không phải so sánh phân nhánh. Vì không tạo decision tree so sánh, argument không áp dụng, và lower bound không bị "phá" — bài toán đã thay đổi: từ "sort bất kỳ data" thành "sort integer trong range nhỏ đã biết". Cái giá phải trả là O(k) space và giả định về key range.
Q2Counting sort place element từ right-to-left thay vì left-to-right — ảnh hưởng gì đến stability?▸
Place right-to-left đảm bảo stability: duyệt array gốc từ cuối về đầu, element nào xuất hiện cuối trong input được đặt vào ô cuối cùng còn trống cho giá trị đó. Element xuất hiện trước thì được đặt vào ô trước đó. Thứ tự tương đối của các element bằng nhau trong output khớp với thứ tự trong input.
Nếu duyệt left-to-right: element đầu tiên sẽ chiếm ô cuối cùng (prefix sum ban đầu trỏ vào vị trí cuối của group), element sau sẽ đặt trước đó — đảo ngược thứ tự gốc. Kết quả vẫn sorted nhưng không stable. Stability của Counting sort quan trọng vì Radix LSD dựa vào nó: nếu Counting sort không stable, thứ tự từ digit trước sẽ bị phá khi sort digit sau.
Q3Radix LSD vs Radix MSD — khi nào pick cái nào?▸
Radix LSD — ưu tiên khi:
- Key có fixed width: 32-bit int, 64-bit long, fixed-length string. Mọi key cùng độ dài → LSD xử lý tự nhiên không cần padding.
- Muốn implementation đơn giản: d pass Counting sort độc lập, không đệ quy.
- Stable sort là yêu cầu: LSD với stable sub-sort luôn stable.
Radix MSD — ưu tiên khi:
- Key có variable length: string độ dài khác nhau. MSD xử lý tự nhiên vì stop khi prefix phân biệt được.
- Cần early termination: nếu chỉ cần top-k hoặc check prefix match, MSD dừng ngay khi đủ thông tin.
- Implement trie-based sort (Burstsort): MSD tự nhiên map sang trie traversal.
Trong production Java với int[]: dùng LSD base 256 (4 pass). Với String[] variable-length: cân nhắc MSD hoặc dùng Arrays.sort() (TimSort) nếu không có bottleneck.
Q4Bucket sort worst case O(n squared) — vì sao?▸
Bucket sort chia range thành k bucket đều nhau và sort từng bucket bằng comparison sort (thường Insertion sort hoặc Collections.sort).
Nếu data không uniform — ví dụ toàn bộ n element rơi vào cùng 1 bucket — bucket đó có n element và cần sort bằng comparison sort O(n log n). Với Insertion sort làm inner sort: O(n squared) cho bucket đó. Các bucket còn lại rỗng, tổng = O(n squared).
Đây là degenerate case khi distribution giả định sai: Bucket sort assume uniform distribution để expected O(n/k) per bucket. Khi assumption sai, không còn guarantee nào — worst case tệ hơn cả comparison sort thuần vì còn tốn thêm O(k) space để tạo bucket. Đo distribution trước khi dùng Bucket sort, hoặc dùng adaptive bucket boundary theo actual quantile.
Q5Cho bài toán: 1 triệu địa chỉ IP (32-bit integer) cần sort. Pick algorithm nào? Vì sao?▸
Radix sort LSD, base 256 (4 pass).
Lý do: địa chỉ IP là 32-bit integer — key có fixed width và Radix sort đây là use case lý tưởng. Với base 256 (1 byte = 1 digit), chỉ cần 4 pass Counting sort, mỗi pass O(n + 256). Tổng: O(4n) = O(n). Với n = 1 triệu, đây nhanh hơn Counting sort trực tiếp (k = 2^32 = 4 tỉ → OOM) và nhanh hơn comparison sort O(n log n).
Counting sort trực tiếp trên 32-bit key không khả thi: cần array kích thước 4 tỉ (16GB cho int[]) → OOM. Bucket sort kém hơn vì IP distribution thường không uniform (nhiều subnet cluster lại). Quick sort hoặc Merge sort O(n log n) = O(20 triệu ops) — chậm hơn Radix O(4 triệu ops). Kết luận: Radix LSD base 256 là lựa chọn tối ưu.
Q6Arrays.sort(int[]) Java không dùng Radix sort dù Radix nhanh hơn với large sorted-int array — lý do?▸
Có ba lý do chính. Thứ nhất, Arrays.sort(int[]) là general-purpose API — không nhận tham số k hay range từ caller. Radix sort cần biết max value để tính số digit (d = log_256 max). JDK không thể scan array để tìm max trước khi sort vì đó đã là O(n) và tốn thêm pass.
Thứ hai, Dual-Pivot Quicksort với Tukey ninther đã rất hiệu quả trên random integer data — benchmark cho thấy chênh lệch so với Radix sort không lớn sau khi tính overhead của 4 pass Radix (bộ nhớ cho count array, output buffer, memory bandwidth). Khoảng 2-3x theo fastutil benchmark trên specific hardware.
Thứ ba, Dual-Pivot Quick in-place O(log n) space trong khi Radix cần O(n + k) buffer. Trong môi trường memory-constrained hoặc với large array, buffer allocation tốn thời gian GC và tăng peak RSS. JDK chọn algorithm an toàn hơn cho general case. Nếu cần Radix speed với int[], dùng fastutil IntArrays.radixSort().
Bài tiếp theo: Skip list — probabilistic balanced, code đơn giản hơn RBTree
Bài này có giúp bạn hiểu bản chất không?