Algorithm Search⌘K

Splay tree in Python

Self-adjusting BST that splays every accessed key to the root; amortized O(log n) per operation.

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

"""Splay tree (Sleator and Tarjan 1985) with bottom-up splaying."""

from typing import Any, Iterator


class Node:
    __slots__ = ("key", "value", "left", "right", "p")

    def __init__(self, key: Any, value: Any = None) -> None:
        self.key = key
        self.value = value
        self.left: "Node | None" = None
        self.right: "Node | None" = None
        self.p: "Node | None" = None


def _rotate(x: Node) -> None:
    y = x.p
    assert y is not None
    z = y.p
    if x is y.left:
        y.left = x.right
        if x.right is not None:
            x.right.p = y
        x.right = y
    else:
        y.right = x.left
        if x.left is not None:
            x.left.p = y
        x.left = y
    y.p = x
    x.p = z
    if z is not None:
        if z.left is y:
            z.left = x
        else:
            z.right = x


def _splay(x: Node) -> int:
    """Splay x to the root of its tree; return the number of rotations."""
    rot = 0
    while x.p is not None:
        y = x.p
        z = y.p
        if z is None:
            _rotate(x)
            rot += 1
        elif (x is y.left) == (y is z.left):
            _rotate(y)
            _rotate(x)
            rot += 2
        else:
            _rotate(x)
            _rotate(x)
            rot += 2
    return rot


class SplayTree:
    """Ordered map. Every operation splays, so the amortized cost is O(log n)."""

    def __init__(self) -> None:
        self.root: Node | None = None
        self.n = 0
        self.rotations = 0  # total rotations, for experiments

    def __len__(self) -> int:
        return self.n

    def _splay(self, x: Node) -> None:
        self.rotations += _splay(x)
        self.root = x

    def access(self, key: Any) -> Node | None:
        """Splay the node of key, or the last node on its search path. Return the node of key or None."""
        x = self.root  # @line L1
        last = None
        while x is not None and (key < x.key or x.key < key):  # @line L2
            last = x
            x = x.left if key < x.key else x.right
        if x is None:  # @line L3
            x = last
        if x is not None:  # @line L4
            self._splay(x)
        r = self.root  # @line L5
        if r is not None and not (key < r.key or r.key < key):
            return r
        return None

    def search(self, key: Any) -> Any:
        x = self.access(key)
        return None if x is None else x.value

    def __contains__(self, key: Any) -> bool:
        return self.access(key) is not None

    def insert(self, key: Any, value: Any = None) -> bool:
        """Map key to value; return True if key was new."""
        if self.root is None:  # @line L6
            self.root = Node(key, value)
            self.n = 1
            return True
        found = self.access(key)  # @line L7
        if found is not None:
            found.value = value
            return False
        z = Node(key, value)  # @line L8
        r = self.root
        assert r is not None
        if key < r.key:  # @line L9
            z.left = r.left
            z.right = r
            r.left = None
        else:  # @line L10
            z.right = r.right
            z.left = r
            r.right = None
        for c in (z.left, z.right):  # @line L11
            if c is not None:
                c.p = z
        self.root = z
        self.n += 1
        return True

    def _join(self, left: Node | None, right: Node | None) -> Node | None:
        """Join two trees whose keys are all smaller in left. Return the new root."""
        if left is None:  # @line L15
            if right is not None:
                right.p = None
            return right
        left.p = None
        m = left
        while m.right is not None:  # @line L16
            m = m.right
        self.rotations += _splay(m)
        m.right = right  # @line L17
        if right is not None:
            right.p = m
        return m

    def delete(self, key: Any) -> bool:
        """Remove key; return True if it was present."""
        x = self.access(key)  # @line L12
        if x is None:
            return False
        left, right = x.left, x.right  # @line L13
        if left is not None:
            left.p = None
        if right is not None:
            right.p = None
        self.root = self._join(left, right)  # @line L14
        self.n -= 1
        return True

    def split(self, key: Any) -> "SplayTree":
        """Keep the keys <= key in self; return a new tree with the keys > key."""
        other = SplayTree()
        self.access(key)  # @line L18
        r = self.root
        if r is None:
            return other
        if not (key < r.key):  # @line L19
            rest = r.right
            r.right = None
        else:  # @line L20
            rest = r
            self.root = r.left
            r.left = None
            if self.root is not None:
                self.root.p = None
        if rest is not None:
            rest.p = None
        other.root = rest
        other.n = sum(1 for _ in other.keys())
        self.n -= other.n
        return other

    def join(self, other: "SplayTree") -> None:
        """Append all keys of other, which must all exceed the keys of self."""
        self.root = self._join(self.root, other.root)
        self.n += other.n
        other.root = None
        other.n = 0

    def minimum(self) -> Any:
        x = self.root
        if x is None:
            return None
        while x.left is not None:
            x = x.left
        self._splay(x)
        return x.key

    def floor(self, key: Any) -> Any:
        """The largest key <= key, or None. Splays."""
        self.access(key)
        r = self.root
        if r is None:
            return None
        if not (key < r.key):
            return r.key
        x = r.left
        if x is None:
            return None
        while x.right is not None:
            x = x.right
        self._splay(x)
        return x.key

    def _nodes(self) -> Iterator[Node]:
        stack: list[Node] = []
        x = self.root
        while stack or x is not None:
            while x is not None:
                stack.append(x)
                x = x.left
            x = stack.pop()
            yield x
            x = x.right

    def keys(self) -> Iterator[Any]:
        for x in self._nodes():
            yield x.key

    def items(self) -> Iterator[tuple[Any, Any]]:
        for x in self._nodes():
            yield x.key, x.value

    def height(self) -> int:
        if self.root is None:
            return -1
        best = 0
        stack = [(self.root, 0)]
        while stack:
            x, d = stack.pop()
            best = max(best, d)
            for c in (x.left, x.right):
                if c is not None:
                    stack.append((c, d + 1))
        return best

    def check_invariant(self) -> None:
        """BST order, parent links, and the size count."""
        if self.root is None:
            assert self.n == 0
            return
        assert self.root.p is None
        ks = list(self.keys())
        assert len(ks) == self.n
        assert all(a < b for a, b in zip(ks, ks[1:]))
        stack = [self.root]
        while stack:
            x = stack.pop()
            for c in (x.left, x.right):
                if c is not None:
                    assert c.p is x
                    stack.append(c)
test_splay_tree.py 96 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/splay-tree/. The authors are listed in prov/MX6QFK.jsonl.

import bisect
import math
import random

from splay_tree import SplayTree


def test_example() -> None:
    t = SplayTree()
    for k in [1, 2, 3, 4, 5, 6, 7]:
        t.insert(k)
    t.check_invariant()
    assert t.height() == 6 and t.root.key == 7
    assert t.search(1) is None and t.root.key == 1
    assert t.height() == 4
    assert t.insert(3, "c") is False and t.root.key == 3 and t.search(3) == "c"
    assert t.delete(3) and 3 not in t and len(t) == 6
    t.check_invariant()
    assert list(t.keys()) == [1, 2, 4, 5, 6, 7]


def test_split_join() -> None:
    t = SplayTree()
    for k in range(0, 100, 3):
        t.insert(k)
    u = t.split(50)
    t.check_invariant()
    u.check_invariant()
    assert list(t.keys()) == list(range(0, 51, 3)) and list(u.keys()) == list(range(51, 100, 3))
    t.join(u)
    t.check_invariant()
    assert list(t.keys()) == list(range(0, 100, 3)) and len(u) == 0


def test_sequential_access_is_linear() -> None:
    n = 2000
    t = SplayTree()
    keys = list(range(n))
    random.Random(5).shuffle(keys)
    for k in keys:
        t.insert(k)
    t.rotations = 0
    for k in range(n):
        assert t.search(k) is None
    assert t.rotations <= 5 * n


def test_amortized_bound_on_chain_accesses() -> None:
    n = 1024
    t = SplayTree()
    for k in range(n):
        t.insert(k)
    t.rotations = 0
    m = 4000
    rng = random.Random(8)
    for _ in range(m):
        t.access(rng.randrange(n))
    assert t.rotations <= m * (3 * math.log2(n) + 1) + n * math.log2(n)


def test_random_against_sorted_dict() -> None:
    rng = random.Random(2718)
    for _ in range(500):
        t = SplayTree()
        ref: dict[int, int] = {}
        for _ in range(rng.randint(1, 60)):
            k = rng.randint(0, 40)
            op = rng.random()
            if op < 0.45:
                assert t.insert(k, -k) == (k not in ref)
                ref[k] = -k
                assert t.root.key == k
            elif op < 0.75:
                assert t.delete(k) == (k in ref)
                ref.pop(k, None)
            elif op < 0.9:
                assert t.search(k) == ref.get(k)
            else:
                ks = sorted(ref)
                i = bisect.bisect_right(ks, k)
                assert t.floor(k) == (ks[i - 1] if i else None)
        t.check_invariant()
        assert list(t.items()) == sorted(ref.items())
        if ref and rng.random() < 0.5:
            cut = rng.randint(-1, 41)
            u = t.split(cut)
            t.check_invariant()
            u.check_invariant()
            assert list(t.keys()) == [k for k in sorted(ref) if k <= cut]
            assert list(u.keys()) == [k for k in sorted(ref) if k > cut]
            t.join(u)
            t.check_invariant()
            assert t.minimum() == min(ref)

What the next level needs

  • the tests did not run in this build

Read the full page about Splay tree