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
# 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