Algorithm Search⌘K

Threaded binary tree in Python

BST whose null child pointers link to in-order predecessor or successor, enabling stackless traversal.

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

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

K = TypeVar("K")


class Node(Generic[K]):
    """A node of a fully threaded binary search tree.

    If lthread is True, left is a thread to the in-order predecessor (or
    None for the smallest key); else left is the left child. The same holds
    for rthread, right and the in-order successor.
    """

    __slots__ = ("key", "left", "right", "lthread", "rthread")

    def __init__(self, key: K) -> None:
        self.key = key
        self.left: "Node[K] | None" = None
        self.right: "Node[K] | None" = None
        self.lthread = True
        self.rthread = True


class ThreadedBinarySearchTree(Generic[K]):
    """A fully threaded binary search tree of distinct keys.

    Empty child fields hold threads to the in-order neighbours, so an
    in-order walk needs no stack and no parent pointers. Search and insert
    take O(h) time for height h; a full walk takes O(n) time and O(1) space.
    """

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

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

    @staticmethod
    def successor_node(x: Node[K]) -> Node[K] | None:
        """The in-order successor of x, or None. O(h) worst, O(1) amortized in a walk."""
        if x.rthread:  # @line L1
            return x.right
        y = x.right  # @line L2
        assert y is not None
        while not y.lthread:  # @line L3
            y = y.left  # type: ignore[assignment]
        return y  # @line L4

    @staticmethod
    def predecessor_node(x: Node[K]) -> Node[K] | None:
        if x.lthread:
            return x.left
        y = x.left
        assert y is not None
        while not y.rthread:
            y = y.right  # type: ignore[assignment]
        return y

    def minimum_node(self) -> Node[K] | None:
        x = self.root
        if x is None:
            return None
        while not x.lthread:
            x = x.left  # type: ignore[assignment]
        return x

    def search(self, k: K) -> Node[K] | None:
        """The node with key k, or None. Threads end the search like NIL."""
        x = self.root
        while x is not None:
            if k < x.key:
                if x.lthread:
                    return None
                x = x.left
            elif x.key < k:
                if x.rthread:
                    return None
                x = x.right
            else:
                return x
        return None

    def insert(self, k: K) -> bool:
        """Insert k. Return False if k was already a key. O(h) time."""
        if self.root is None:  # @line L5
            self.root = Node(k)
            self.n = 1
            return True
        x = self.root  # @line L6
        while True:  # @line L7
            if not (k < x.key or x.key < k):  # @line L8
                return False
            if k < x.key:  # @line L9
                if not x.lthread:  # @line L10
                    x = x.left  # type: ignore[assignment]
                    continue
                z = Node(k)  # @line L11
                z.left, z.right = x.left, x
                x.left, x.lthread = z, False
                break
            if not x.rthread:  # @line L12
                x = x.right  # type: ignore[assignment]
                continue
            z = Node(k)  # @line L13
            z.left, z.right = x, x.right
            x.right, x.rthread = z, False
            break
        self.n += 1
        return True

    def successor(self, k: K) -> K | None:
        x = self.search(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(k)
        if x is None:
            raise KeyError(k)
        s = self.predecessor_node(x)
        return None if s is None else s.key

    def __iter__(self) -> Iterator[K]:
        """In-order walk with O(1) extra space."""
        x = self.minimum_node()
        while x is not None:
            yield x.key
            x = self.successor_node(x)

    def check_invariant(self) -> None:
        """Check the BST property on child links and every thread."""
        order: list[Node[K]] = []
        stack: list[Node[K]] = []
        x = self.root
        while stack or x is not None:  # in-order walk on child links only
            while x is not None:
                stack.append(x)
                x = None if x.lthread else x.left
            x = stack.pop()
            order.append(x)
            x = None if x.rthread else x.right
        assert len(order) == self.n
        for i, node in enumerate(order):
            if i > 0:
                assert order[i - 1].key < node.key
            if node.lthread:
                assert node.left is (order[i - 1] if i > 0 else None)
            if node.rthread:
                assert node.right is (order[i + 1] if i + 1 < len(order) else None)

    def threads(self) -> dict[Any, tuple[Any, Any]]:
        """For each key, the keys its threads point to ('-' for a child link)."""
        out = {}
        stack = [self.root] if self.root else []
        while stack:
            x = stack.pop()
            lt = (x.left.key if x.left else None) if x.lthread else "-"
            rt = (x.right.key if x.right else None) if x.rthread else "-"
            out[x.key] = (lt, rt)
            if not x.lthread:
                stack.append(x.left)
            if not x.rthread:
                stack.append(x.right)
        return out
test_threaded_binary_tree.py 59 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/threaded-binary-tree/. The authors are listed in prov/EJENNS.jsonl.

import random

from threaded_binary_tree import ThreadedBinarySearchTree


def test_example() -> None:
    t: ThreadedBinarySearchTree[int] = ThreadedBinarySearchTree()
    for k in [50, 30, 70, 20, 40, 60, 80]:
        assert t.insert(k)
        t.check_invariant()
    th = t.threads()
    assert th[20] == (None, 30) and th[40] == (30, 50)
    assert th[60] == (50, 70) and th[80] == (70, None)
    assert th[50] == ("-", "-")
    assert t.insert(45)
    t.check_invariant()
    th = t.threads()
    assert th[45] == (40, 50) and th[40] == (30, "-")
    assert list(t) == [20, 30, 40, 45, 50, 60, 70, 80]
    assert t.successor(45) == 50 and t.successor(30) == 40
    assert t.predecessor(60) == 50 and t.successor(80) is None


def test_edges() -> None:
    t: ThreadedBinarySearchTree[int] = ThreadedBinarySearchTree()
    assert list(t) == [] and t.search(1) is None
    assert t.insert(1) and not t.insert(1)
    assert list(t) == [1] and len(t) == 1
    assert t.successor(1) is None and t.predecessor(1) is None


def test_chain() -> None:
    t: ThreadedBinarySearchTree[int] = ThreadedBinarySearchTree()
    for k in range(2000, 0, -1):
        t.insert(k)
    t.check_invariant()
    assert list(t) == list(range(1, 2001))


def test_random_against_sorted() -> None:
    rng = random.Random(1960)
    for _ in range(500):
        t: ThreadedBinarySearchTree[int] = ThreadedBinarySearchTree()
        s: set[int] = set()
        for _ in range(rng.randint(0, 40)):
            k = rng.randint(0, 60)
            assert t.insert(k) == (k not in s)
            s.add(k)
        t.check_invariant()
        ks = sorted(s)
        assert list(t) == 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 else None)
        for q in range(-1, 62):
            assert (t.search(q) is not None) == (q in s)

What the next level needs

  • the tests did not run in this build

Read the full page about Threaded binary tree