1. Introduction & Motivation
Every data structure covered so far in this series is ephemeral: it has exactly one current state, and any update — insert, delete, set a value — mutates that state in place. Once you call insert(root, 5) on a normal binary search tree, the tree as it existed one line earlier is gone. If you needed that earlier shape for anything, you should have copied it first, and a full copy costs $O(n)$.
A persistent data structure removes that tradeoff. Every update produces a brand-new version of the structure, and every version that ever existed — including the very first one — remains fully valid and queryable forever. Calling insert on a persistent BST does not destroy the old root; it returns a new root, and both roots keep working correctly, independently, for as long as your program holds a reference to either one.
This lesson is about how that is possible without paying $O(n)$ per update. The technique is called path copying, and it applies to any structure built out of nodes connected by pointers in a tree shape: binary search trees, treaps, segment trees, and tries all qualify, and the same idea is the reason purely functional programming languages can treat "update a data structure" as an ordinary, cheap expression rather than a dangerous in-place mutation.
2. Prerequisites
This lesson builds directly on two earlier pieces of the curriculum and assumes you are comfortable with both:
- Standard (ephemeral) BST insert — see BST Operations in the BST/AVL/Red-Black lesson (Part 10). Persistent BST insert is the same recursive descent, with one change: instead of mutating a child pointer, it returns a new node.
- Standard (ephemeral) Segment Tree — see the Segment Tree section of the Advanced Structures lesson, which covers the recursive build/update/query pattern this lesson assumes and only modifies at the node-allocation level.
Neither prerequisite is re-derived here. If the ordinary recursive insert or the ordinary build/update/query pattern is unfamiliar, read those sections first — everything below is the same recursion with a persistence twist layered on top.
3. Core Insight: Path Copying
Take any tree-shaped structure and any update that changes a single node deep inside it. The naive way to persist that update is to deep-copy the entire structure before mutating the copy — correct, but $O(n)$ in both time and space, since it duplicates every node even though only one node actually changed.
Because a balanced BST, treap, or segment tree has height $O(\log n)$, path copying clones $O(\log n)$ new nodes per update and shares the rest — giving $O(\log n)$ extra time and extra space per update, not $O(n)$. The old root is never touched, so it, and everything reachable from it, is exactly as valid after the update as before it.
4. A Concrete Example
Inserting 4 into a 3-node persistent BST
Version A already contains {5, 3, 8}: node 5 is the root, node 3 is its left child, node 8 is its right child; neither 3 nor 8 has any children. Inserting 4 walks the usual BST path: $4 \lt 5$, go left to node 3; $4 > 3$, go right — node 3 has no right child, so a brand-new leaf node 4 is attached there. That path is 5 → 3 → (new) 4.
Path copying clones every node on that path except the freshly created leaf, and shares every node off that path unchanged:
| Node | Role in Version A | What Version B does with it |
|---|---|---|
| 5 (root) | original root, left=3, right=8 | cloned — new root 5′, left points at new node 3′, right still points at the original node 8 |
| 3 | left child of 5, no children | cloned — new node 3′, right now points at new node 4, left is still empty |
| 8 | right child of 5 | shared — the exact same node object; Version B's root points at it directly, no copy made |
| 4 | did not exist | new — freshly allocated leaf, right child of node 3′ |
Version B allocates 3 new nodes (5′, 3′, 4) to represent a 4-node tree — not 4, because node 8 is reused. Version A's root still points at the original, untouched node 5, whose left child is still the original node 3 with no right child, so contains(rootA, 4) correctly reports false while contains(rootB, 4) reports true.
5. Persistent BST
Applying the recipe from Section 3 to a BST gives an insert that returns a new root instead of mutating in place. Each language below keeps a growing list of historical roots and then queries an old root after a later insert has happened, to demonstrate that the old version is unaffected.
Persistent Treap
A treap keeps its BST-ordering property alongside a randomly assigned heap priority, and its two workhorse operations, split and merge, are already written as recursive functions that rebuild a small number of nodes on their way back up the call stack — see the Treap, Splay Tree & Cartesian Tree lesson for the full derivation of split/merge, which is not repeated here. That shape is exactly what path copying wants: instead of mutating left/right pointers while unwinding the recursion, allocate a new node with the recursive result plugged in, and return it. No new algorithmic idea is required, only the same node-return-instead-of-mutate substitution already used for the BST above:
# Ephemeral merge mutates: left.right = merge(left.right, right); return left
# Persistent merge instead allocates a new node on the way back up:
def merge(left, right):
if left is None: return right
if right is None: return left
if left.priority > right.priority:
return Node(left.val, left.priority, left.left, merge(left.right, right))
return Node(right.val, right.priority, merge(left, right.left), right.right)
Because split and merge together implement treap insert and delete, persisting those two functions persists the whole treap for free.
6. Persistent Segment Tree
The base recursive build/update/query pattern is not re-derived here; see the Segment Tree Implementation section of the Advanced Structures lesson for the ephemeral version this extends.
The classic competitive-programming use of this structure is answering "how many elements $\le x$ appear in the subarray $[l, r]$" across many historical versions at once: build version $i$ as a segment tree over the value domain after inserting the first $i$ array elements, so roots[i] represents "counts so far, as of position $i$". Querying roots[r] and roots[l-1] for the range of values $\le x$ and subtracting the two counts — the same telescoping trick as a 1D prefix sum, just applied to persistent-tree roots instead of a running total — answers the question in $O(\log(\text{value range}))$ per query. This "persistent segment tree of prefix counts" pattern is exactly what the Hard exercise below asks you to build.
Persistent Trie
Tries are trees too, so nothing new is required: see the Trie Variants lesson for the base trie mechanics (or the Advanced Structures lesson's Trie section if that page is not yet part of your reading list). Inserting a key clones only the $O(\text{key length})$ nodes along the path spelled out by that key's characters, and shares every other branch of the trie with the previous version:
# Ephemeral: node.children[ch] = child; recurse; mutate in place.
# Persistent: clone the node with an updated children map instead:
def insert(node, key, i=0):
node = node.clone() if node else TrieNode()
if i == len(key):
node.is_end = True
return node
ch = key[i]
node.children = dict(node.children) # shallow copy of the map
node.children[ch] = insert(node.children.get(ch), key, i + 1)
return node
This is the identical clone-the-path, share-the-rest pattern already shown twice above for the BST and the segment tree — a full re-implementation would not teach anything new.
7. Finger Trees
Data.Sequence in Haskell and similar persistent deque/sequence types in other functional languages. It is built from nested pairs — a shallow "finger" of a few elements at each end, wrapping a recursively-defined finger tree of larger 2-3 node groups in the middle — so that both ends are cheap to reach while the recursive middle amortizes the cost of deep structural changes. Its natural implementation relies on algebraic data types, lazy evaluation, and pattern matching in a way that does not translate cleanly into Python, C++, or Java's typical imperative style, so it is included here only so the vocabulary is recognizable, not as a fourth structure to build.8. Complexity Analysis
Per update, for persistent BST, treap, segment tree, and trie via path copying:
$$\text{Time} = O(\log n), \qquad \text{Extra space} = O(\log n)$$
compared to $O(n)$ time and $O(n)$ extra space if each update instead deep-copied the entire structure before mutating the copy. Query on any historical root costs exactly what it costs on the equivalent ephemeral structure — $O(\log n)$ for a balanced BST/treap/segment tree, $O(\text{key length})$ for a trie — because query never mutates anything and is identical code to the non-persistent version.
After $m$ updates to an initial structure of $n$ nodes, total memory across all versions is $O(n + m\log n)$, not $O(mn)$: each version pays only for the $O(\log n)$ nodes it introduced, and every version continues to share the original $n$ nodes it never touched.
9. Applications & Alternatives
Where persistence changes the design
- Version control systems: a commit is conceptually a new, complete snapshot of the whole tree, made cheap only because unchanged files and directories are shared with the parent commit.
- Functional programming language runtimes: immutable maps, sets, and vectors in languages like Clojure and Haskell are persistent trees under the hood, which is what lets "updating" a value be a safe, referentially-transparent expression instead of a mutation.
- "Query the array as it was at time T" problems: a recurring competitive-programming pattern where a sequence of updates is applied to an array and queries must be answered against an arbitrary earlier version — solved directly with a persistent segment tree keyed by version number.
- Undo/redo systems: each edit produces a new version; undo is simply switching the "current" pointer back to an older root, no reconstruction required.
The alternative to path copying is fat nodes or path copying with node-level versioning (storing a small history of values per node instead of cloning whole nodes), which can reduce memory further for update-heavy workloads at the cost of a more complex query. For most applications, plain path copying as shown above is simpler to implement correctly and is the default choice.
10. Exercises & Mastery
Easy
- Given the sequence of inserts
7, 3, 9, 1, 5into an initially empty persistent BST, state, for each insert, exactly which nodes are newly cloned and which nodes (if any) are shared with the previous version. - For the same sequence, state how many total node objects exist after all five inserts, and compare that to how many would exist if every insert instead deep-copied the whole tree.
Medium
- Given the persistent segment tree's
buildandqueryfunctions from Section 6, implementupdateyourself before looking at the solution, and verify that querying an old root after your update still returns the pre-update values. - Extend the persistent BST in Section 5 with a persistent
deletethat also returns a new root, reasoning about which nodes on the deletion path must be cloned.
Hard
- Using a persistent segment tree of prefix counts (Section 6), implement k-th smallest element in a range query: given an array and queries of the form "what is the k-th smallest value in $[l, r]$", build one version per prefix and binary-search over the value domain inside each query, using the version-subtraction trick described in Section 6. Warm up first with the plain prefix sums & difference arrays lesson if the subtract-two-prefixes idea itself is unfamiliar — that lesson is not re-derived here.
Review rhythm: tomorrow, redraw the worked example from Section 4 from memory, labeling cloned vs. shared nodes; next week, implement the persistent BST and persistent segment tree in a language you did not use here; several weeks later, find the persistence idea inside a tool you already use (a version control system or an undo stack) and explain it in those terms without looking back at this lesson.
11. Summary Cheat Sheet
- Ephemeral structures have one current state; persistent structures keep every past version fully queryable.
- Path copying: clone only the $O(\log n)$ nodes from the root to the changed node; share every other subtree by reference with the previous version.
- The old root, and everything reachable from it, is never mutated — that is what guarantees old versions stay correct forever.
- Persistent BST insert and persistent segment tree update both return a new root; keep a list of historical roots to query any version.
- Treap split/merge and trie insert persist for free once they are written as "return a new node" instead of "mutate and return void".
- Finger trees are a persistent sequence structure used in functional languages; conceptual only here, since their natural form is not imperative.
- Cost per update: $O(\log n)$ time and space, versus $O(n)$ for a naive full deep copy.
- Classic application: a persistent segment tree of prefix counts answers range rank/k-th-smallest queries across historical array versions.