Tries, Radix Trees & Bitwise 0-1 Structures
Standard prefix Trie node maps, prefix query matching, Radix / Patricia compressed edges, and Bitwise 0-1 Trie for maximum XOR queries.
Tries shift retrieval complexity from dataset cardinality to key length , structuring dynamic dictionaries into character-by-character digital search paths. From autocomplete engines and compressed radix routing to bitwise XOR maximization and Aho-Corasick multi-pattern scanning, string trees establish the foundation for high-performance lexical processing.
1. Executive Summary & Learning Objectives
#First described by René de la Briandais in 1959 and named by Edward Fredkin in 1960 from retrieval, a Trie (or prefix tree) is an ordered tree data structure where keys are usually strings. Unlike standard binary search trees where nodes store full keys, no node in a Trie stores its entire key; instead, its position within the tree defines the prefix it represents.
By the end of this chapter, you will be able to:
- Implement pointer-safe Standard Trie insertion, prefix search, and bottom-up memory-reclaiming deletion in time.
- Evaluate memory consumption patterns between flat array pointer tables () and dynamic hash maps across sparse and dense alphabets.
- Contrast standard Tries with Compressed Radix Trees (PATRICIA tries), proving why edge compaction bounds total nodes to at most .
- Construct a Bitwise 0-1 Trie to solve the Maximum XOR Pair and Subarray problem in strictly linear time.
- Trace the architectural foundations of Suffix Trees (Ukkonen's construction) and Aho-Corasick multi-pattern automaton failure links.
2. Standard Trie Architecture & Invariants
#A Standard Trie satisfies four core structural properties:
- Root Representation: The root node corresponds to the empty prefix
"". - Edge Character Semantics: Each outgoing edge corresponds to exactly one character from alphabet .
- Common Prefix Sharing: All descendants of a given node share the identical string prefix defined by the path from the root.
- Terminal Marker: A boolean flag (
isEndOfWord) or stored terminal value distinguishes intermediate prefix junctions from fully completed words.
Prefix Tree Topology
#The table below illustrates a Trie storing the set of words: ["app", "apple", "apply", "bat", "ball"]:
| Node Path | Stored Character | Active Prefix | isEndOfWord | Child Pointers Present | Structural Classification |
|---|---|---|---|---|---|
| Root | "" | "" | false | 'a', 'b' | Root Junction |
| Root a | 'a' | "a" | false | 'p' | Shared Prefix Node |
| Root a p | 'p' | "ap" | false | 'p' | Shared Prefix Node |
| Root a p p | 'p' | "app" | true | 'l' | Terminal Word ("app") & Prefix Junction |
| Root a p p l | 'l' | "appl" | false | 'e', 'y' | Branching Node |
| Root a p p l e | 'e' | "apple" | true | null | Terminal Word ("apple") / Leaf |
| Root a p p l y | 'y' | "apply" | true | null | Terminal Word ("apply") / Leaf |
| Root b | 'b' | "b" | false | 'a' | Shared Prefix Node |
| Root b a | 'a' | "ba" | false | 't', 'l' | Branching Node |
| Root b a t | 't' | "bat" | true | null | Terminal Word ("bat") / Leaf |
| Root b a l | 'l' | "bal" | false | 'l' | Single-Child Node |
| Root b a l l | 'l' | "ball" | true | null | Terminal Word ("ball") / Leaf |
3. Complete Implementation: Standard Trie
#export class TrieNode {
children: Map<string, TrieNode> = new Map();
isEndOfWord: boolean = false;
}
export class Trie {
root: TrieNode = new TrieNode();
public insert(word: string): void {
let curr = this.root;
for (const char of word) {
if (!curr.children.has(char)) {
curr.children.set(char, new TrieNode());
}
curr = curr.children.get(char)!;
}
curr.isEndOfWord = true;
}
public search(word: string): boolean {
let curr = this.root;
for (const char of word) {
if (!curr.children.has(char)) return false;
curr = curr.children.get(char)!;
}
return curr.isEndOfWord;
}
public startsWith(prefix: string): boolean {
let curr = this.root;
for (const char of prefix) {
if (!curr.children.has(char)) return false;
curr = curr.children.get(char)!;
}
return true;
}
public delete(word: string): boolean {
return this.deleteHelper(this.root, word, 0);
}
private deleteHelper(curr: TrieNode, word: string, depth: number): boolean {
if (depth === word.length) {
if (!curr.isEndOfWord) return false; // Word not present
curr.isEndOfWord = false;
// If node has no other branches, signal caller to prune it
return curr.children.size === 0;
}
const char = word[depth];
const child = curr.children.get(char);
if (!child) return false;
const shouldPruneChild = this.deleteHelper(child, word, depth + 1);
if (shouldPruneChild) {
curr.children.delete(char);
// Prune curr if it is not an end-of-word and has no other children
return !curr.isEndOfWord && curr.children.size === 0;
}
return false;
}
}4. Compressed Tries: Radix Tree & PATRICIA Trie
#In a Standard Trie, non-branching chains representing long unique words waste considerable pointer overhead. For example, storing "antidisestablishmentarianism" requires 28 nodes and pointer references.
A Radix Tree (formalized by Donald R. Morrison in 1968 as PATRICIA — Practical Algorithm To Retrieve Information Coded in Alphanumeric) compresses every linear path of single-child nodes into a single edge labeled with an edge substring:
| Architectural Metric | Standard Trie | Radix Tree (Compressed Trie) |
|---|---|---|
| Edge Label | Exactly 1 character () | Variable-length substring () |
| Node Count Bound | (Sum of all character lengths) | nodes strictly (for stored words) |
| Branching Factor | Array of size per node | Dynamic list or small array of edges |
| Internal Node Degree | Can have degree 1 (linear chains) | Degree strictly guaranteed (except root) |
| Production Use Cases | Autocomplete, Spellcheckers | Linux Kernel Page Cache, IP routing tables (CIDR) |
5. Bitwise 0-1 Trie & Maximum XOR Subarray
#A Bitwise 0-1 Trie treats 32-bit integers as binary strings indexed from Most Significant Bit (MSB, bit 31) down to Least Significant Bit (LSB, bit 0):
- Alphabet size (
0= left child,1= right child). - Every branch has fixed maximum depth .
The Maximum XOR Pair Algorithm ()
#Given an array of integers , find .
Greedy Principle
To maximize , we evaluate bits from MSB (bit 31) down to 0:
- If the -th bit of is , we greedily search for a matching integer whose -th bit is the complement , because .
- If a child branch with bit exists in the 0-1 Trie, traverse it and set the -th bit of the answer to .
- If it does not exist, take the matching branch ().
export class BinaryTrieNode {
children: [BinaryTrieNode | null, BinaryTrieNode | null] = [null, null];
}
export class BinaryTrie {
root: BinaryTrieNode = new BinaryTrieNode();
public insert(num: number): void {
let curr = this.root;
for (let i = 31; i >= 0; i--) {
const bit = (num >>> i) & 1;
if (curr.children[bit] === null) {
curr.children[bit] = new BinaryTrieNode();
}
curr = curr.children[bit]!;
}
}
public findMaxXOR(num: number): number {
let curr = this.root;
let maxXor = 0;
for (let i = 31; i >= 0; i--) {
const bit = (num >>> i) & 1;
const toggledBit = 1 - bit;
if (curr.children[toggledBit] !== null) {
maxXor = maxXor | (1 << i);
curr = curr.children[toggledBit]!;
} else {
curr = curr.children[bit]!;
}
}
return maxXor;
}
}6. Step-by-Step Dry Run State Trace: Maximum XOR Query
#Consider a 3-bit binary Trie populated with integers :
Query: findMaxXOR(5) where (Evaluating bits 2 down to 0):
| Bit Position | Target Bit | Desired Complement | Branch Available in Trie? | Action Taken | Accumulated XOR Value |
|---|---|---|---|---|---|
| Bit 2 () | Yes (Nodes and start with ) | Follow branch . Set bit 2 of result to . | |||
| Bit 1 () | Yes (Both and have bit 1 as ) | Follow branch . Set bit 1 of result to . | |||
| Bit 0 () | Yes (Node has bit 0 as ) | Follow branch (matching ). Set bit 0 of result to . |
Verification: . The algorithm dynamically found the optimal partner in bit evaluations!
7. Advanced String Structures: Suffix Trees & Aho-Corasick Automata
#Suffix Trees & Ukkonen's Linear-Time Algorithm
#A Suffix Tree is a compacted Trie containing all suffixes of string terminated by sentinel $:
- Ukkonen's Algorithm (1995) builds a suffix tree online in strictly linear time and space.
- Capabilities:
- Substring search for pattern in time, completely independent of text length .
- Longest Repeated Substring in time.
- Longest Common Substring between two strings in time.
Aho-Corasick Multi-Pattern Automaton
#Invented by Alfred Aho and Margaret Corasick in 1975, this automaton searches for a dictionary of patterns simultaneously:
- Augments a Trie with Failure Transitions (computed via BFS, mirroring the KMP prefix function) and Dictionary Output Links.
- Traverses the input text in a single pass without backtracking: running in strictly time.
- Standard engine in network intrusion detection (Snort) and antivirus signature matching (ClamAV).
8. Asymptotic Complexity Matrix
#| Data Structure | Lookup Time | Insertion Time | Deletion Time | Memory Bound |
|---|---|---|---|---|
| Standard Trie | ||||
| Radix Tree (PATRICIA) | nodes strictly | |||
| Bitwise 0-1 Trie | ( or ) | |||
| Suffix Tree | (Ukkonen) | — | ||
Hash Table (Set<string>) | average | average | average | (No prefix sharing) |
9. Common Traps, Edge Cases & Implementation Pitfalls
#- Memory Blowup with Fixed-Size Child Arrays:
- Declaring
children: TrieNode[26]consumes 26 pointer references per node even for leaves. For sparse trees, use a dynamicMap<char, TrieNode>or compressed Radix Tree.
- Declaring
- Deleting Shared Prefixes:
- When deleting a word like
"app"when"apple"exists, clearing children pointers corrupts"apple". Only toggleisEndOfWord = false. Only delete nodes whenchildren.size === 0andisEndOfWord === false.
- When deleting a word like
- Signed Bit Shift in 0-1 Tries:
- In JavaScript and TypeScript, using
num >> iexecutes a sign-propagating shift. Always use the unsigned right shift operatornum >>> ito prevent negative MSB sign extension bugs.
- In JavaScript and TypeScript, using
10. References & Academic Attribution
#- Fredkin, E. (1960). Trie memory. Communications of the ACM, 3(9), 490–499.
- Morrison, D. R. (1968). PATRICIA—Practical Algorithm To Retrieve Information Coded in Alphanumeric. Journal of the ACM (JACM), 15(4), 514–534.
- Aho, A. V., & Corasick, M. J. (1975). Efficient string matching: an aid to bibliographic search. Communications of the ACM, 18(6), 333–340.
- Ukkonen, E. (1995). On-line construction of suffix trees. Algorithmica, 14(3), 249–260.