Algorithm Search⌘K

Binary search tree in Python

Binary tree satisfying the BST property; supports dictionary operations in time proportional to its height.

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

from collections.abc import Iterator
from typing import Any, Generic, Protocol, TypeVar


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


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


class Node(Generic[K, V]):
    """A node with a key, a value, two children and a parent pointer."""

    __slots__ = ("key", "value", "left", "right", "p")

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


class BinarySearchTree(Generic[K, V]):
    """An unbalanced binary search tree that maps distinct keys to values.

    Each operation takes O(h) time, where h is the height of the tree. The
    height is between floor(lg n) and n - 1. All loops are iterative, so a
    tall tree does not overflow the Python stack. Keys need only "<".
    """

    def __init__(self) -> None:
        self.root: Node[K, V] | None = None
        self.n = 0

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

    def search_node(self, k: K) -> Node[K, V] | None:
        """Return the node with key k, or None."""
        x = self.root  # @line L1
        while x is not None and (k < x.key or x.key < k):  # @line L2
            x = x.left if k < x.key else x.right  # @line L3
        return x  # @line L4

    def search(self, k: K) -> V | None:
        """Return the value of k, or None if k is not a key."""
        x = self.search_node(k)
        return None if x is None else x.value

    def contains(self, k: K) -> bool:
        return self.search_node(k) is not None

    @staticmethod
    def minimum_node(x: Node[K, V]) -> Node[K, V]:
        """Return the node with the smallest key in the subtree of x."""
        while x.left is not None:  # @line L5
            x = x.left
        return x  # @line L6

    @staticmethod
    def maximum_node(x: Node[K, V]) -> Node[K, V]:
        while x.right is not None:
            x = x.right
        return x

    def minimum(self) -> K | None:
        return None if self.root is None else self.minimum_node(self.root).key

    def maximum(self) -> K | None:
        return None if self.root is None else self.maximum_node(self.root).key

    def successor_node(self, x: Node[K, V]) -> Node[K, V] | None:
        """Return the node with the next larger key, or None."""
        if x.right is not None:  # @line L7
            return self.minimum_node(x.right)
        y = x.p  # @line L8
        while y is not None and x is y.right:  # @line L9
            x, y = y, y.p
        return y  # @line L10

    def predecessor_node(self, x: Node[K, V]) -> Node[K, V] | None:
        if x.left is not None:
            return self.maximum_node(x.left)
        y = x.p
        while y is not None and x is y.left:
            x, y = y, y.p
        return y

    def successor(self, k: K) -> K | None:
        """The next larger key after the key k of the tree, or None."""
        x = self.search_node(k)
        if x is None:
            raise KeyError(k)
        s = self.successor_node(x)
        return None if s is None else s.key

    def predecessor(self, k: K) -> K | None:
        x = self.search_node(k)
        if x is None:
            raise KeyError(k)
        s = self.predecessor_node(x)
        return None if s is None else s.key

    def insert(self, k: K, v: V = None) -> None:  # type: ignore[assignment]
        """Map k to v. If k is a key, replace its value."""
        y, x = None, self.root  # @line L11
        while x is not None:  # @line L12
            y = x  # @line L13
            if k < x.key:  # @line L14
                x = x.left
            elif x.key < k:
                x = x.right
            else:
                x.value = v  # @line L15
                return
        z = Node(k, v)
        z.p = y  # @line L16
        if y is None:  # @line L17
            self.root = z
        elif k < y.key:  # @line L18
            y.left = z
        else:
            y.right = z  # @line L19
        self.n += 1

    def _transplant(self, u: Node[K, V], v: Node[K, V] | None) -> None:
        """Put the subtree of v in the place of the subtree of u."""
        if u.p is None:  # @line L20
            self.root = v
        elif u is u.p.left:  # @line L21
            u.p.left = v
        else:
            u.p.right = v  # @line L22
        if v is not None:  # @line L23
            v.p = u.p

    def delete(self, k: K) -> bool:
        """Remove k and its value. Return True if k was a key."""
        z = self.search_node(k)
        if z is None:
            return False
        if z.left is None:  # @line L24
            self._transplant(z, z.right)
        elif z.right is None:  # @line L25
            self._transplant(z, z.left)
        else:
            y = self.minimum_node(z.right)  # @line L26
            if y is not z.right:  # @line L27
                self._transplant(y, y.right)
                y.right = z.right
                y.right.p = y
            self._transplant(z, y)  # @line L28
            y.left = z.left
            y.left.p = y
        self.n -= 1
        return True

    def items(self) -> Iterator[tuple[K, V]]:
        """The pairs in increasing order of key (in-order walk with a stack)."""
        stack: list[Node[K, V]] = []
        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.key, x.value
            x = x.right

    def keys(self) -> list[K]:
        return [k for k, _ in self.items()]

    def height(self) -> int:
        """The number of edges on a longest root-to-leaf path; -1 if empty."""
        if self.root is None:
            return -1
        best, stack = 0, [(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:
        """Check the BST property, the parent pointers and the count."""
        count = 0
        stack: list[tuple[Node[K, V], Any, Any]] = []
        if self.root is not None:
            assert self.root.p is None
            stack.append((self.root, None, None))
        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.p is x
                stack.append((x.left, lo, x.key))
            if x.right is not None:
                assert x.right.p is x
                stack.append((x.right, x.key, hi))
        assert count == self.n
test_binary_search_tree.py 84 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/binary-search-tree/. The authors are listed in prov/MBF8HP.jsonl.

import bisect
import random

from binary_search_tree import BinarySearchTree

EXAMPLE = [15, 6, 18, 3, 7, 17, 20, 2, 4, 13, 9]


def build(keys: list[int]) -> BinarySearchTree[int, int]:
    t: BinarySearchTree[int, int] = BinarySearchTree()
    for k in keys:
        t.insert(k, 10 * k)
        t.check_invariant()
    return t


def test_example() -> None:
    t = build(EXAMPLE)
    assert t.keys() == sorted(EXAMPLE)
    assert t.height() == 4
    assert t.search(13) == 130
    assert t.search(8) is None
    assert t.minimum() == 2 and t.maximum() == 20
    assert t.successor(13) == 15
    assert t.successor(4) == 6
    assert t.predecessor(17) == 15
    assert t.successor(20) is None
    assert t.delete(6)
    t.check_invariant()
    assert t.root is not None and t.root.left is not None and t.root.left.key == 7
    assert t.delete(15)
    t.check_invariant()
    assert t.root is not None and t.root.key == 17
    assert t.keys() == [2, 3, 4, 7, 9, 13, 17, 18, 20]


def test_edges() -> None:
    t: BinarySearchTree[int, str] = BinarySearchTree()
    assert t.search(1) is None and t.minimum() is None and t.height() == -1
    assert not t.delete(1)
    t.insert(1, "a")
    t.insert(1, "b")
    assert len(t) == 1 and t.search(1) == "b"
    assert t.delete(1) and len(t) == 0 and t.root is None


def test_sorted_input_is_a_path() -> None:
    t = build(list(range(3000)))
    assert t.height() == 2999
    assert t.search(2999) == 29990
    for k in range(0, 3000, 2):
        assert t.delete(k)
    t.check_invariant()
    assert t.keys() == list(range(1, 3000, 2))


def test_random_against_model() -> None:
    rng = random.Random(4127)
    for _ in range(500):
        t: BinarySearchTree[int, int] = BinarySearchTree()
        model: dict[int, int] = {}
        for _ in range(rng.randint(0, 40)):
            k = rng.randint(0, 30)
            op = rng.random()
            if op < 0.5:
                t.insert(k, op.__hash__())
                model[k] = op.__hash__()
            elif op < 0.8:
                assert t.delete(k) == (k in model)
                model.pop(k, None)
            else:
                assert t.search(k) == model.get(k)
            t.check_invariant()
        ks = sorted(model)
        assert t.keys() == ks
        for i, k in enumerate(ks):
            assert t.successor(k) == (ks[i + 1] if i + 1 < len(ks) else None)
            assert t.predecessor(k) == (ks[i - 1] if i > 0 else None)
        assert t.minimum() == (ks[0] if ks else None)
        j = bisect.bisect_left(ks, 15)
        assert (t.search(15) is not None) == (j < len(ks) and ks[j] == 15)

What the next level needs

  • the tests did not run in this build

Read the full page about Binary search tree