๐Ÿฆ€ Functional Rust

263: AVL Tree

Difficulty: 3 Level: Advanced A self-balancing binary search tree that guarantees O(log n) operations by automatically rotating after every insert.

The Problem This Solves

A plain binary search tree degenerates on sorted input. Insert `[1, 2, 3, 4, 5]` into an unbalanced BST and you get a linked list โ€” O(n) search, O(n) insert, O(n) everything. That's fine for random data but catastrophic for sorted or nearly-sorted data, which is exactly what production systems encounter (log timestamps, user IDs, sorted imports). An AVL tree fixes this by tracking the height of every subtree and rebalancing via rotations whenever the left/right heights differ by more than 1. The invariant holds after every insertion, so you're always within one rotation of balance. The result: O(log n) search, insert, and delete โ€” guaranteed, regardless of input order. This example builds a complete AVL tree from scratch in Rust, showing how move semantics make structural sharing explicit, how named enum fields improve readability over positional tuples, and how `Box` ownership drives the tree reconstruction during rotations.

The Intuition

A self-balancing BST that tracks heights and applies left/right rotations after each insert to keep the tree no more than 1 level out of balance.

How It Works in Rust

// Each node stores: left child, value, right child, and cached height.
// height = 1 + max(left.height, right.height)
pub enum Avl<T> {
 Empty,
 Node { left: Box<Avl<T>>, value: T, right: Box<Avl<T>>, height: i32 },
}

// balance_factor = left.height - right.height
// > 1: left-heavy โ†’ rotate right
// < -1: right-heavy โ†’ rotate left

fn rebalance(self) -> Self {
 let bf = self.balance_factor();
 if bf > 1      { self.rotate_right() }  // left subtree too tall
 else if bf < -1 { self.rotate_left()  }  // right subtree too tall
 else            { self }                  // already balanced
}

// insert is recursive + rebalance on the way back up
pub fn insert(&self, x: T) -> Self {
 match self {
     Avl::Empty => Self::node(Avl::Empty, x, Avl::Empty),
     Avl::Node { left, value, right, .. } => match x.cmp(value) {
         Ordering::Less    => Self::node(left.insert(x), value.clone(), (**right).clone()).rebalance(),
         Ordering::Greater => Self::node((**left).clone(), value.clone(), right.insert(x)).rebalance(),
         Ordering::Equal   => self.clone(),  // no duplicates
     },
 }
}
Rotations consume `self` by value (move semantics) and reconstruct the tree. This makes the structural change explicit โ€” you can see exactly which subtrees move where.

What This Unlocks

Key Differences

ConceptOCamlRust
Node type`type 'a avl = Empty \Node of 'a avl 'a 'a avl * int``enum Avl<T> { Empty, Node { left, value, right, height } }`
Named fieldsPositional (tuple in variant)Named struct fields in enum variant
RotationPattern match, create new nodes (GC handles old)Consume `self` by move, reconstruct โ€” borrow checker enforces
CloningImplicit sharing (GC)Explicit `.clone()` required
Height type`int` (native)`i32` (explicit)
/// AVL Tree โ€” Self-Balancing BST with Rotations

use std::cmp::{max, Ordering};

#[derive(Debug, Clone, PartialEq)]
pub enum Avl<T> {
    Empty,
    Node {
        left: Box<Avl<T>>,
        value: T,
        right: Box<Avl<T>>,
        height: i32,
    },
}

impl<T: Ord + Clone> Avl<T> {
    pub fn empty() -> Self { Avl::Empty }

    pub fn height(&self) -> i32 {
        match self {
            Avl::Empty => 0,
            Avl::Node { height, .. } => *height,
        }
    }

    fn node(left: Avl<T>, value: T, right: Avl<T>) -> Self {
        let h = 1 + max(left.height(), right.height());
        Avl::Node { left: Box::new(left), value, right: Box::new(right), height: h }
    }

    fn balance_factor(&self) -> i32 {
        match self {
            Avl::Empty => 0,
            Avl::Node { left, right, .. } => left.height() - right.height(),
        }
    }

    fn rotate_right(self) -> Self {
        match self {
            Avl::Node { left, value, right, .. } => match *left {
                Avl::Node { left: ll, value: lv, right: lr, .. } =>
                    Self::node(*ll, lv, Self::node(*lr, value, *right)),
                _ => Self::node(*left, value, *right),
            },
            other => other,
        }
    }

    fn rotate_left(self) -> Self {
        match self {
            Avl::Node { left, value, right, .. } => match *right {
                Avl::Node { left: rl, value: rv, right: rr, .. } =>
                    Self::node(Self::node(*left, value, *rl), rv, *rr),
                _ => Self::node(*left, value, *right),
            },
            other => other,
        }
    }

    fn rebalance(self) -> Self {
        let bf = self.balance_factor();
        if bf > 1 { self.rotate_right() }
        else if bf < -1 { self.rotate_left() }
        else { self }
    }

    pub fn insert(&self, x: T) -> Self {
        match self {
            Avl::Empty => Self::node(Avl::Empty, x, Avl::Empty),
            Avl::Node { left, value, right, .. } => match x.cmp(value) {
                Ordering::Less => Self::node(left.insert(x), value.clone(), (**right).clone()).rebalance(),
                Ordering::Greater => Self::node((**left).clone(), value.clone(), right.insert(x)).rebalance(),
                Ordering::Equal => self.clone(),
            },
        }
    }

    pub fn inorder(&self) -> Vec<T> {
        match self {
            Avl::Empty => vec![],
            Avl::Node { left, value, right, .. } => {
                let mut result = left.inorder();
                result.push(value.clone());
                result.extend(right.inorder());
                result
            }
        }
    }

    pub fn from_iter(items: impl IntoIterator<Item = T>) -> Self {
        items.into_iter().fold(Avl::empty(), |tree, x| tree.insert(x))
    }
}

fn main() {
    let tree = Avl::build([7, 3, 9, 1, 5, 8, 10, 2]);
    println!("inorder: {:?}", tree.inorder());
    println!("height: {}", tree.height());

    // Sorted insertion stays balanced thanks to rotations
    let sorted_tree = Avl::build(1..=15);
    println!("sorted 1..=15 inorder: {:?}", sorted_tree.inorder());
    println!("sorted 1..=15 height: {}", sorted_tree.height());
}

/* Output:
   inorder: [1, 2, 3, 5, 7, 8, 9, 10]
   height: 4
   sorted 1..=15 inorder: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
   sorted 1..=15 height: 4
*/
type 'a avl = Empty | Node of 'a avl * 'a * 'a avl * int

let height = function Empty -> 0 | Node (_, _, _, h) -> h
let node l v r = Node (l, v, r, 1 + max (height l) (height r))
let balance t = match t with Empty -> 0 | Node (l,_,r,_) -> height l - height r

let rotate_right = function
  | Node (Node (ll, lv, lr, _), v, r, _) -> node (node ll lv lr) v r
  | t -> t

let rotate_left = function
  | Node (l, v, Node (rl, rv, rr, _), _) -> node l v (node rl rv rr)
  | t -> t

let rebalance t = match balance t with
  | b when b > 1 -> rotate_right t
  | b when b < -1 -> rotate_left t
  | _ -> t

let rec insert x = function
  | Empty -> node Empty x Empty
  | Node (l, v, r, _) ->
    if x < v then rebalance (node (insert x l) v r)
    else if x > v then rebalance (node l v (insert x r))
    else node l v r

let rec inorder = function
  | Empty -> [] | Node (l,v,r,_) -> inorder l @ [v] @ inorder r

let () =
  let t = List.fold_left (fun t x -> insert x t) Empty [7;3;9;1;5;8;10;2] in
  assert (inorder t = [1;2;3;5;7;8;9;10]);
  (* Balance check: height should be small *)
  assert (height t <= 4);
  print_endline "ok"

๐Ÿ“Š Detailed Comparison

OCaml vs Rust: AVL Tree โ€” Self-Balancing BST

Side-by-Side Code

OCaml

๐Ÿช Show OCaml equivalent
type 'a avl = Empty | Node of 'a avl * 'a * 'a avl * int

let height = function Empty -> 0 | Node (_, _, _, h) -> h
let node l v r = Node (l, v, r, 1 + max (height l) (height r))

let rotate_right = function
| Node (Node (ll, lv, lr, _), v, r, _) -> node (node ll lv lr) v r
| t -> t

let rec insert x = function
| Empty -> node Empty x Empty
| Node (l, v, r, _) ->
 if x < v then rebalance (node (insert x l) v r)
 else if x > v then rebalance (node l v (insert x r))
 else node l v r

Rust (idiomatic with named fields)

#[derive(Debug, Clone, PartialEq)]
pub enum Avl<T> {
 Empty,
 Node { left: Box<Avl<T>>, value: T, right: Box<Avl<T>>, height: i32 },
}

impl<T: Ord + Clone> Avl<T> {
 fn rotate_right(self) -> Self {
     match self {
         Avl::Node { left, value, right, .. } => match *left {
             Avl::Node { left: ll, value: lv, right: lr, .. } =>
                 Self::node(Self::node(*ll, lv, *lr), value, *right),
             _ => Self::node(*left, value, *right),
         },
         other => other,
     }
 }

 pub fn insert(&self, x: T) -> Self {
     match self {
         Avl::Empty => Self::node(Avl::Empty, x, Avl::Empty),
         Avl::Node { left, value, right, .. } => match x.cmp(value) {
             Ordering::Less => Self::node(left.insert(x), value.clone(), (**right).clone()).rebalance(),
             Ordering::Greater => Self::node((**left).clone(), value.clone(), right.insert(x)).rebalance(),
             Ordering::Equal => self.clone(),
         },
     }
 }
}

Type Signatures

ConceptOCamlRust
Tree type`'a avl``Avl<T>`
Height`'a avl -> int``fn height(&self) -> i32`
Insert`'a -> 'a avl -> 'a avl``fn insert(&self, x: T) -> Self`
Rotate`'a avl -> 'a avl``fn rotate_right(self) -> Self`
Balance factor`'a avl -> int``fn balance_factor(&self) -> i32`

Key Insights

1. Rotation = ownership transfer: OCaml creates new nodes from pattern-matched pieces; Rust moves ownership through `self` by value, making the restructuring zero-copy where possible 2. Two-level pattern matching: OCaml matches `Node(Node(ll,lv,lr,_), v, r, _)` in one arm; Rust needs nested `match` since it can't destructure two levels of `Box` at once 3. Named vs positional fields: OCaml's `Node of 'a avl 'a 'a avl * int` uses positional access; Rust's named struct fields (`left`, `value`, `right`, `height`) are self-documenting 4. Balance invariant verification: Both languages can express `is_balanced` as a recursive check; Rust's type system doesn't encode the invariant statically (both rely on runtime checks) 5. Clone cost transparency: Rust's `.clone()` on unchanged subtrees makes the persistence cost visible; OCaml shares references silently via GC

When to Use Each Style

Use AVL tree when: you need guaranteed O(log n) operations and care about worst-case performance Use standard BTreeMap when: you want a production-ready balanced tree โ€” Rust's stdlib provides one