Algorithm Search⌘K

AVL tree in Python

The first self-balancing BST: balance factors in {-1, 0, 1} are restored by single or double rotations after updates.

File
avl_tree.py, 182 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
avl_tree.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/avl-tree/. The authors are listed in prov/195NAP.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", "left", "right", "h")

    def __init__(self, key: K) -> None:
        self.key = key
        self.left: Node[K] | None = None
        self.right: Node[K] | None = None
        self.h = 1  # the number of nodes on the longest downward path


def height(t: Node[K] | None) -> int:
    return t.h if t is not None else 0


def update(t: Node[K]) -> None:
    t.h = 1 + max(height(t.left), height(t.right))


def bf(t: Node[K]) -> int:
    """Balance factor: height of the right subtree minus height of the left one."""
    return height(t.right) - height(t.left)


def rotate_left(x: Node[K]) -> Node[K]:
    y = x.right
    assert y is not None
    x.right = y.left
    y.left = x
    update(x)
    update(y)
    return y


def rotate_right(y: Node[K]) -> Node[K]:
    x = y.left
    assert x is not None
    y.left = x.right
    x.right = y
    update(y)
    update(x)
    return x


def rebalance(t: Node[K]) -> Node[K]:
    update(t)  # @line L6
    if bf(t) == 2:  # @line L7
        assert t.right is not None
        if bf(t.right) < 0:  # @line L8
            t.right = rotate_right(t.right)
        return rotate_left(t)  # @line L9
    if bf(t) == -2:  # @line L10
        assert t.left is not None
        if bf(t.left) > 0:  # @line L11
            t.left = rotate_left(t.left)
        return rotate_right(t)  # @line L12
    return t  # @line L13


def insert(t: Node[K] | None, k: K) -> Node[K]:
    if t is None:  # @line L1
        return Node(k)
    if k < t.key:  # @line L2
        t.left = insert(t.left, k)
    elif t.key < k:  # @line L3
        t.right = insert(t.right, k)
    else:
        return t  # @line L4
    return rebalance(t)  # @line L5


def delete(t: Node[K] | None, k: K) -> Node[K] | None:
    if t is None:  # @line L14
        return None
    if k < t.key:  # @line L15
        t.left = delete(t.left, k)
    elif t.key < k:  # @line L16
        t.right = delete(t.right, k)
    else:  # @line L17
        if t.left is None:  # @line L18
            return t.right
        if t.right is None:  # @line L19
            return t.left
        m = t.right  # @line L20
        while m.left is not None:
            m = m.left
        t.key = m.key  # @line L21
        t.right = delete(t.right, m.key)
    return rebalance(t)  # @line L22


class AVLTree(Generic[K]):
    """An ordered set of keys kept in an AVL tree.

    Each operation takes O(log n) time in the worst case for n keys, and the
    recursion depth is the height, at most about 1.44 lg n. Keys must be
    totally ordered by <; equal keys are stored once.
    """

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

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

    def contains(self, k: K) -> bool:
        t = self.root
        while t is not None:
            if k < t.key:
                t = t.left
            elif t.key < k:
                t = t.right
            else:
                return True
        return False

    def insert(self, k: K) -> bool:
        """Insert k. Return True if k was not present."""
        if self.contains(k):
            return False
        self.root = insert(self.root, k)
        self.n += 1
        return True

    def delete(self, k: K) -> bool:
        """Delete k. Return True if k was present."""
        if not self.contains(k):
            return False
        self.root = delete(self.root, k)
        self.n -= 1
        return True

    def height(self) -> int:
        return height(self.root)

    def keys(self) -> list[K]:
        out: list[K] = []
        stack: list[Node[K]] = []
        t = self.root
        while stack or t is not None:
            while t is not None:
                stack.append(t)
                t = t.left
            t = stack.pop()
            out.append(t.key)
            t = t.right
        return out

    def __iter__(self) -> Iterator[K]:
        return iter(self.keys())

    def check_invariant(self) -> None:
        """Raise AssertionError unless the tree is an AVL search tree of n keys."""

        def walk(t: Node[K] | None, lo: K | None, hi: K | None) -> tuple[int, int]:
            if t is None:
                return 0, 0
            assert lo is None or lo < t.key
            assert hi is None or t.key < hi
            hl, cl = walk(t.left, lo, t.key)
            hr, cr = walk(t.right, t.key, hi)
            assert t.h == 1 + max(hl, hr), "stored height is wrong"
            assert abs(hr - hl) <= 1, "balance factor out of range"
            return t.h, cl + cr + 1

        _, count = walk(self.root, None, None)
        assert count == self.n
test_avl_tree.py 88 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/avl-tree/. The authors are listed in prov/195NAP.jsonl.

import math
import random

from avl_tree import AVLTree, Node


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:
    a: AVLTree[int] = AVLTree()
    for k in [10, 20, 30, 40, 50, 25]:
        a.insert(k)
        a.check_invariant()
    assert shape(a.root) == ((10, 20, 25), 30, (None, 40, 50))
    a.delete(40)
    a.check_invariant()
    assert shape(a.root) == ((10, 20, 25), 30, 50)
    a.delete(50)
    a.check_invariant()
    assert shape(a.root) == (10, 20, (25, 30, None))


def test_sorted_input_is_perfect() -> None:
    a: AVLTree[int] = AVLTree()
    for k in range(1, 8):
        a.insert(k)
    assert shape(a.root) == ((1, 2, 3), 4, (5, 6, 7))
    for k in range(8, 1024):
        a.insert(k)
    a.check_invariant()
    assert a.height() == 10


def test_child_with_zero_balance_after_delete() -> None:
    a: AVLTree[int] = AVLTree()
    for k in [1, 7, 2, 6, 3, 5, 8, 4]:
        a.insert(k)
    assert shape(a.root) == ((1, 2, None), 3, ((4, 5, None), 6, (None, 7, 8)))
    a.delete(1)
    a.check_invariant()
    assert shape(a.root) == ((2, 3, (4, 5, None)), 6, (None, 7, 8))


def test_edges() -> None:
    a: AVLTree[int] = AVLTree()
    assert not a.delete(1) and not a.contains(1) and a.height() == 0
    assert a.insert(5) and not a.insert(5)
    assert len(a) == 1 and a.keys() == [5]
    assert a.delete(5) and len(a) == 0 and a.root is None
    for k in [3, 3, 3]:
        a.insert(k)
    assert a.keys() == [3]


def test_height_bound() -> None:
    rng = random.Random(4)
    a: AVLTree[int] = AVLTree()
    for _ in range(3000):
        a.insert(rng.randint(0, 10**6))
        n = len(a)
        assert a.height() < 1.4405 * math.log2(n + 2) - 0.3277


def test_random_against_set() -> None:
    rng = random.Random(1962)
    for _ in range(500):
        a: AVLTree[int] = AVLTree()
        model: set[int] = set()
        for _ in range(rng.randint(0, 80)):
            k = rng.randint(0, 40)
            if rng.random() < 0.6:
                assert a.insert(k) == (k not in model)
                model.add(k)
            else:
                assert a.delete(k) == (k in model)
                model.discard(k)
            assert a.contains(k) == (k in model)
        a.check_invariant()
        assert a.keys() == sorted(model)

What the next level needs

  • the tests did not run in this build

Read the full page about AVL tree