972 Persistent Tree
Tutorial
The Problem
Implement a persistent binary search tree where insert and delete return new tree versions sharing unchanged subtrees via Rc. Only the nodes along the modified path are newly allocated; nodes not on the insertion path are shared between old and new versions. This is the Rust analog of OCaml's immutable BST.
🎯 Learning Outcomes
enum Bst<T> { Empty, Node(Rc<Bst<T>>, T, Rc<Bst<T>>) } with Rc for child sharinginsert(&self, x: T) -> Bst<T> that allocates new nodes only on the path to the insertion point and Rc::clones the unchanged subtreemember(&self, x: &T) -> bool as a simple recursive searchmin_val(&self) -> Option<&T> by descending left until EmptyBst itself is Clone (not wrapped in Rc) — callers hold Bst<T> directly; Rc is used for internal child referencesCode Example
#![allow(clippy::all)]
// 972: Persistent Binary Search Tree
// Functional update: insert/delete return new Rc-shared trees
// Shared nodes between versions via Rc<BstNode<T>>
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq)]
pub enum Bst<T> {
Empty,
Node(Rc<Bst<T>>, T, Rc<Bst<T>>),
}
impl<T: Ord + Clone> Bst<T> {
pub fn empty() -> Self {
Bst::Empty
}
/// Insert returns a new tree sharing unchanged subtrees
pub fn insert(&self, x: T) -> Self {
match self {
Bst::Empty => Bst::Node(Rc::new(Bst::Empty), x, Rc::new(Bst::Empty)),
Bst::Node(l, v, r) => {
if x < *v {
Bst::Node(Rc::new(l.insert(x)), v.clone(), Rc::clone(r))
} else if x > *v {
Bst::Node(Rc::clone(l), v.clone(), Rc::new(r.insert(x)))
} else {
self.clone() // duplicate: return same (Rc-shared)
}
}
}
}
pub fn member(&self, x: &T) -> bool {
match self {
Bst::Empty => false,
Bst::Node(l, v, r) => {
if x == v {
true
} else if x < v {
l.member(x)
} else {
r.member(x)
}
}
}
}
pub fn min_val(&self) -> Option<&T> {
match self {
Bst::Empty => None,
Bst::Node(l, v, _) => {
if matches!(l.as_ref(), Bst::Empty) {
Some(v)
} else {
l.min_val()
}
}
}
}
/// Delete returns a new tree, old tree unchanged
pub fn delete(&self, x: &T) -> Self {
match self {
Bst::Empty => Bst::Empty,
Bst::Node(l, v, r) => {
if x < v {
Bst::Node(Rc::new(l.delete(x)), v.clone(), Rc::clone(r))
} else if x > v {
Bst::Node(Rc::clone(l), v.clone(), Rc::new(r.delete(x)))
} else {
// Found node: replace with min of right subtree
match r.min_val() {
None => (**l).clone(), // no right subtree
Some(m) => {
let m = m.clone();
let new_r = r.delete(&m);
Bst::Node(Rc::clone(l), m, Rc::new(new_r))
}
}
}
}
}
}
pub fn to_vec(&self) -> Vec<T> {
match self {
Bst::Empty => vec![],
Bst::Node(l, v, r) => {
let mut result = l.to_vec();
result.push(v.clone());
result.extend(r.to_vec());
result
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tree() -> Bst<i32> {
Bst::empty()
.insert(5)
.insert(3)
.insert(7)
.insert(1)
.insert(4)
}
#[test]
fn test_insert_sorted() {
let t = make_tree();
assert_eq!(t.to_vec(), vec![1, 3, 4, 5, 7]);
}
#[test]
fn test_persistence() {
let t0 = Bst::empty().insert(5).insert(3).insert(7).insert(1);
let t1 = t0.insert(4);
// t0 is unchanged
assert_eq!(t0.to_vec(), vec![1, 3, 5, 7]);
assert_eq!(t1.to_vec(), vec![1, 3, 4, 5, 7]);
}
#[test]
fn test_member() {
let t = make_tree();
assert!(t.member(&4));
assert!(t.member(&5));
assert!(!t.member(&2));
assert!(!t.member(&6));
}
#[test]
fn test_delete_leaf() {
let t = make_tree();
let t2 = t.delete(&1);
assert_eq!(t2.to_vec(), vec![3, 4, 5, 7]);
assert_eq!(t.to_vec(), vec![1, 3, 4, 5, 7]); // unchanged
}
#[test]
fn test_delete_internal() {
let t = make_tree();
let t2 = t.delete(&3);
assert_eq!(t2.to_vec(), vec![1, 4, 5, 7]);
let t3 = t.delete(&5); // delete root
assert_eq!(t3.to_vec(), vec![1, 3, 4, 7]);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Sharing mechanism | Rc::clone — explicit O(1) | GC — implicit |
T: Clone bound | Required to copy v into new nodes | No bound; values are copied by the runtime |
| Duplicate insert | self.clone() | Return t (same pointer via GC) |
| Node allocation | Rc::new(...) | Constructor (GC managed) |
Persistent BSTs are the foundation of OCaml's Map and Set modules. Each Map.add or Set.add returns a new tree sharing unchanged branches — O(log n) allocation, O(log n) lookup.
OCaml Approach
type 'a bst = Empty | Node of 'a bst * 'a * 'a bst
let rec insert x = function
| Empty -> Node (Empty, x, Empty)
| Node (l, v, r) as t ->
if x < v then Node (insert x l, v, r) (* r is shared (GC) *)
else if x > v then Node (l, v, insert x r) (* l is shared *)
else t (* duplicate: return same node (GC shares it) *)
let rec member x = function
| Empty -> false
| Node (l, v, r) ->
if x = v then true
else if x < v then member x l
else member x r
OCaml's GC makes structural sharing automatic. Node (insert x l, v, r) shares r without explicit Rc::clone because the GC tracks references. The Rust Rc::clone call is the explicit acknowledgment of the same operation.
Full Source
#![allow(clippy::all)]
// 972: Persistent Binary Search Tree
// Functional update: insert/delete return new Rc-shared trees
// Shared nodes between versions via Rc<BstNode<T>>
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq)]
pub enum Bst<T> {
Empty,
Node(Rc<Bst<T>>, T, Rc<Bst<T>>),
}
impl<T: Ord + Clone> Bst<T> {
pub fn empty() -> Self {
Bst::Empty
}
/// Insert returns a new tree sharing unchanged subtrees
pub fn insert(&self, x: T) -> Self {
match self {
Bst::Empty => Bst::Node(Rc::new(Bst::Empty), x, Rc::new(Bst::Empty)),
Bst::Node(l, v, r) => {
if x < *v {
Bst::Node(Rc::new(l.insert(x)), v.clone(), Rc::clone(r))
} else if x > *v {
Bst::Node(Rc::clone(l), v.clone(), Rc::new(r.insert(x)))
} else {
self.clone() // duplicate: return same (Rc-shared)
}
}
}
}
pub fn member(&self, x: &T) -> bool {
match self {
Bst::Empty => false,
Bst::Node(l, v, r) => {
if x == v {
true
} else if x < v {
l.member(x)
} else {
r.member(x)
}
}
}
}
pub fn min_val(&self) -> Option<&T> {
match self {
Bst::Empty => None,
Bst::Node(l, v, _) => {
if matches!(l.as_ref(), Bst::Empty) {
Some(v)
} else {
l.min_val()
}
}
}
}
/// Delete returns a new tree, old tree unchanged
pub fn delete(&self, x: &T) -> Self {
match self {
Bst::Empty => Bst::Empty,
Bst::Node(l, v, r) => {
if x < v {
Bst::Node(Rc::new(l.delete(x)), v.clone(), Rc::clone(r))
} else if x > v {
Bst::Node(Rc::clone(l), v.clone(), Rc::new(r.delete(x)))
} else {
// Found node: replace with min of right subtree
match r.min_val() {
None => (**l).clone(), // no right subtree
Some(m) => {
let m = m.clone();
let new_r = r.delete(&m);
Bst::Node(Rc::clone(l), m, Rc::new(new_r))
}
}
}
}
}
}
pub fn to_vec(&self) -> Vec<T> {
match self {
Bst::Empty => vec![],
Bst::Node(l, v, r) => {
let mut result = l.to_vec();
result.push(v.clone());
result.extend(r.to_vec());
result
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tree() -> Bst<i32> {
Bst::empty()
.insert(5)
.insert(3)
.insert(7)
.insert(1)
.insert(4)
}
#[test]
fn test_insert_sorted() {
let t = make_tree();
assert_eq!(t.to_vec(), vec![1, 3, 4, 5, 7]);
}
#[test]
fn test_persistence() {
let t0 = Bst::empty().insert(5).insert(3).insert(7).insert(1);
let t1 = t0.insert(4);
// t0 is unchanged
assert_eq!(t0.to_vec(), vec![1, 3, 5, 7]);
assert_eq!(t1.to_vec(), vec![1, 3, 4, 5, 7]);
}
#[test]
fn test_member() {
let t = make_tree();
assert!(t.member(&4));
assert!(t.member(&5));
assert!(!t.member(&2));
assert!(!t.member(&6));
}
#[test]
fn test_delete_leaf() {
let t = make_tree();
let t2 = t.delete(&1);
assert_eq!(t2.to_vec(), vec![3, 4, 5, 7]);
assert_eq!(t.to_vec(), vec![1, 3, 4, 5, 7]); // unchanged
}
#[test]
fn test_delete_internal() {
let t = make_tree();
let t2 = t.delete(&3);
assert_eq!(t2.to_vec(), vec![1, 4, 5, 7]);
let t3 = t.delete(&5); // delete root
assert_eq!(t3.to_vec(), vec![1, 3, 4, 7]);
}
}#[cfg(test)]
mod tests {
use super::*;
fn make_tree() -> Bst<i32> {
Bst::empty()
.insert(5)
.insert(3)
.insert(7)
.insert(1)
.insert(4)
}
#[test]
fn test_insert_sorted() {
let t = make_tree();
assert_eq!(t.to_vec(), vec![1, 3, 4, 5, 7]);
}
#[test]
fn test_persistence() {
let t0 = Bst::empty().insert(5).insert(3).insert(7).insert(1);
let t1 = t0.insert(4);
// t0 is unchanged
assert_eq!(t0.to_vec(), vec![1, 3, 5, 7]);
assert_eq!(t1.to_vec(), vec![1, 3, 4, 5, 7]);
}
#[test]
fn test_member() {
let t = make_tree();
assert!(t.member(&4));
assert!(t.member(&5));
assert!(!t.member(&2));
assert!(!t.member(&6));
}
#[test]
fn test_delete_leaf() {
let t = make_tree();
let t2 = t.delete(&1);
assert_eq!(t2.to_vec(), vec![3, 4, 5, 7]);
assert_eq!(t.to_vec(), vec![1, 3, 4, 5, 7]); // unchanged
}
#[test]
fn test_delete_internal() {
let t = make_tree();
let t2 = t.delete(&3);
assert_eq!(t2.to_vec(), vec![1, 4, 5, 7]);
let t3 = t.delete(&5); // delete root
assert_eq!(t3.to_vec(), vec![1, 3, 4, 7]);
}
}
Deep Comparison
Persistent BST — Comparison
Core Insight
A persistent BST creates only O(log n) new nodes per operation — the path from root to the changed node. All unchanged subtrees are shared between the old and new versions. OCaml's GC makes this transparent; Rust uses Rc::clone on unchanged subtrees (O(1) pointer copy) and Rc::new(subtree.insert(x)) for the new path.
OCaml Approach
type 'a bst = Empty | Node of 'a bst * 'a * 'a bst — clean recursive typeinsert l x creates new nodes on the path; unchanged branches reused by GCNode (insert l x, v, r) — r is shared (pointer, not copy)delete: min_val r to find in-order successor for node removalRust Approach
enum Bst<T> { Empty, Node(Rc<Bst<T>>, T, Rc<Bst<T>>) } — Rc wraps childrenRc::clone(r) — O(1) pointer copy, shares unchanged subtreeRc::new(l.insert(x)) — allocates new node on changed path only(**l).clone() — deref Rc, then clone the Bst (needed for delete leaf case)T: Ord + Clone — needed for comparison and copying values into new nodesComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Sharing mechanism | GC implicit | Rc::clone explicit |
| New node on path | Node (insert l x, v, r) | Node(Rc::new(l.insert(x)), v.clone(), Rc::clone(r)) |
| Unchanged branch | Reused automatically | Rc::clone(branch) (O(1)) |
| Node lifetime | GC | Rc drops when last ref gone |
| Trait bounds | None (structural) | T: Ord + Clone |
| Delete successor | min_val r; delete r m | r.min_val(); r.delete(&m) |
| New nodes per op | O(log n) | O(log n) |
Exercises
delete(&self, x: &T) -> Bst<T> — handle leaf, one-child, and two-child cases.to_sorted_vec(&self) -> Vec<T> via in-order traversal.size(&self) -> usize recursively and cache it in Node for O(1) lookup.insert.Rc::ptr_eq on unchanged subtrees.