Java Core / Sorting algorithms
Part 01 · Java Core · 1.1.01 · Interview deep dive

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ì.

08 thuật toán06 tình huống05 practiceJava 21 baseline
Cách đọc: nội dung nền và ví dụ BK được bảo toàn. Các khối Bổ sung trình bày suy luận, ví dụ hoặc bài kiểm thử tự xây dựng; các khối Research nêu API contract và implementation có dẫn nguồn. Chi tiết Java lấy Java SE 21 và snapshot OpenJDK 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

  1. Dữ liệu nằm trong memory hay external storage; kích thước và memory budget?
  2. Cần stable không; object có nhiều khóa hay không?
  3. Input gần sorted, có nhiều duplicate hay key thuộc miền nhỏ?
  4. Cần worst-case guarantee hay average case đủ?
  5. Có được mutate input; comparator có đúng contract?
Bổ sung · Khung ra quyết định

Đổ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đá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

AlgorithmBest / Average / WorstMemoryStableKhi đáng dùng
Bubble sortO(n) có early stop / O(n²) / O(n²)O(1)Chủ yếu để giải thích; hiếm khi dùng production.
Selection sortO(n²) / O(n²) / O(n²)O(1)Thường khôngSố lần swap ít, nhưng comparison vẫn nhiều.
Insertion sortO(n) / O(n²) / O(n²)O(1)Input nhỏ hoặc gần sorted; thường là base case của hybrid sort.
Merge sortO(n log n) mọi caseO(n) với arrayCần stable và worst-case chắc chắn; hợp external sorting.
Quick sortO(n log n) average, O(n²) worstO(log n) stack averageKhôngArray primitive/in-memory; locality tốt, cần pivot/partition tốt.
Heap sortO(n log n) mọi caseO(1)KhôngCần worst-case và in-place, chấp nhận locality kém hơn quicksort.
Counting sortO(n + k)O(k)Có thểInteger key có miền k nhỏ; không phải comparison sort.
Radix sortO(d(n + k))Phụ thuộc implementationCó nếu mỗi pass stableKey số/string có số digit hữu hạn và phân phối phù hợp.
Lower bound: comparison sort tổng quát cần Ω(n log n) comparisons trong worst case. Counting/radix vượt giới hạn này vì dùng cấu trúc của key, không chỉ comparison.
Bổ sung · Phạm vi của bảng nguồn

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.

Bổ sung · Ví dụ và ranh giới guarantee

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:

TRACE · Chỉ so sánh key
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.

Bổ sung · Phân tích từ code nguồn

Invariant, buffer và cách gọi đúng

Trước vòng merge, hai đoạn [left, mid][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.

JAVA · Cách gọi bổ sung
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.

Bổ sung · Phân tích từ code nguồn

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.

Failure case suy ra trực tiếp: với last-element pivot và phép thử < ở đâ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ì?

Tóm tắt nguồn BK: đọc bốn ý sau cùng bảng caveat ngay bên dưới; không khái quát tên thuật toán cho mọi overload.

Research · Java SE 21 / OpenJDK jdk-21+35

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 primitiveKhô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.sortList 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.parallelSortObject 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.

Comparator contract: comparator không transitive hoặc không nhất quán có thể cho kết quả sai hoặc ném 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);
Research · Contract và ví dụ kiểm thử

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 > 00 > −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?
Quick sort thường in-place, cache locality tốt và average O(n log n), nhưng worst O(n²), không stable. Merge sort O(n log n) chắc chắn và stable, đổi lại array cần O(n) buffer. Chọn theo stability, memory và worst-case requirement.
Bổ sung · Áp dụng và evidence

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?
Với partition nhỏ hoặc gần sorted, overhead thấp, cache-friendly và adaptive; hybrid algorithms thường chuyển sang insertion sort ở base case.
Bổ sung · Áp dụng và evidence

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?
Dùng external merge sort: đọc từng chunk vừa RAM, sort và ghi run ra disk, sau đó k-way merge bằng priority queue; giới hạn file handles, buffer I/O và giữ deterministic key.
Bổ sung · Áp dụng và evidence

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ả?
Không. Giữ min-heap kích thước 100 cho O(n log k), hoặc quickselect nếu cần kth boundary; full sort O(n log n) lãng phí.
Bổ sung · Áp dụng và evidence

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?
Khi key integer có miền k đủ nhỏ so với n và memory cho count array chấp nhận được. Nếu miền rất thưa hoặc cực lớn, O(k) memory/initialization khiến nó không phù hợp.
Bổ sung · Áp dụng và evidence

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?
Giữ relative order của records có cùng key, cho phép multi-pass sorting và tránh UI/list nhảy thứ tự không cần thiết. Dù vậy production API nên thêm unique tie-breaker để pagination deterministic.
Bổ sung · Áp dụng và evidence

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

  1. Tự viết insertion, merge và quick sort; test empty, one item, sorted, reverse, duplicate và extreme integer.
  2. Property test: output không giảm, có cùng multiset với input và không mutate khi contract nói immutable.
  3. Đếm comparisons/swaps thay vì chỉ đo milliseconds; chạy random, nearly sorted, reverse và all-equal.
  4. Tạo comparator sai transitivity rồi giải thích failure; sửa bằng comparator composition.
  5. Giải bài top-K bằng heap và so complexity/evidence với full sort.
Bổ sung · Cách thực hiện 5 mục practice nguồn

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.

JAVA 21 · SortingLab.java · Code học tập + smoke tests
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");
    }
}
TERMINAL · Chạy trong thư mục chứa file
javac -encoding UTF-8 SortingLab.java
java -ea SortingLab
EXPECTED · Kết quả smoke test
PASS sorts=924 copy=308 topK=1232
PASS stability comparator mutability negative-k

924 lượt sort = 308 datasets × 3 thuật toán; 308 datasets = 8 edge cases + 300 generated cases; 1.232 lượt top-K = 308 × 4 giá trị k. Không có số liệu latency/throughput trong output này. Stability test trong runner kiểm List.sort; để kiểm một merge sort cho records do bạn tự viết, cần bổ sung bản tagged-record riêng.

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

Nguồn: Java 21 Arrays · List.sort · CLRS, Introduction to Algorithms.

CLRS, Introduction to Algorithms là tài liệu được nêu trong nguồn; gói nguồn không ghi edition hoặc URL. Các tài liệu web bổ sung dưới đây được đối chiếu ngày 12/09/2026. Không sao chép benchmark speedup từ tài liệu thành cam kết hiệu năng.