Algorithm Search⌘K

Treap split in Python

Splits a treap by key or position into two treaps, one holding smaller elements and one holding the rest, in expected logarithmic time.

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

from __future__ import annotations

from typing import Generic, Iterator, Protocol, TypeVar


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


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


class Node(Generic[K]):
    __slots__ = ("key", "pri", "left", "right")

    def __init__(self, key: K, pri: float) -> None:
        self.key = key
        self.pri = pri
        self.left: Node[K] | None = None
        self.right: Node[K] | None = None


def build(pairs: list[tuple[K, float]]) -> Node[K] | None:
    """Build the treap of distinct (key, priority) pairs.

    Sorts by key and keeps the right spine on a stack, as for a Cartesian
    tree. O(n log n) for the sort, O(n) after it.
    """
    spine: list[Node[K]] = []
    for k, p in sorted(pairs, key=lambda kp: kp[0]):
        x = Node(k, p)
        last: Node[K] | None = None
        while spine and spine[-1].pri < p:
            last = spine.pop()
        x.left = last
        if spine:
            spine[-1].right = x
        spine.append(x)
    return spine[0] if spine else None


def keys(t: Node[K] | None) -> Iterator[K]:
    """Yield the keys of t in increasing order. O(size of t)."""
    stack: list[Node[K]] = []
    while stack or t is not None:
        while t is not None:
            stack.append(t)
            t = t.left
        t = stack.pop()
        yield t.key
        t = t.right


def check_treap(t: Node[K] | None) -> int:
    """Raise AssertionError unless t has search order on keys and max-heap
    order on priorities; return its number of nodes."""
    count = 0
    stack: list[tuple[Node[K], K | None, K | None]] = [(t, None, None)] if t else []
    while stack:
        x, lo, hi = stack.pop()
        count += 1
        assert lo is None or lo < x.key
        assert hi is None or x.key < hi
        if x.left is not None:
            assert x.left.pri <= x.pri
            stack.append((x.left, lo, x.key))
        if x.right is not None:
            assert x.right.pri <= x.pri
            stack.append((x.right, x.key, hi))
    return count


def split(t: Node[K] | None, k: K) -> tuple[Node[K] | None, Node[K] | None]:
    """Split the treap t into (L, R): the keys less than k go to L, the
    keys at least k go to R. Both are treaps. The nodes of t are reused.

    Precondition: t is a treap. Cost O(depth of the search path of k),
    which is O(log n) expected for random priorities.
    """
    if t is None:  # @line L1
        return None, None
    if t.key < k:  # @line L2
        a, b = split(t.right, k)  # @line L3
        t.right = a  # @line L4
        return t, b  # @line L5
    # @line L6
    a, b = split(t.left, k)  # @line L7
    t.left = b  # @line L8
    return a, t  # @line L9
test_treap_split.py 87 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/treap-split/. The authors are listed in prov/WYJ3AW.jsonl.

import random

from treap_split import Node, build, check_treap, keys, split

PAIRS = [(10, 20), (20, 10), (30, 60), (40, 80), (50, 30), (60, 50), (70, 40), (80, 70)]


def shape(t: "Node[int] | None") -> object:
    if t is None:
        return None
    if t.left is None and t.right is None:
        return t.key
    return (shape(t.left), t.key, shape(t.right))


def test_page_example() -> None:
    t = build([(k, float(p)) for k, p in PAIRS])
    assert shape(t) == (((None, 10, 20), 30, None), 40, ((50, 60, 70), 80, None))
    a, b = split(t, 55)
    assert shape(a) == (((None, 10, 20), 30, None), 40, 50)
    assert shape(b) == ((None, 60, 70), 80, None)
    check_treap(a)
    check_treap(b)


def test_edge_cases() -> None:
    assert split(None, 5) == (None, None)
    t = build([(k, float(p)) for k, p in PAIRS])
    a, b = split(t, 0)
    assert a is None and list(keys(b)) == [k for k, _ in sorted(PAIRS)]
    t = build([(k, float(p)) for k, p in PAIRS])
    a, b = split(t, 99)
    assert b is None and list(keys(a)) == [k for k, _ in sorted(PAIRS)]


def test_equal_key_goes_right() -> None:
    # keys less than k go left; a key equal to k goes right
    t = build([(k, float(p)) for k, p in PAIRS])
    a, b = split(t, 40)
    assert list(keys(a)) == [10, 20, 30]
    assert list(keys(b)) == [40, 50, 60, 70, 80]
    assert b is not None and b.key == 40


def test_random_against_sets() -> None:
    rng = random.Random(1989)
    for _ in range(600):
        n = rng.randrange(0, 50)
        ks = rng.sample(range(100), n)
        t = build([(k, rng.random()) for k in ks])
        k = rng.randrange(-1, 102)
        a, b = split(t, k)
        assert list(keys(a)) == sorted(x for x in ks if x < k)
        assert list(keys(b)) == sorted(x for x in ks if x >= k)
        assert check_treap(a) + check_treap(b) == n


def test_expected_path_length() -> None:
    # split visits one node for each level of the search path of k
    rng = random.Random(5)
    n = 4096
    total = 0
    visits = [0]

    def counting_split(t: "Node[int] | None", k: int) -> tuple:
        if t is None:
            return None, None
        visits[0] += 1
        if t.key < k:
            a, b = counting_split(t.right, k)
            t.right = a
            return t, b
        a, b = counting_split(t.left, k)
        t.left = b
        return a, t

    trials = 50
    for _ in range(trials):
        t = build([(k, rng.random()) for k in range(n)])
        visits[0] = 0
        counting_split(t, rng.randrange(n))
        total += visits[0]
    # about 2 ln n, which is less than 17 for n = 4096
    assert total / trials < 3 * 8.32

What the next level needs

  • the tests did not run in this build

Read the full page about Treap split