Tries (Prefix Trees)
What is a Trie?
A Trie (pronounced “try” or “tree”) is a tree-based data structure used for storing strings. Unlike a standard binary tree, nodes in a Trie do not store the key itself. Instead, the position of a node in the tree defines the key (prefix) it is associated with.
Key Characteristics
- Root: Usually represents an empty string.
- Edges: Represent characters.
- Nodes: Contain an array (or map) of children and a boolean flag indicating if the current path represents a complete word.
Why Use a Trie?
- Fast Lookups: Searching for a word of length
takes exactly time, regardless of how many millions of words are in the dictionary. - Prefix Search: Perfect for “starts with” queries, which are expensive in Hash Tables.
- Space Efficiency: Shared prefixes (e.g., “apple” and “apply”) only store their common parts (“appl”) once.
Usage Cases
- Autocomplete suggestions in search bars.
- Spell Checkers and word validation.
- IP Routing (longest prefix match).
Practice Exercise
Implement a Trie from scratch with Insert, Search, and StartsWith methods. Ensure it handles lower-case English characters (‘a’ through ‘z’).
Answer
1. Trie Implementation ( Time for all operations)
public class TrieNode {
// Array of 26 children (a-z)
public TrieNode[] Children = new TrieNode[26];
public bool IsEndOfWord;
}
public class Trie {
private readonly TrieNode _root = new();
public void Insert(string word) {
TrieNode curr = _root;
foreach (char c in word) {
int idx = c - 'a';
if (curr.Children[idx] == null) {
curr.Children[idx] = new TrieNode();
}
curr = curr.Children[idx];
}
curr.IsEndOfWord = true;
}
public bool Search(string word) {
TrieNode node = GetNode(word);
return node != null && node.IsEndOfWord;
}
public bool StartsWith(string prefix) {
return GetNode(prefix) != null;
}
private TrieNode GetNode(string s) {
TrieNode curr = _root;
foreach (char c in s) {
int idx = c - 'a';
if (curr.Children[idx] == null) return null;
curr = curr.Children[idx];
}
return curr;
}
}
2. Autocomplete Logic (Using DFS)
To find all words starting with a prefix, find the prefix node first, then perform a Depth-First Search to find all IsEndOfWord nodes in its subtree.
public List<string> AutoComplete(string prefix) {
var result = new List<string>();
TrieNode startNode = GetNode(prefix);
if (startNode != null) {
Dfs(startNode, prefix, result);
}
return result;
}
private void Dfs(TrieNode node, string currentPrefix, List<string> result) {
if (node.IsEndOfWord) result.Add(currentPrefix);
for (int i = 0; i < 26; i++) {
if (node.Children[i] != null) {
char nextChar = (char)('a' + i);
Dfs(node.Children[i], currentPrefix + nextChar, result);
}
}
}
Summary
Tries are the gold standard for prefix-based string operations. While they consume more memory than a Hash Set (due to storing pointers for every character), their ability to perform localized prefix searches and shared storage makes them indispensable for dictionary-heavy applications.