children = {} (hash-map node) or children = [None]*26 (fixed-array node) at each trie level.| Operation | Time | Space | Notes |
|---|---|---|---|
insert(word) | O(m) | O(m) new nodes worst case | m = word length; shared prefixes reuse existing nodes |
search(word) | O(m) | O(1) | Returns true only if is_end flag is set |
starts_with(prefix) | O(p) | O(1) | p = prefix length; checks node existence only |
delete(word) | O(m) | O(1) freed per unique suffix node | Must check children before removing nodes bottom-up |
autocomplete(prefix, k) | O(p + n) | O(n) | p = prefix length, n = nodes in subtree; DFS/BFS to collect k results |
longest_prefix(text) | O(m) | O(1) | Walk trie, track last is_end node seen |
wildcard_search(pattern) | O(Σw × m) | O(m) stack | w = number of wildcards; backtracking on '.' or '?' |
count_prefix(prefix) | O(p) | O(1) | Requires augmented count field at each node |
| XOR maximum | O(B) | O(N×B) | B = bit width; binary trie for XOR queries |
| Compressed trie (radix) | O(m) | ~O(N) total | Collapses single-child chains; fewer nodes, same time |
START
|-- Need prefix-based operations (autocomplete, startsWith, longest prefix)?
| |-- YES --> Use a Trie (this unit)
| +-- NO |
|-- Need only exact-match lookup?
| |-- YES --> Use a HashMap / HashSet (O(1) average)
| +-- NO |
|-- Need sorted iteration of keys?
| |-- YES --> Use a sorted array + binary search, or balanced BST
| +-- NO |
|-- Keys are integers and you need XOR-max queries?
| |-- YES --> Use a binary trie (bitwise trie)
| +-- NO |
|-- Memory is severely constrained and keys share few prefixes?
| |-- YES --> Use a HashMap; tries waste memory on sparse trees
| +-- NO |
+-- DEFAULT --> Trie with hash-map children is a safe general choice
Each node holds a mapping from character to child node and a boolean flag indicating end-of-word. [src1]
class TrieNode:
def __init__(self):
self.children = {} # char -> TrieNode
self.is_end = False # marks complete word
Verify: node = TrieNode(); assert node.children == {} and node.is_end == False
Walk the trie character by character, creating nodes as needed, and mark the final node. [src1]
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_end = True
Verify: t = Trie(); t.insert("apple"); assert "a" in t.root.children
Search requires is_end == True at the final node; starts_with only requires the path exists. [src4]
def search(self, word: str) -> bool:
node = self._find_node(word)
return node is not None and node.is_end
def starts_with(self, prefix: str) -> bool:
return self._find_node(prefix) is not None
def _find_node(self, prefix: str):
node = self.root
for ch in prefix:
if ch not in node.children:
return None
node = node.children[ch]
return node
Verify: t.insert("apple"); assert t.search("apple") and not t.search("app") and t.starts_with("app")
Bottom-up recursive removal: only delete nodes that are not shared with other words. [src5]
def delete(self, word: str) -> bool:
def _delete(node, word, depth):
if depth == len(word):
if not node.is_end:
return False
node.is_end = False
return len(node.children) == 0
ch = word[depth]
if ch not in node.children:
return False
should_delete = _delete(node.children[ch], word, depth + 1)
if should_delete:
del node.children[ch]
return not node.is_end and len(node.children) == 0
return False
return _delete(self.root, word, 0)
Verify: t.insert("apple"); t.insert("app"); t.delete("apple"); assert t.search("app") and not t.search("apple")
Collect all words under a given prefix using DFS. [src3]
def autocomplete(self, prefix: str, limit: int = 10) -> list:
node = self._find_node(prefix)
if node is None:
return []
results = []
def dfs(node, path):
if len(results) >= limit:
return
if node.is_end:
results.append(prefix + path)
for ch in sorted(node.children):
dfs(node.children[ch], path + ch)
dfs(node, "")
return results
Verify: t.insert("app"); t.insert("apple"); t.insert("application"); assert t.autocomplete("app") == ["app", "apple", "application"]
# Input: list of words to insert, queries to search/autocomplete
# Output: boolean search results, list of autocomplete suggestions
class TrieNode:
__slots__ = ('children', 'is_end') # memory optimization
def __init__(self):
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_end = True
def search(self, word: str) -> bool:
node = self._find(word)
return node is not None and node.is_end
def starts_with(self, prefix: str) -> bool:
return self._find(prefix) is not None
def _find(self, prefix: str):
node = self.root
for ch in prefix:
if ch not in node.children:
return None
node = node.children[ch]
return node
// Input: words array, prefix string
// Output: array of autocomplete suggestions
class TrieNode {
constructor() {
this.children = new Map();
this.isEnd = false;
}
}
class Trie {
constructor() { this.root = new TrieNode(); }
insert(word) {
let node = this.root;
for (const ch of word) {
if (!node.children.has(ch))
node.children.set(ch, new TrieNode());
node = node.children.get(ch);
}
node.isEnd = true;
}
search(word) {
const node = this.#find(word);
return node !== null && node.isEnd;
}
startsWith(prefix) {
return this.#find(prefix) !== null;
}
#find(prefix) {
let node = this.root;
for (const ch of prefix) {
if (!node.children.has(ch)) return null;
node = node.children.get(ch);
}
return node;
}
}
// Input: String words, String prefix
// Output: boolean for search, List for autocomplete
class Trie {
private static final int ALPHA = 26;
private int[][] children;
private boolean[] isEnd;
private int size = 0;
public Trie(int maxNodes) {
children = new int[maxNodes][ALPHA];
isEnd = new boolean[maxNodes];
for (int[] row : children) Arrays.fill(row, -1);
size = 1; // root = node 0
}
public void insert(String word) {
int node = 0;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (children[node][idx] == -1)
children[node][idx] = size++;
node = children[node][idx];
}
isEnd[node] = true;
}
public boolean search(String word) {
int node = find(word);
return node != -1 && isEnd[node];
}
private int find(String s) {
int node = 0;
for (char c : s.toCharArray()) {
int idx = c - 'a';
if (children[node][idx] == -1) return -1;
node = children[node][idx];
}
return node;
}
}
// Input: strings to insert, prefix to search
// Output: bool for search, []string for autocomplete
package trie
type TrieNode struct {
Children map[byte]*TrieNode
IsEnd bool
}
type Trie struct { Root *TrieNode }
func NewTrie() *Trie {
return &Trie{Root: &TrieNode{
Children: make(map[byte]*TrieNode),
}}
}
func (t *Trie) Insert(word string) {
node := t.Root
for i := 0; i < len(word); i++ {
ch := word[i]
if _, ok := node.Children[ch]; !ok {
node.Children[ch] = &TrieNode{
Children: make(map[byte]*TrieNode),
}
}
node = node.Children[ch]
}
node.IsEnd = true
}
func (t *Trie) Search(word string) bool {
node := t.find(word)
return node != nil && node.IsEnd
}
func (t *Trie) find(prefix string) *TrieNode {
node := t.Root
for i := 0; i < len(prefix); i++ {
child, ok := node.Children[prefix[i]]
if !ok { return nil }
node = child
}
return node
}
# BAD -- allocates 26 slots per node even when alphabet is larger
class TrieNode:
def __init__(self):
self.children = [None] * 26 # breaks for uppercase, digits, Unicode
self.is_end = False
def insert_char(self, c):
idx = ord(c) - ord('a') # IndexError for 'A', '0', etc.
# GOOD -- handles any character set, allocates only for characters present
class TrieNode:
def __init__(self):
self.children = {} # works with any hashable character
self.is_end = False
# BAD -- naively sets is_end to False without cleanup
def bad_delete(self, word):
node = self.root
for ch in word:
node = node.children[ch]
node.is_end = False # leaves orphaned nodes in memory
# GOOD -- only removes nodes that have no children and are not end-of-word
def delete(self, word):
def _del(node, word, d):
if d == len(word):
if not node.is_end: return False
node.is_end = False
return len(node.children) == 0
ch = word[d]
if ch not in node.children: return False
should_del = _del(node.children[ch], word, d + 1)
if should_del:
del node.children[ch]
return not node.is_end and len(node.children) == 0
return False
_del(self.root, word, 0)
# BAD -- each TrieNode carries a full __dict__, wasting ~100+ bytes per node
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
# GOOD -- __slots__ eliminates __dict__, saving ~60-100 bytes per node
class TrieNode:
__slots__ = ('children', 'is_end')
def __init__(self):
self.children = {}
self.is_end = False
[None]*128 (ASCII) allocates ~1.28 billion slots. Fix: use dict or Map children. [src6]is_end in search(). [src1]... (all wildcards) degenerate to full-tree DFS. Fix: impose a maximum result count or depth limit. [src4]| Use When | Don't Use When | Use Instead |
|---|---|---|
| Need autocomplete or prefix search on a dictionary | Only need exact-match key-value lookup | HashMap / HashSet |
| Building a spell checker with edit-distance suggestions | Keys are numeric IDs with no prefix relationships | Hash table or array index |
| Implementing longest-prefix match (IP routing tables) | Memory is severely constrained and keys share few prefixes | HashMap (lower per-key overhead) |
| Need sorted iteration of all keys with a given prefix | Keys are very long (>1 KB) and rarely share prefixes | Sorted array + binary search |
| Word game solvers (Boggle, Scrabble) with prefix pruning | Need range queries on numeric keys | Balanced BST or B-tree |
| XOR-maximum queries on integers (binary trie) | Dataset is static and fits in a sorted array | Binary search (simpler, cache-friendly) |
datrie (Python), art (Go), or apache/commons-collections (Java) provide production-ready implementations.