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
- Sorted iteration in O(n): `inorder()` traversal yields elements in sorted order without any extra sorting step.
- Balanced search in O(log n): Unlike `HashMap`, AVL trees maintain order โ you can find min/max, predecessor, and successor efficiently.
- Foundation for interval trees, segment trees: AVL is the starting point for augmented BSTs that answer range queries.
Key Differences
| Concept | OCaml | Rust | |
|---|---|---|---|
| Node type | `type 'a avl = Empty \ | Node of 'a avl 'a 'a avl * int` | `enum Avl<T> { Empty, Node { left, value, right, height } }` |
| Named fields | Positional (tuple in variant) | Named struct fields in enum variant | |
| Rotation | Pattern match, create new nodes (GC handles old) | Consume `self` by move, reconstruct โ borrow checker enforces | |
| Cloning | Implicit 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 rRust (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
| Concept | OCaml | Rust |
|---|---|---|
| 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