Algorithm Search⌘K

Java system sort in Python

The sorting routine in the Java standard library, using a tuned quicksort for primitives and a stable mergesort variant for objects.

File
java_system_sort.py, 214 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
java_system_sort.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/java-system-sort/. The authors are listed in prov/YQKT8T.jsonl.

"""A model of the two sorts of the Java standard library.

Arrays of primitive values (here: int and float) go to a dual-pivot
quicksort with an insertion sort for short ranges and a heapsort when the
recursion is too deep. Other arrays go to a stable merge of natural runs in
the style of Timsort. The constants are those of the JDK 7 and 8 sources
where the model has the same step.
"""

from typing import Protocol, TypeVar


class Ordered(Protocol):
    def __lt__(self, other: "Ordered", /) -> bool: ...


T = TypeVar("T", bound=Ordered)

INSERTION_SORT_THRESHOLD = 47
MIN_MERGE = 32


def java_system_sort(a: list[T]) -> None:
    """Sort a in place, in non-decreasing order.

    Primitive values use the unstable dual-pivot quicksort, other items use
    the stable run-merging sort. Cost: O(n log n) comparisons in the worst
    case on both paths, and O(n) on a sorted array of objects.
    """
    n = len(a)
    if all(type(x) in (int, float) for x in a):  # @line L1
        _sort_primitive(a, 0, n - 1, 2 * n.bit_length() + 2)
    else:
        _sort_objects(a)  # @line L2


# The path for primitive values.


def _sort_primitive(a: list[T], lo: int, hi: int, depth: int) -> None:
    if hi - lo < INSERTION_SORT_THRESHOLD:  # @line L3
        _insertion_sort(a, lo, hi)
        return
    if depth == 0:  # @line L4
        _heapsort(a, lo, hi)
        return
    p, q = _choose_pivots(a, lo, hi)  # @line L5
    lt, gt = lo + 1, hi - 1  # @line L6: three-way partition around p <= q
    k = lt
    while k <= gt:
        if a[k] < p:
            a[k], a[lt] = a[lt], a[k]
            lt += 1
        elif q < a[k]:
            while q < a[gt] and k < gt:
                gt -= 1
            a[k], a[gt] = a[gt], a[k]
            gt -= 1
            if a[k] < p:
                a[k], a[lt] = a[lt], a[k]
                lt += 1
        k += 1
    lt -= 1  # @line L7: the pivots go to their places
    gt += 1
    a[lo], a[lt] = a[lt], a[lo]
    a[hi], a[gt] = a[gt], a[hi]
    _sort_primitive(a, lo, lt - 1, depth - 1)  # @line L8
    _sort_primitive(a, gt + 1, hi, depth - 1)
    if p < q:  # @line L9
        _sort_primitive(a, lt + 1, gt - 1, depth - 1)


def _choose_pivots(a: list[T], lo: int, hi: int) -> tuple[T, T]:
    """Sort five sample items, and move the second and fourth to a[lo] and a[hi]."""
    seventh = (hi - lo + 1) // 7
    e3 = (lo + hi) // 2
    idx = [e3 - 2 * seventh, e3 - seventh, e3, e3 + seventh, e3 + 2 * seventh]
    for s in range(1, 5):
        x, t = a[idx[s]], s
        while t > 0 and x < a[idx[t - 1]]:
            a[idx[t]] = a[idx[t - 1]]
            t -= 1
        a[idx[t]] = x
    a[lo], a[idx[1]] = a[idx[1]], a[lo]
    a[hi], a[idx[3]] = a[idx[3]], a[hi]
    return a[lo], a[hi]


def _insertion_sort(a: list[T], lo: int, hi: int) -> None:
    for i in range(lo + 1, hi + 1):
        x, j = a[i], i
        while j > lo and x < a[j - 1]:
            a[j] = a[j - 1]
            j -= 1
        a[j] = x


def _heapsort(a: list[T], lo: int, hi: int) -> None:
    m = hi - lo + 1
    for s in range(m // 2 - 1, -1, -1):
        _sift_down(a, lo, s, m)
    for end in range(m - 1, 0, -1):
        a[lo], a[lo + end] = a[lo + end], a[lo]
        _sift_down(a, lo, 0, end)


def _sift_down(a: list[T], lo: int, s: int, m: int) -> None:
    while 2 * s + 1 < m:
        c = 2 * s + 1
        if c + 1 < m and a[lo + c] < a[lo + c + 1]:
            c += 1
        if not a[lo + s] < a[lo + c]:
            return
        a[lo + s], a[lo + c] = a[lo + c], a[lo + s]
        s = c


# The path for objects: a stable merge of natural runs.


def _sort_objects(a: list[T]) -> None:
    n = len(a)
    min_run = _min_run_length(n)
    runs: list[list[int]] = []  # [start, length], from left to right
    i = 0
    while i < n:
        run = _count_run_and_make_ascending(a, i, n)  # @line L10
        if run < min_run:
            end = min(i + min_run, n)
            _binary_insertion_sort(a, i, end, i + run)
            run = end - i
        runs.append([i, run])  # @line L11
        _merge_collapse(a, runs)
        i += run
    while len(runs) > 1:  # @line L12
        k = len(runs) - 2
        if k > 0 and runs[k - 1][1] < runs[k + 1][1]:
            k -= 1
        _merge_at(a, runs, k)


def _min_run_length(n: int) -> int:
    r = 0
    while n >= MIN_MERGE:
        r |= n & 1
        n >>= 1
    return n + r


def _count_run_and_make_ascending(a: list[T], lo: int, n: int) -> int:
    """Return the length of the run at lo; reverse it first if it strictly descends."""
    hi = lo + 1
    if hi == n:
        return 1
    if a[hi] < a[lo]:
        while hi < n and a[hi] < a[hi - 1]:
            hi += 1
        a[lo:hi] = a[lo:hi][::-1]
    else:
        while hi < n and not a[hi] < a[hi - 1]:
            hi += 1
    return hi - lo


def _binary_insertion_sort(a: list[T], lo: int, hi: int, start: int) -> None:
    """Sort a[lo:hi], of which a[lo:start] is already sorted; equal items keep their order."""
    for i in range(start, hi):
        x = a[i]
        left, right = lo, i
        while left < right:
            mid = (left + right) // 2
            if x < a[mid]:
                right = mid
            else:
                left = mid + 1
        a[left + 1 : i + 1] = a[left:i]
        a[left] = x


def _merge_collapse(a: list[T], runs: list[list[int]]) -> None:
    """Merge runs on the top of the stack until the run lengths satisfy the invariants."""
    while len(runs) > 1:
        k = len(runs) - 2
        length = [r[1] for r in runs]
        if (k > 0 and length[k - 1] <= length[k] + length[k + 1]) or (
            k > 1 and length[k - 2] <= length[k - 1] + length[k]
        ):
            if length[k - 1] < length[k + 1]:
                k -= 1
        elif length[k] > length[k + 1]:
            break
        _merge_at(a, runs, k)


def _merge_at(a: list[T], runs: list[list[int]], k: int) -> None:
    """Merge the adjacent runs k and k + 1, stably, with a copy of the left run."""
    lo, n1 = runs[k]
    mid, n2 = runs[k + 1]
    left = a[lo:mid]
    i, j, out = 0, mid, lo
    while i < n1 and j < mid + n2:
        if a[j] < left[i]:
            a[out] = a[j]
            j += 1
        else:
            a[out] = left[i]
            i += 1
        out += 1
    a[out : out + n1 - i] = left[i:]
    runs[k] = [lo, n1 + n2]
    del runs[k + 1]
test_java_system_sort.py 80 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/java-system-sort/. The authors are listed in prov/YQKT8T.jsonl.

import random

import java_system_sort as js
from java_system_sort import java_system_sort


class Key:
    def __init__(self, k: int, tag: int) -> None:
        self.k, self.tag = k, tag

    def __lt__(self, other: "Key") -> bool:
        return self.k < other.k


def test_example_objects() -> None:
    a = [str(x) for x in [3, 4, 5, 6, 0, 1, 9, 8, 7, 2]]
    java_system_sort(a)
    assert a == [str(x) for x in range(10)]


def test_example_primitives() -> None:
    a = [(37 * i) % 101 for i in range(101)]
    java_system_sort(a)
    assert a == list(range(101))


def test_edges() -> None:
    for a in ([], [1], [2, 1], [1, 1, 1], [3, 2, 1], [2**63 - 1, -(2**63)], ["b", "a"], [1.5, -2.0, 0.0]):
        b = list(a)
        java_system_sort(b)
        assert b == sorted(a)


def test_objects_are_stable() -> None:
    rng = random.Random(7)
    for n in (10, 100, 1000):
        a = [Key(rng.randint(0, 9), i) for i in range(n)]
        java_system_sort(a)
        assert all(
            a[i].k < a[i + 1].k or (a[i].k == a[i + 1].k and a[i].tag < a[i + 1].tag) for i in range(n - 1)
        )


def test_heapsort_fallback() -> None:
    rng = random.Random(3)
    a = [rng.randint(0, 50) for _ in range(500)]
    b = list(a)
    js._sort_primitive(b, 0, len(b) - 1, 1)
    assert b == sorted(a)


def test_runs_and_patterns() -> None:
    n = 2000
    for a in (
        list(range(n)),
        list(range(n, 0, -1)),
        [i % 17 for i in range(n)],
        [0] * n,
        list(range(n // 2)) + list(range(n // 2)),
    ):
        b = [str(x).zfill(5) for x in a]
        c = list(a)
        java_system_sort(b)
        java_system_sort(c)
        assert b == sorted(b) and c == sorted(a)


def test_random_against_sorted() -> None:
    rng = random.Random(1997)
    for t in range(600):
        n = rng.randint(0, 400)
        a = [rng.randint(-50, 50) for _ in range(n)]
        if t % 2:
            a = [str(x) for x in a]
        b = list(a)
        java_system_sort(b)
        assert b == sorted(a)

What the next level needs

  • the tests did not run in this build

Read the full page about Java system sort