Algorithm Search⌘K

Merge sort tree in Python

Segment tree whose nodes store sorted copies of their ranges, answering counting and k-th queries.

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

from bisect import bisect_left, bisect_right
from typing import Callable, Optional, Sequence


def _merge(x: list[int], y: list[int]) -> list[int]:
    """Merge two sorted lists into one sorted list in O(len(x) + len(y))."""
    out: list[int] = []
    i = j = 0
    while i < len(x) and j < len(y):
        if y[j] < x[i]:
            out.append(y[j])
            j += 1
        else:
            out.append(x[i])
            i += 1
    out.extend(x[i:])
    out.extend(y[j:])
    return out


class MergeSortTree:
    """Static array with counting, successor and k-th queries on ranges.

    Build: O(n log n) time and space. count_less, count_leq and successor:
    O(log^2 n). kth_smallest: O(log^3 n). Ranges are half-open [l, r).
    """

    def __init__(self, a: Sequence[int]) -> None:
        """Build the tree of sorted lists over a."""
        self.n = len(a)
        size = 1
        while size < self.n:
            size *= 2
        self.size = size
        self.lists: list[list[int]] = [[] for _ in range(2 * size)]
        for i, x in enumerate(a):  # @line L7
            self.lists[size + i] = [x]
        for v in range(size - 1, 0, -1):  # @line L8
            self.lists[v] = _merge(self.lists[2 * v], self.lists[2 * v + 1])

    def _check_range(self, l: int, r: int) -> None:
        if not 0 <= l <= r <= self.n:
            raise IndexError(f"bad range [{l}, {r}) for length {self.n}")

    def _count(self, l: int, r: int, search: Callable[[list[int]], int]) -> int:
        """Sum search(S_v) over the canonical nodes of [l, r)."""
        self._check_range(l, r)
        c = 0
        l += self.size  # @line L1
        r += self.size
        while l < r:  # @line L2
            if l & 1:  # @line L3
                c += search(self.lists[l])
                l += 1
            if r & 1:  # @line L4
                r -= 1
                c += search(self.lists[r])
            l //= 2  # @line L5
            r //= 2
        return c  # @line L6

    def count_less(self, l: int, r: int, x: int) -> int:
        """Number of k in [l, r) with a[k] < x."""
        return self._count(l, r, lambda s: bisect_left(s, x))

    def count_leq(self, l: int, r: int, x: int) -> int:
        """Number of k in [l, r) with a[k] <= x."""
        return self._count(l, r, lambda s: bisect_right(s, x))

    def successor(self, l: int, r: int, x: int) -> Optional[int]:
        """Least a[k] >= x with k in [l, r), or None."""
        self._check_range(l, r)
        best: Optional[int] = None
        l += self.size
        r += self.size
        nodes: list[int] = []
        while l < r:
            if l & 1:
                nodes.append(l)
                l += 1
            if r & 1:
                r -= 1
                nodes.append(r)
            l //= 2
            r //= 2
        for v in nodes:
            s = self.lists[v]
            j = bisect_left(s, x)
            if j < len(s) and (best is None or s[j] < best):
                best = s[j]
        return best

    def kth_smallest(self, l: int, r: int, k: int) -> int:
        """The k-th smallest item of a[l:r], with k counted from 1."""
        self._check_range(l, r)
        if not 1 <= k <= r - l:
            raise ValueError(f"k = {k} out of range for {r - l} items")
        root = self.lists[1]
        lo, hi = 0, self.n - 1  # @line L9
        while lo < hi:  # @line L10
            m = (lo + hi) // 2  # @line L11
            if self.count_leq(l, r, root[m]) >= k:  # @line L12
                hi = m
            else:
                lo = m + 1
        return root[lo]  # @line L13

    def check_invariant(self) -> None:
        """Each node list is the sorted merge of its children's lists."""
        for v in range(self.size, 2 * self.size):
            assert len(self.lists[v]) == (1 if v - self.size < self.n else 0)
        for v in range(1, self.size):
            s = self.lists[v]
            assert s == sorted(self.lists[2 * v] + self.lists[2 * v + 1])
        assert len(self.lists[1]) == self.n
test_merge_sort_tree.py 66 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/merge-sort-tree/. The authors are listed in prov/8KPVMD.jsonl.

import random

import pytest

from merge_sort_tree import MergeSortTree


def test_example() -> None:
    a = [5, 2, 8, 6, 3, 7, 9, 1]
    t = MergeSortTree(a)
    t.check_invariant()
    assert t.lists[2] == [2, 5, 6, 8]
    assert t.lists[3] == [1, 3, 7, 9]
    assert t.lists[4:8] == [[2, 5], [6, 8], [3, 7], [1, 9]]
    assert t.lists[1] == [1, 2, 3, 5, 6, 7, 8, 9]
    assert t.count_less(1, 7, 7) == 3
    assert t.count_leq(1, 7, 6) == 3
    assert t.kth_smallest(1, 7, 3) == 6
    assert t.successor(1, 7, 4) == 6
    assert t.successor(1, 7, 10) is None


def test_edges() -> None:
    t = MergeSortTree([])
    t.check_invariant()
    assert t.count_less(0, 0, 5) == 0
    assert t.successor(0, 0, 5) is None
    with pytest.raises(ValueError):
        t.kth_smallest(0, 0, 1)
    t = MergeSortTree([4])
    assert t.kth_smallest(0, 1, 1) == 4
    assert t.count_leq(0, 1, 4) == 1
    assert t.count_less(0, 1, 4) == 0
    t = MergeSortTree([2, 2, 2, 1, 1])
    t.check_invariant()
    assert [t.kth_smallest(0, 5, k) for k in range(1, 6)] == [1, 1, 2, 2, 2]
    with pytest.raises(IndexError):
        t.count_less(3, 2, 0)
    with pytest.raises(IndexError):
        t.count_less(0, 6, 0)
    with pytest.raises(ValueError):
        t.kth_smallest(1, 3, 3)


def test_random() -> None:
    rng = random.Random(8)
    for _ in range(600):
        n = rng.randint(1, 20)
        a = [rng.randint(-5, 5) for _ in range(n)]
        t = MergeSortTree(a)
        t.check_invariant()
        for _ in range(5):
            l = rng.randint(0, n)
            r = rng.randint(l, n)
            x = rng.randint(-6, 6)
            part = a[l:r]
            assert t.count_less(l, r, x) == sum(1 for y in part if y < x)
            assert t.count_leq(l, r, x) == sum(1 for y in part if y <= x)
            big = [y for y in part if y >= x]
            assert t.successor(l, r, x) == (min(big) if big else None)
            if part:
                k = rng.randint(1, len(part))
                assert t.kth_smallest(l, r, k) == sorted(part)[k - 1]

What the next level needs

  • the tests did not run in this build

Read the full page about Merge sort tree