Sorting: chọn thuật toán bằng constraint
Phỏng vấn không chỉ cần nhớ Big-O. Cần nói được stability, memory, input distribution, worst case và Java API thực sự dùng gì.
jdk-21+35 làm phạm vi đối chiếu, không tuyên bố đúng nguyên trạng cho mọi JDK.Khung trả lời trước khi chọn
- Dữ liệu nằm trong memory hay external storage; kích thước và memory budget?
- Cần stable không; object có nhiều khóa hay không?
- Input gần sorted, có nhiều duplicate hay key thuộc miền nhỏ?
- Cần worst-case guarantee hay average case đủ?
- Có được mutate input; comparator có đúng contract?
Đổi mỗi constraint thành một điều cần kiểm chứng
Trước khi chọn tên thuật toán, viết một câu mô tả workload: “Tôi có n records, khóa so sánh là …, cần giữ thứ tự khi bằng khóa hay không, được phép sửa input hay không, có … bộ nhớ phụ và cần … giới hạn worst case.” Đây là mẫu trả lời, không phải giả định mặc định cho mọi bài toán.
Phân biệt đúng thứ tự, giữ đủ dữ liệu và đáp ứng tài nguyên. Một output nhìn đã sorted vẫn sai nếu làm mất duplicate; một thuật toán đúng vẫn không phù hợp nếu vượt RAM hoặc làm tăng latency của request khác. Các bài practice bên dưới yêu cầu evidence cho từng phần.
Bảng so sánh cốt lõi
| Algorithm | Best / Average / Worst | Memory | Stable | Khi đáng dùng |
|---|---|---|---|---|
| Bubble sort | O(n) có early stop / O(n²) / O(n²) | O(1) | Có | Chủ yếu để giải thích; hiếm khi dùng production. |
| Selection sort | O(n²) / O(n²) / O(n²) | O(1) | Thường không | Số lần swap ít, nhưng comparison vẫn nhiều. |
| Insertion sort | O(n) / O(n²) / O(n²) | O(1) | Có | Input nhỏ hoặc gần sorted; thường là base case của hybrid sort. |
| Merge sort | O(n log n) mọi case | O(n) với array | Có | Cần stable và worst-case chắc chắn; hợp external sorting. |
| Quick sort | O(n log n) average, O(n²) worst | O(log n) stack average | Không | Array primitive/in-memory; locality tốt, cần pivot/partition tốt. |
| Heap sort | O(n log n) mọi case | O(1) | Không | Cần worst-case và in-place, chấp nhận locality kém hơn quicksort. |
| Counting sort | O(n + k) | O(k) | Có thể | Integer key có miền k nhỏ; không phải comparison sort. |
| Radix sort | O(d(n + k)) | Phụ thuộc implementation | Có nếu mỗi pass stable | Key số/string có số digit hữu hạn và phân phối phù hợp. |
Không đọc Big-O tách khỏi implementation
Quy ước: n là số phần tử; k trong counting/radix là kích thước miền key hoặc radix; d là số pass/digit. k trong bài top-K ở dưới là số kết quả cần giữ, không phải miền key.
- Quick sort: O(log n) stack là trường hợp trung bình/cân bằng. Bản đệ quy hai nhánh có thể cần O(n) stack khi partition lệch liên tục. “In-place” trong cách gọi thông dụng không xóa chi phí stack. [R9]
- Counting sort: O(k) trong bảng là bộ đếm. Bản stable phân phối records sang output array còn cần O(n), nên tổng bộ nhớ phụ thường là O(n + k). Không được suy ra “stable counting sort cho records chỉ cần O(k)” từ dòng tóm tắt. [R10]
- Radix sort: điều kiện mỗi pass stable đặc biệt quan trọng với LSD. Cần định nghĩa cách xử lý dấu, độ dài key và alphabet; cách sort byte của số âm không thể bỏ qua bit dấu. [R10]
- Merge/heap: ký hiệu O(n log n) trong bảng là upper bound của các phiên bản được xét, không phải lời khẳng định Θ(n log n) cho mọi input và mọi biến thể. Merge sort nguồn vẫn thực hiện merge ở mỗi mức; biến thể adaptive có thể bỏ việc thừa. [R8]
Suy luận về lower bound: trường hợp tổng quát với n key phân biệt phải phân biệt n! hoán vị; decision tree nhị phân cần độ sâu ít nhất log₂(n!). Vì vậy không mâu thuẫn khi một input đã sorted được xử lý tuyến tính, hoặc khi counting/radix khai thác cấu trúc key thay vì chỉ hỏi “nhỏ hơn hay lớn hơn”.
Ba thuộc tính hay bị hỏi
Stable nghĩa là hai phần tử bằng nhau theo sort key giữ nguyên relative order. Nó quan trọng khi sort nhiều lần: ví dụ sort theo tên rồi stable-sort theo phòng ban.
In-place thường nghĩa auxiliary memory O(1), nhưng recursion stack vẫn phải tính khi phân tích. Adaptive nghĩa tận dụng input đã gần sorted, như insertion sort hoặc TimSort.
Internal sort giả định data nằm trong memory. External sort chia data thành sorted runs rồi k-way merge vì toàn bộ data không vừa RAM.
Equal theo comparator, không phải theo identity
Ví dụ tự xây dựng: mỗi record có (key, id), nhưng comparator chỉ đọc key. Sau stable sort, A vẫn đứng trước B:
Input: [(2, A), (1, X), (2, B)]
Stable sort: [(1, X), (2, A), (2, B)]
Với multi-pass sorting, sort khóa phụ trước rồi stable-sort khóa chính: ví dụ theo tên trước, theo phòng ban sau như nguồn. Nếu thêm ID vào comparator, các record cùng business key nhưng khác ID không còn “equal” đối với toàn comparator. Stability và deterministic tie-breaker giải quyết hai yêu cầu khác nhau. [R3]
Đề xuất contract: ghi rõ “sort tại chỗ” hay “trả bản sao đã sort”. Trả bản sao không đồng nghĩa bản sao đó immutable. Trong runner, sortedCopy bảo toàn input nhưng kết quả vẫn là một int[] có thể sửa; test kiểm đúng lời hứa này.
Merge sort: stable và predictable
static void mergeSort(int[] a, int left, int right, int[] buffer) {
if (left >= right) return;
int mid = left + (right - left) / 2;
mergeSort(a, left, mid, buffer);
mergeSort(a, mid + 1, right, buffer);
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right) {
buffer[k++] = a[i] <= a[j] ? a[i++] : a[j++];
}
while (i <= mid) buffer[k++] = a[i++];
while (j <= right) buffer[k++] = a[j++];
System.arraycopy(buffer, left, a, left, right - left + 1);
}
Dùng <= để lấy phần tử bên trái trước khi bằng nhau, nhờ đó giữ stability. Cấp buffer một lần bên ngoài để tránh allocation ở mỗi recursion.
Invariant, buffer và cách gọi đúng
Trước vòng merge, hai đoạn [left, mid] và [mid + 1, right] đã sorted. Mỗi bước lấy phần tử nhỏ hơn ở đầu hai đoạn; khi bằng nhau lấy bên trái trước. Quy tắc này bảo toàn thứ tự của các phần tử bằng khóa qua mỗi lần merge.
Hàm nội bộ giả định array và buffer khác nhau, buffer đủ dài và chỉ số hợp lệ. Cách gọi toàn mảng dưới đây cấp một buffer duy nhất; với mảng rỗng, right = -1 và base case trả về ngay.
int[] data = {3, -1, 3, Integer.MIN_VALUE};
int[] buffer = new int[data.length];
mergeSort(data, 0, data.length - 1, buffer);
Suy ra từ implementation này: thời gian O(n log n), buffer O(n), stack O(log n); tổng auxiliary space vẫn O(n). Không cấp buffer mới cho từng recursive call. Nếu tự chuyển sang records, test stability bằng ID/ordinal; hai số nguyên cùng giá trị không đủ để quan sát identity bị đảo. Nền tảng divide–sort–merge được đối chiếu tại [R8].
Quick sort: partition và worst case
static int partition(int[] a, int low, int high) {
int pivot = a[high];
int boundary = low;
for (int i = low; i < high; i++) {
if (a[i] < pivot) swap(a, i, boundary++);
}
swap(a, boundary, high);
return boundary;
}
Luôn chọn phần tử cuối làm pivot có thể tạo O(n²) trên input đã sorted. Randomized pivot, median-of-three và 3-way partition giúp giảm input xấu/nhiều duplicate; introsort chuyển sang heapsort khi recursion quá sâu để chặn worst case.
Lomuto partition: phần nào đã đúng?
Trong vòng lặp, [low, boundary) chứa giá trị nhỏ hơn pivot; [boundary, i) chứa giá trị lớn hơn hoặc bằng pivot; phần từ i đến trước high chưa xét. Cuối vòng lặp, swap đưa pivot vào vị trí boundary. Ví dụ [3, 1, 3, 2] cho [1, 2, 3, 3] và pivot index bằng 1.
< ở đây, input sorted, reverse hoặc all-equal đều có thể khiến mỗi lần chỉ loại được một phần tử. Số key comparisons là (n−1) + … + 1 = n(n−1)/2; stack của wrapper đệ quy có thể tăng tuyến tính và gây StackOverflowError. Không chạy bản học tập này trên array lớn rồi xem đó là benchmark của Java library.Phạm vi biện pháp: random pivot cải thiện expected behavior, median-of-three là heuristic, không phải hard worst-case guarantee; với nhiều duplicate, 3-way partition tách riêng vùng bằng pivot. Introsort dùng fallback để chặn degeneration. Đừng gán worst case của đoạn Lomuto trên cho mọi hybrid sort. [R9] [R5]
Source chỉ cung cấp partition, chưa phải một quick sort hoàn chỉnh. Runner ở phần Practice bổ sung swap, wrapper và hai lời gọi đệ quy trên [low, p−1], [p+1, high]; không sort lại pivot.
Java thực tế dùng gì?
Arrays.sort(int[])và các primitive arrays dùng Dual-Pivot Quicksort theo Java API; không stable.Arrays.sort(Object[])là stable, adaptive, iterative mergesort được điều chỉnh từ TimSort.List.sortyêu cầu stable sort;Collections.sortchuyển choList.sort.Arrays.parallelSortchia/merge song song; chỉ đáng dùng sau benchmark với data đủ lớn và CPU phù hợp.
API contract khác implementation note
Phần tóm tắt BK được giữ ở trên; các caveat sau hiệu chỉnh cách hiểu “primitive arrays” và “parallelSort”. Java API cho phép thay thuật toán miễn là giữ contract: stability của object sort là guarantee, còn tên thuật toán không phải lời hứa bất biến giữa các bản JDK. [R1]
| Trường hợp | Điều nên ghi nhớ sau khi đối chiếu |
|---|---|
Arrays.sort với primitive | Không dựa vào stability guarantee cho records. Trong snapshot OpenJDK đã kiểm tra, byte[] dùng counting/insertion; short[], char[] có nhánh counting. Nhánh int[] là hybrid: insertion, xử lý runs, partition và heap fallback. Không phải mọi lời gọi chỉ chạy một thuật toán thuần. [R5] |
Arrays.sort(Object[]) | Stable là contract. Adaptive iterative mergesort/TimSort là mô tả implementation; không suy ra O(1) auxiliary memory. [R1] |
List.sort / Collections.sort | List cần modifiable, không bắt buộc resizable. Default List.sort tạo array, sort rồi ghi lại; implementation cụ thể có thể override. Collections.sort delegate sang List.sort. [R2] [R4] |
Arrays.parallelSort | Object overload được mô tả bằng parallel sort-merge và vẫn stable. Trong snapshot đã kiểm tra, các overload byte[]/char[]/short[] đi vào sorter tuần tự; int[] truyền common-pool parallelism vào hybrid sorter. Tên API không chứng minh mọi input sẽ được chạy đa luồng. [R1] [R6] |
Đề xuất đo lường: benchmark chính overload, JDK build, data type, distribution và mức tải CPU sẽ dùng thật. Ghi cả memory/allocation và tác động lên workload khác; đừng dùng kết quả chạy riêng để cam kết latency khi common pool đang bận. Quy trình đo nằm ở Practice 3.
IllegalArgumentException. Tránh subtraction như a.age - b.age vì overflow; dùng Integer.compare.Comparator<Employee> byTeamThenSalary =
Comparator.comparing(Employee::team)
.thenComparing(Employee::salary, Comparator.reverseOrder())
.thenComparing(Employee::id); // deterministic tie-breaker
employees.sort(byTeamThenSalary);
Không có exception chưa có nghĩa comparator đúng
Contract gồm đối xứng dấu khi đảo đối số, transitivity, và tính nhất quán với phần tử thứ ba khi hai phần tử compare bằng 0. Không bắt buộc compare-bằng-0 luôn đồng nghĩa equals. Việc sort phát hiện vi phạm và ném IllegalArgumentException là tùy chọn, không phải validator đầy đủ. [R3] [R2]
Ví dụ tự xây dựng: MAX_VALUE > 0 và 0 > −1, nhưng phép trừ MAX_VALUE − (−1) tràn thành số âm. Runner kiểm witness này trực tiếp, rồi kiểm các luật trên comparator dùng Integer.compare; không chờ library phải ném lỗi. Quy tắc overflow tham chiếu tại [R12].
Giả định của code Employee: team, salary, id không null; salary là BigDecimal; ID duy nhất trong dataset. Thứ tự là team tăng, salary giảm, ID tăng. Với dữ liệu thật có null, định nghĩa chính sách null tường minh trước khi sort. [R3]
Suy luận thiết kế: unique tie-breaker tạo thứ tự xác định trên cùng một snapshot. Nó không tự ngăn việc chèn/xóa records giữa hai lần gọi API gây lệch pagination; cần mô tả cả snapshot/cursor policy và không đổi sort keys trong lúc so sánh.
Câu hỏi phỏng vấn tình huống
1. Quick sort và merge sort khác nhau thế nào?
Mental model: quick sort đặt pivot rồi xử lý các partition; merge sort xử lý hai nửa rồi hợp nhất. So sánh cùng data type và cùng comparator, không so int[] với boxed objects rồi quy mọi chênh lệch cho thuật toán.
Evidence đề xuất: chạy cùng input random, nearly sorted, reverse, all-equal; báo comparisons, stack depth, allocation và stability bằng tagged records. “Quick sort in-place” vẫn phải nói rõ có tính recursion stack không. [R8] [R9]
2. Vì sao insertion sort vẫn xuất hiện trong library?
Trade-off: một thuật toán O(n²) vẫn có ích khi n của subproblem rất nhỏ; điều đó không biến nó thành lựa chọn tốt cho mảng lớn tùy ý. Hybrid cutoff là implementation detail, không phải một hằng số nên học thuộc cho mọi JDK. [R5]
Evidence đề xuất: thay đổi kích thước và mức độ gần sorted, đếm cả comparisons lẫn moves. Đảm bảo lần chạy sau không vô tình nhận array đã sorted từ lần trước.
3. Sort 500 GB log với RAM 8 GB thế nào?
Kế hoạch triển khai đề xuất: chừa RAM cho parser, object overhead và I/O buffers; không lấy toàn bộ 8 GB làm chunk size. Nếu mỗi run chứa C byte dữ liệu tương đương đầu vào thì số run r xấp xỉ ⌈500 GB/C⌉. Chọn fan-in f từ cả giới hạn file handles và ngân sách buffer; nếu r > f phải merge nhiều vòng.
Evidence: lưu manifest run, số records, checksum từng file, key đầu/cuối và checkpoint; thử dừng/restart giữa quá trình tạo run hoặc merge. Kiểm output không giảm và không mất/nhân bản records trên dataset kiểm thử có oracle. Checksum file xác nhận integrity, không thay thế kiểm chứng thứ tự hay business key.
Suy luận chi phí: với f ≥ 2, số vòng merge xấp xỉ ⌈logfr⌉; mỗi vòng đọc và ghi toàn bộ dữ liệu. I/O, temporary disk và cleanup cần được budget, không chỉ CPU. Đây là mô hình ước lượng, không phải benchmark 500 GB đã được chạy.
4. Tìm top 100 trong 100 triệu records có cần sort tất cả?
Phạm vi: giả định cần 100 giá trị lớn nhất; với 100 nhỏ nhất thì đảo heap/comparator. Heap giữ tối đa k phần tử; muốn trả kết quả sorted còn cần xử lý k phần tử cuối. PriorityQueue iterator không đảm bảo sorted order; runner rút head để dựng kết quả. [R11]
Trade-off và evidence: quickselect hợp khi có random access và chấp nhận partition/mutate; nó không tự sắp thứ tự toàn bộ top-K. Với heap, đọc stream một lượt, so kết quả với full sort trên sample kiểm được; nêu rõ chính sách duplicate và tie-breaker cho records.
5. Khi nào counting sort tốt hơn quicksort?
Failure case: chỉ ít key khác nhau nhưng min–max rất xa vẫn có thể tạo count array quá lớn. Tính range bằng kiểu đủ rộng, ví dụ long range = (long) max - (long) min + 1;, rồi kiểm giới hạn allocation và memory budget trước khi cấp mảng.
Evidence: dùng cả miền nhỏ, miền thưa và giá trị âm; so đúng multiset và đo cả bước khởi tạo bộ đếm. Nếu mang payload và cần stable, tính thêm output buffer O(n) như phần caveat của bảng. [R10]
6. Sort ổn định có ích trong business code ra sao?
Ví dụ áp dụng: UI muốn những records cùng mức ưu tiên tiếp tục theo thứ tự hiện có; stable sort giữ thứ tự đó trong một lần xử lý. Nếu order đầu vào thay đổi giữa request thì output của các tie cũng có thể đổi.
Evidence đề xuất: gắn ID/ordinal, kiểm thứ tự các record cùng key trước/sau; sau đó shuffle input rồi thêm unique tie-breaker và kiểm output trên cùng dataset. Với pagination, test cả insert/delete giữa các trang thay vì xem stability là lời bảo đảm snapshot.
Practice
- Tự viết insertion, merge và quick sort; test empty, one item, sorted, reverse, duplicate và extreme integer.
- Property test: output không giảm, có cùng multiset với input và không mutate khi contract nói immutable.
- Đếm comparisons/swaps thay vì chỉ đo milliseconds; chạy random, nearly sorted, reverse và all-equal.
- Tạo comparator sai transitivity rồi giải thích failure; sửa bằng comparator composition.
- Giải bài top-K bằng heap và so complexity/evidence với full sort.
Năm mục trên giữ nguyên mục tiêu nguồn. Các acceptance gate dưới đây làm rõ cách chứng minh đã hoàn thành; không tăng số lượng bài practice.
Practice 1 · Implement và phá giả định về input
Việc làm: tự viết insertion, merge, quick sort rồi so với ba phiên bản trong runner. Giữ các dataset empty, one item, sorted, reverse, duplicate và extreme integer; bổ sung all-equal và nearly sorted.
Failure injection: đổi điều kiện merge từ <= thành < trong phiên bản tagged-record để quan sát mất stability; cho last-pivot quick sort chạy input lệch với n tăng dần trong môi trường có giới hạn tài nguyên.
Acceptance / nộp: code, kết quả test theo từng thuật toán, trace một merge và một partition. Giải thích tại sao kiểm mảng số nguyên thôi chưa chứng minh stability; không yêu cầu cố làm process cạn stack để được tính hoàn thành.
Practice 2 · Properties, multiset và mutation
Việc làm: kiểm đồng thời output không giảm, multiset không đổi, đúng oracle và input không mutate khi contract hứa điều đó. Dùng seed ghi lại được, sinh cả miền ít giá trị và toàn miền int.
Failure injection: bỏ một phần tử trùng, đổi một số thành số khác nhưng vẫn giữ sorted order, hoặc trả thẳng input trong API đáng ra trả bản sao. Từng lỗi phải bị ít nhất một assertion bắt được.
Acceptance / nộp: generator, seed, counterexample thu nhỏ và log failure trước/sau sửa. Runner là seeded property-style smoke test, không phải exhaustive proof hay framework property testing có shrinking tự động.
Practice 3 · Đo công việc trước khi đo thời gian
Việc làm: thêm counter kiểu long cho key comparisons, swaps/moves và max recursion depth. Quy ước swap với chính nó có tính hay không; merge cần đếm moves/copies chứ không chỉ swaps. Chạy random, nearly sorted, reverse và all-equal trên nhiều n.
Failure injection: cố ý dùng lại array đã sorted ở lần đo thứ hai, sau đó sửa bằng input mới/bản sao đồng nhất cho từng invocation. Báo cả hai kết quả để chỉ ra benchmark đã đổi workload.
Timing bổ sung: dùng JMH khi cần so hiệu năng JVM; có warmup, nhiều lần đo/fork, seed và cấu hình JDK/CPU/heap/GC. Tách rõ đo “copy + sort” với “sort trên dữ liệu đã chuẩn bị”; setup/copy ngoài timer vẫn có thể ảnh hưởng cache/GC. Không dùng invocation-level setup một cách máy móc. [R7] [R13]
Acceptance / nộp: CSV counters, cấu hình đo, phạm vi timer và kết luận theo workload; không ghi một thuật toán “nhanh nhất” chung chung. Với code Lomuto nguồn, giải thích đường tăng comparisons trên input sorted/all-equal bằng tổng n(n−1)/2.
Practice 4 · Comparator sai rồi sửa đúng
Việc làm: tạo comparator subtraction để có witness overflow; kiểm trực tiếp sign symmetry, transitivity và zero-consistency. Sửa bằng Integer.compare/comparator composition; thêm chính sách null và tie-breaker khi domain yêu cầu.
Failure injection: dùng ba giá trị MAX_VALUE, 0, −1. Không viết test đòi sort bắt buộc ném exception. [R12] [R2]
Acceptance / nộp: comparator trước/sau, counterexample, test luật so sánh và output Employee đúng thứ tự team/salary/id. Chỉ ra luật nào bị phá, không chỉ nói “TimSort bị lỗi”.
Practice 5 · Top-K bằng bounded heap
Việc làm: đọc stream, giữ min-heap tối đa k cho largest-K; so kết quả với full sort trên cùng dữ liệu. Runner giữ cả multiplicity của số bằng nhau, trả descending order, k = 0 trả rỗng không đọc stream, k > n trả n phần tử; k âm bị từ chối.
Failure injection: dùng nhầm max-heap, trả iterator như thể đã sorted, hoặc bỏ duplicate ngoài ý muốn. Với records có cùng score, thêm ID nếu hợp đồng cần chọn đúng một thứ tự xác định. [R11]
Acceptance / nộp: code, test các biên k, đối chiếu full-sort oracle và peak heap size. Với k ≥ 2, pha chọn có upper bound O(n log k), bộ nhớ giữ O(k); dựng output sorted thêm O(k log k). Không dùng full array đầu vào để tuyên bố giải pháp streaming chỉ tốn O(k) end-to-end.
Runner tự chứa · SortingLab.java
Copy nguyên khối sau vào SortingLab.java, chạy bằng JDK 21. Chỉ dùng Java standard library. Ba sample nguồn được giữ ở trên; runner bổ sung class, imports, dữ liệu Employee, swap, wrappers và assertions để các đoạn đó chạy được.
import java.math.BigDecimal;
import java.util.*;
import java.util.function.Consumer;
public final class SortingLab {
private static int sortChecks;
private static int copyChecks;
private static int topKChecks;
static void mergeSort(int[] a, int left, int right, int[] buffer) {
if (left >= right) return;
int mid = left + (right - left) / 2;
mergeSort(a, left, mid, buffer);
mergeSort(a, mid + 1, right, buffer);
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right) {
buffer[k++] = a[i] <= a[j] ? a[i++] : a[j++];
}
while (i <= mid) buffer[k++] = a[i++];
while (j <= right) buffer[k++] = a[j++];
System.arraycopy(buffer, left, a, left, right - left + 1);
}
static int partition(int[] a, int low, int high) {
int pivot = a[high];
int boundary = low;
for (int i = low; i < high; i++) {
if (a[i] < pivot) swap(a, i, boundary++);
}
swap(a, boundary, high);
return boundary;
}
static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
static void merge(int[] a) {
Objects.requireNonNull(a, "a");
mergeSort(a, 0, a.length - 1, new int[a.length]);
}
// Teaching version: last pivot, recursive Lomuto; not a production default.
static void quick(int[] a) {
Objects.requireNonNull(a, "a");
quickSort(a, 0, a.length - 1);
}
static void quickSort(int[] a, int low, int high) {
if (low >= high) return;
int p = partition(a, low, high);
quickSort(a, low, p - 1);
quickSort(a, p + 1, high);
}
static void insertionSort(int[] a) {
Objects.requireNonNull(a, "a");
for (int i = 1; i < a.length; i++) {
int value = a[i], j = i - 1;
while (j >= 0 && a[j] > value) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = value;
}
}
// Non-mutating API: a new array, not an immutable returned array.
static int[] sortedCopy(int[] input) {
Objects.requireNonNull(input, "input");
int[] copy = input.clone();
merge(copy);
return copy;
}
// Largest k VALUES, with multiplicities, descending; k == 0 consumes nothing.
static int[] largestK(PrimitiveIterator.OfInt input, int k) {
Objects.requireNonNull(input, "input");
if (k < 0) throw new IllegalArgumentException("k must be nonnegative");
if (k == 0) return new int[0];
PriorityQueue<Integer> heap = new PriorityQueue<>();
while (input.hasNext()) {
int value = input.nextInt();
if (heap.size() < k) {
heap.offer(value);
} else if (value > heap.peek()) {
heap.poll();
heap.offer(value);
}
}
int[] result = new int[heap.size()];
for (int i = result.length - 1; i >= 0; i--) {
result[i] = heap.remove();
}
return result;
}
static Map<Integer, Integer> multiset(int[] a) {
Map<Integer, Integer> counts = new HashMap<>();
for (int value : a) counts.merge(value, 1, Integer::sum);
return counts;
}
static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
static void verify(int[] input) {
int[] expected = input.clone();
Arrays.sort(expected);
List<Consumer<int[]>> sorts = List.of(
SortingLab::insertionSort, SortingLab::merge, SortingLab::quick);
for (Consumer<int[]> sort : sorts) {
int[] actual = input.clone();
sort.accept(actual);
check(Arrays.equals(actual, expected), "oracle mismatch");
for (int i = 1; i < actual.length; i++) {
check(actual[i - 1] <= actual[i], "not nondecreasing");
}
check(multiset(actual).equals(multiset(input)), "multiset changed");
sortChecks++;
}
int[] before = input.clone();
int[] copy = sortedCopy(input);
check(Arrays.equals(before, input), "input mutated");
check(copy != input && Arrays.equals(copy, expected), "copy contract");
copyChecks++;
for (int k : new int[] {0, 1, 3, 100}) {
int[] actual = largestK(Arrays.stream(input).iterator(), k);
int[] wanted = new int[Math.min(k, input.length)];
for (int i = 0; i < wanted.length; i++) {
wanted[i] = expected[expected.length - 1 - i];
}
check(Arrays.equals(actual, wanted), "top-k mismatch");
topKChecks++;
}
}
record Item(int key, String id) {}
record Employee(String team, BigDecimal salary, String id) {}
static void verifyBusinessSorting() {
List<Item> items = new ArrayList<>(List.of(
new Item(2, "A"), new Item(1, "X"), new Item(2, "B")));
items.sort(Comparator.comparingInt(Item::key));
check(items.stream().map(Item::id).toList().equals(
List.of("X", "A", "B")), "stable order lost");
List<Employee> employees = new ArrayList<>(List.of(
new Employee("A", new BigDecimal("20"), "E2"),
new Employee("B", new BigDecimal("50"), "E3"),
new Employee("A", new BigDecimal("20"), "E1"),
new Employee("A", new BigDecimal("30"), "E4")));
Comparator<Employee> byTeamThenSalary =
Comparator.comparing(Employee::team)
.thenComparing(Employee::salary, Comparator.reverseOrder())
.thenComparing(Employee::id); // deterministic tie-breaker
employees.sort(byTeamThenSalary);
check(employees.stream().map(Employee::id).toList().equals(
List.of("E4", "E1", "E2", "E3")), "tie-breaker mismatch");
String[] backing = {"b", "a"};
Arrays.asList(backing).sort(Comparator.naturalOrder());
check(backing[0].equals("a"), "backing array not changed");
boolean rejected = false;
try {
List.of(2, 1).sort(Integer::compare);
} catch (UnsupportedOperationException expected) {
rejected = true;
}
check(rejected, "unmodifiable list accepted sort");
}
static void verifyComparator() {
Comparator<Integer> bad = (a, b) -> a - b;
int x = Integer.MAX_VALUE, y = 0, z = -1;
check(bad.compare(x, y) > 0 && bad.compare(y, z) > 0
&& bad.compare(x, z) < 0, "expected transitivity witness");
// A sorting method need not detect every invalid comparator.
Comparator<Integer> good = Integer::compare;
int[] values = {Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE};
for (int a : values) for (int b : values) for (int c : values) {
int ab = good.compare(a, b), bc = good.compare(b, c);
check(Integer.signum(ab) == -Integer.signum(good.compare(b, a)),
"sign symmetry");
if (ab > 0 && bc > 0) check(good.compare(a, c) > 0, "transitivity");
if (ab == 0) check(Integer.signum(good.compare(a, c))
== Integer.signum(good.compare(b, c)), "zero consistency");
}
}
public static void main(String[] args) {
int[][] edges = {
{}, {7}, {1, 2, 3, 4}, {4, 3, 2, 1}, {2, 2, 2, 2},
{3, 1, 3, -1, 1}, {Integer.MAX_VALUE, 0, Integer.MIN_VALUE, -1},
{1, 2, 4, 3, 5}
};
for (int[] edge : edges) verify(edge);
Random random = new Random(1729);
for (int trial = 0; trial < 300; trial++) {
int[] input = new int[random.nextInt(129)];
for (int i = 0; i < input.length; i++) {
input[i] = trial % 2 == 0 ? random.nextInt(9) - 4 : random.nextInt();
}
verify(input);
}
verifyBusinessSorting();
verifyComparator();
boolean rejected = false;
try {
largestK(Arrays.stream(new int[] {1}).iterator(), -1);
} catch (IllegalArgumentException expected) {
rejected = true;
}
check(rejected, "negative k accepted");
System.out.printf("PASS sorts=%d copy=%d topK=%d%n",
sortChecks, copyChecks, topKChecks);
System.out.println("PASS stability comparator mutability negative-k");
}
}
javac -encoding UTF-8 SortingLab.java
java -ea SortingLab
PASS sorts=924 copy=308 topK=1232
PASS stability comparator mutability negative-k
Gate: không kết luận thuật toán “nhanh nhất” nếu chưa nêu input, data type, stability, memory và worst-case constraint.
Tài liệu và phạm vi xác minh
- [R1] Java 21 Arrays — API và implementation notes
- [R2] Java 21 List.sort — stability, mutability, default implementation
- [R3] Java 21 Comparator — contract và composition
- [R4] Java 21 Collections.sort — delegation
- [R5] OpenJDK jdk-21+35 — DualPivotQuicksort.java
- [R6] OpenJDK jdk-21+35 — Arrays.java
- [R7] OpenJDK JMH — hướng dẫn và samples
- [R8] Princeton Algorithms — Mergesort
- [R9] Princeton Algorithms — Quicksort
- [R10] Princeton Algorithms — LSD.java, counting passes và signed integers
- [R11] Java 21 PriorityQueue — head, traversal và complexity
- [R12] JLS 21 §15.18.2 — integer addition/subtraction và overflow
- [R13] OpenJDK JMH sample 38 — pitfalls của per-invocation setup