Trie
A comprehensive guide to Trie (Prefix Tree) algorithms and techniques for Data Structures and Algorithms.
Table of Contents
- Introduction to Trie
- Basic Trie Implementation
- Search and Prefix Operations
- Advanced Trie Techniques
- Compressed Trie (Radix Tree)
- Trie with HashMap
- Bitwise Trie
- Trie Applications
- Usage Examples
Introduction to Trie
A Trie (pronounced "try") is a tree-like data structure used for storing and searching strings efficiently. Also known as a prefix tree, it's particularly useful for applications involving string prefixes, autocomplete, and word games.
Key Properties
- Root: Empty node representing empty string
- Edges: Represent characters
- Nodes: Represent prefixes
- Leaf marking: Indicates end of valid word
- Path from root: Represents a string/prefix
Visual Representation
root
/ | \
a b c
/ | \
p a a
/ | \
p t t
/ | |
e h h
(ape) (bath) (cat)
When to Use Trie
- Prefix-based searches (autocomplete, search suggestions)
- Word games (Scrabble, Boggle, crosswords)
- String matching with multiple patterns
- IP routing tables
- Dictionary operations with prefix queries
- Longest common prefix problems
Time Complexity Overview
Operation | Time | Space |
---|---|---|
Insert | O(m) | O(ALPHABET_SIZE × N × M) |
Search | O(m) | - |
Delete | O(m) | - |
Prefix Search | O(p) | - |
Where m = length of word, p = length of prefix, N = number of words, M = average length
Basic Trie Implementation
1. Array-based Trie Node
class TrieNode {
constructor() {
this.children = new Array(26).fill(null); // For lowercase a-z
this.isEndOfWord = false;
this.count = 0; // Optional: count of words ending here
}
}
class Trie {
constructor() {
this.root = new TrieNode();
}
// Helper function to get character index
getIndex(char) {
return char.charCodeAt(0) - 'a'.charCodeAt(0);
}
// Insert a word into the trie
insert(word) {
let current = this.root;
for (const char of word) {
const index = this.getIndex(char);
if (current.children[index] === null) {
current.children[index] = new TrieNode();
}
current = current.children[index];
}
current.isEndOfWord = true;
current.count++;
}
// Search for a word in the trie
search(word) {
let current = this.root;
for (const char of word) {
const index = this.getIndex(char);
if (current.children[index] === null) {
return false;
}
current = current.children[index];
}
return current.isEndOfWord;
}
// Check if any word starts with the given prefix
startsWith(prefix) {
let current = this.root;
for (const char of prefix) {
const index = this.getIndex(char);
if (current.children[index] === null) {
return false;
}
current = current.children[index];
}
return true;
}
}
Time Complexity: O(m) for all operations | Space Complexity: O(ALPHABET_SIZE × N × M)
2. HashMap-based Trie Node (More Flexible)
class TrieNodeMap {
constructor() {
this.children = new Map();
this.isEndOfWord = false;
this.count = 0;
}
}
class TrieMap {
constructor() {
this.root = new TrieNodeMap();
}
insert(word) {
let current = this.root;
for (const char of word) {
if (!current.children.has(char)) {
current.children.set(char, new TrieNodeMap());
}
current = current.children.get(char);
}
current.isEndOfWord = true;
current.count++;
}
search(word) {
let current = this.root;
for (const char of word) {
if (!current.children.has(char)) {
return false;
}
current = current.children.get(char);
}
return current.isEndOfWord;
}
startsWith(prefix) {
let current = this.root;
for (const char of prefix) {
if (!current.children.has(char)) {
return false;
}
current = current.children.get(char);
}
return true;
}
// Get all words with given prefix
getWordsWithPrefix(prefix) {
let current = this.root;
// Navigate to prefix node
for (const char of prefix) {
if (!current.children.has(char)) {
return [];
}
current = current.children.get(char);
}
// DFS to collect all words
const words = [];
this.dfs(current, prefix, words);
return words;
}
dfs(node, currentWord, words) {
if (node.isEndOfWord) {
words.push(currentWord);
}
for (const [char, childNode] of node.children) {
this.dfs(childNode, currentWord + char, words);
}
}
}
🔧 Technique: HashMap-based implementation handles any character set (Unicode, special chars)!
Search and Prefix Operations
1. Word Search with Wildcards
class WildcardTrie extends TrieMap {
// Search with '.' as wildcard matching any character
searchWildcard(word) {
return this.searchHelper(this.root, word, 0);
}
searchHelper(node, word, index) {
if (index === word.length) {
return node.isEndOfWord;
}
const char = word[index];
if (char === '.') {
// Try all possible characters
for (const childNode of node.children.values()) {
if (this.searchHelper(childNode, word, index + 1)) {
return true;
}
}
return false;
} else {
// Regular character
if (!node.children.has(char)) {
return false;
}
return this.searchHelper(node.children.get(char), word, index + 1);
}
}
}
2. Auto-complete System
class AutoComplete {
constructor() {
this.trie = new TrieMap();
this.frequencies = new Map();
}
// Add sentence with frequency
input(sentence, frequency = 1) {
this.frequencies.set(sentence, (this.frequencies.get(sentence) || 0) + frequency);
this.trie.insert(sentence);
}
// Get top k suggestions for prefix
search(prefix, k = 3) {
const words = this.trie.getWordsWithPrefix(prefix);
// Sort by frequency (descending) then lexicographically
words.sort((a, b) => {
const freqA = this.frequencies.get(a) || 0;
const freqB = this.frequencies.get(b) || 0;
if (freqA !== freqB) {
return freqB - freqA;
}
return a.localeCompare(b);
});
return words.slice(0, k);
}
}
3. Word Break with Trie
function wordBreakTrie(s, wordDict) {
// Build trie from dictionary
const trie = new TrieMap();
for (const word of wordDict) {
trie.insert(word);
}
const memo = new Map();
function canBreak(start) {
if (start === s.length) return true;
if (memo.has(start)) return memo.get(start);
let current = trie.root;
for (let i = start; i < s.length; i++) {
const char = s[i];
if (!current.children.has(char)) {
break;
}
current = current.children.get(char);
if (current.isEndOfWord && canBreak(i + 1)) {
memo.set(start, true);
return true;
}
}
memo.set(start, false);
return false;
}
return canBreak(0);
}
4. Longest Word in Dictionary
function longestWord(words) {
const trie = new TrieMap();
// Insert all words
for (const word of words) {
trie.insert(word);
}
let longestWord = "";
function dfs(node, currentWord) {
// Update longest word if current is longer (or lexicographically smaller)
if (currentWord.length > longestWord.length ||
(currentWord.length === longestWord.length && currentWord < longestWord)) {
longestWord = currentWord;
}
for (const [char, childNode] of node.children) {
// Only continue if the prefix is also a valid word
if (childNode.isEndOfWord) {
dfs(childNode, currentWord + char);
}
}
}
dfs(trie.root, "");
return longestWord;
}
Advanced Trie Techniques
1. Delete Operation with Cleanup
class AdvancedTrie extends TrieMap {
delete(word) {
return this.deleteHelper(this.root, word, 0);
}
deleteHelper(node, word, index) {
if (index === word.length) {
// Reached end of word
if (!node.isEndOfWord) {
return false; // Word doesn't exist
}
node.isEndOfWord = false;
node.count = 0;
// Return true if node has no children (can be deleted)
return node.children.size === 0;
}
const char = word[index];
const childNode = node.children.get(char);
if (!childNode) {
return false; // Word doesn't exist
}
const shouldDeleteChild = this.deleteHelper(childNode, word, index + 1);
if (shouldDeleteChild) {
node.children.delete(char);
// Return true if current node can also be deleted
return !node.isEndOfWord && node.children.size === 0;
}
return false;
}
// Check if trie is empty
isEmpty() {
return this.root.children.size === 0;
}
// Get total number of words
getTotalWords() {
return this.getTotalWordsHelper(this.root);
}
getTotalWordsHelper(node) {
let count = node.count;
for (const childNode of node.children.values()) {
count += this.getTotalWordsHelper(childNode);
}
return count;
}
}
2. Longest Common Prefix
function longestCommonPrefix(words) {
if (!words || words.length === 0) return "";
const trie = new TrieMap();
// Insert all words
for (const word of words) {
trie.insert(word);
}
let lcp = "";
let current = trie.root;
// Traverse while there's exactly one child and no word ends here
while (current.children.size === 1 && !current.isEndOfWord) {
const char = current.children.keys().next().value;
lcp += char;
current = current.children.get(char);
}
return lcp;
}
3. Count Distinct Substrings
function countDistinctSubstrings(s) {
const trie = new TrieMap();
// Insert all suffixes
for (let i = 0; i < s.length; i++) {
let current = trie.root;
for (let j = i; j < s.length; j++) {
const char = s[j];
if (!current.children.has(char)) {
current.children.set(char, new TrieNodeMap());
}
current = current.children.get(char);
}
}
// Count nodes (excluding root)
return countNodes(trie.root) - 1;
}
function countNodes(node) {
let count = 1; // Count current node
for (const childNode of node.children.values()) {
count += countNodes(childNode);
}
return count;
}
4. Replace Words (Root Dictionary)
function replaceWords(dictionary, sentence) {
const trie = new TrieMap();
// Build trie with dictionary roots
for (const root of dictionary) {
trie.insert(root);
}
const words = sentence.split(' ');
const result = [];
for (const word of words) {
let current = trie.root;
let replacement = "";
let found = false;
for (const char of word) {
if (!current.children.has(char)) {
break;
}
replacement += char;
current = current.children.get(char);
if (current.isEndOfWord) {
found = true;
break;
}
}
result.push(found ? replacement : word);
}
return result.join(' ');
}
Compressed Trie (Radix Tree)
Space-optimized trie that compresses chains of single-child nodes.
1. Radix Tree Implementation
class RadixNode {
constructor(label = "") {
this.label = label; // String label for this edge
this.children = new Map();
this.isEndOfWord = false;
}
}
class RadixTree {
constructor() {
this.root = new RadixNode();
}
insert(word) {
this.insertHelper(this.root, word);
}
insertHelper(node, word) {
if (word.length === 0) {
node.isEndOfWord = true;
return;
}
const firstChar = word[0];
if (node.children.has(firstChar)) {
const child = node.children.get(firstChar);
const commonLength = this.getCommonPrefixLength(child.label, word);
if (commonLength === child.label.length) {
// Full match with child label, continue recursively
this.insertHelper(child, word.substring(commonLength));
} else {
// Partial match, need to split the node
this.splitNode(child, commonLength);
this.insertHelper(child, word.substring(commonLength));
}
} else {
// No matching child, create new node
const newNode = new RadixNode(word);
newNode.isEndOfWord = true;
node.children.set(firstChar, newNode);
}
}
getCommonPrefixLength(str1, str2) {
let i = 0;
while (i < str1.length && i < str2.length && str1[i] === str2[i]) {
i++;
}
return i;
}
splitNode(node, splitIndex) {
const remainingLabel = node.label.substring(splitIndex);
const originalChildren = new Map(node.children);
const originalIsEndOfWord = node.isEndOfWord;
// Create new child with remaining label
const newChild = new RadixNode(remainingLabel);
newChild.children = originalChildren;
newChild.isEndOfWord = originalIsEndOfWord;
// Update current node
node.label = node.label.substring(0, splitIndex);
node.children.clear();
node.children.set(remainingLabel[0], newChild);
node.isEndOfWord = false;
}
search(word) {
let current = this.root;
let remaining = word;
while (remaining.length > 0) {
const firstChar = remaining[0];
if (!current.children.has(firstChar)) {
return false;
}
const child = current.children.get(firstChar);
if (remaining.startsWith(child.label)) {
remaining = remaining.substring(child.label.length);
current = child;
} else {
return false;
}
}
return current.isEndOfWord;
}
}
⚡ Optimization: Radix trees use O(n) space in worst case vs O(ALPHABET_SIZE × n) for regular tries!
Trie with HashMap
Advanced applications using tries with additional data structures.
1. Word Search II (Board + Dictionary)
function findWords(board, words) {
// Build trie from words
const trie = new TrieMap();
for (const word of words) {
trie.insert(word);
}
const result = new Set();
const rows = board.length;
const cols = board[0].length;
const directions = [[0, 1], [1, 0], [0, -1], [-1, 0]];
function dfs(row, col, node, currentWord, visited) {
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
if (visited.has(`${row},${col}`)) return;
const char = board[row][col];
if (!node.children.has(char)) return;
const childNode = node.children.get(char);
const newWord = currentWord + char;
if (childNode.isEndOfWord) {
result.add(newWord);
}
visited.add(`${row},${col}`);
for (const [dr, dc] of directions) {
dfs(row + dr, col + dc, childNode, newWord, visited);
}
visited.delete(`${row},${col}`);
}
// Start DFS from each cell
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
dfs(i, j, trie.root, "", new Set());
}
}
return Array.from(result);
}
2. Top K Frequent Words
function topKFrequent(words, k) {
// Count frequencies
const frequency = new Map();
for (const word of words) {
frequency.set(word, (frequency.get(word) || 0) + 1);
}
// Build trie with frequency information
class FreqTrieNode extends TrieNodeMap {
constructor() {
super();
this.frequency = 0;
}
}
const trie = new TrieMap();
trie.root = new FreqTrieNode();
for (const [word, freq] of frequency) {
let current = trie.root;
for (const char of word) {
if (!current.children.has(char)) {
current.children.set(char, new FreqTrieNode());
}
current = current.children.get(char);
}
current.isEndOfWord = true;
current.frequency = freq;
}
// Collect words with frequencies
const wordsWithFreq = [];
function collectWords(node, currentWord) {
if (node.isEndOfWord) {
wordsWithFreq.push([currentWord, node.frequency]);
}
for (const [char, childNode] of node.children) {
collectWords(childNode, currentWord + char);
}
}
collectWords(trie.root, "");
// Sort by frequency (desc) then lexicographically (asc)
wordsWithFreq.sort(([wordA, freqA], [wordB, freqB]) => {
if (freqA !== freqB) return freqB - freqA;
return wordA.localeCompare(wordB);
});
return wordsWithFreq.slice(0, k).map(([word, freq]) => word);
}
3. Stream of Characters
class StreamChecker {
constructor(words) {
this.trie = new TrieMap();
this.stream = "";
this.maxWordLength = 0;
// Insert reversed words for suffix matching
for (const word of words) {
const reversedWord = word.split('').reverse().join('');
this.trie.insert(reversedWord);
this.maxWordLength = Math.max(this.maxWordLength, word.length);
}
}
query(letter) {
this.stream += letter;
// Keep only last maxWordLength characters
if (this.stream.length > this.maxWordLength) {
this.stream = this.stream.substring(this.stream.length - this.maxWordLength);
}
// Check if any suffix of reversed stream matches a word
let current = this.trie.root;
for (let i = this.stream.length - 1; i >= 0; i--) {
const char = this.stream[i];
if (!current.children.has(char)) {
return false;
}
current = current.children.get(char);
if (current.isEndOfWord) {
return true;
}
}
return false;
}
}
Bitwise Trie
Special trie for handling binary representations and XOR operations.
1. Maximum XOR Trie
class BitTrieNode {
constructor() {
this.children = new Array(2).fill(null); // 0 and 1
this.value = -1; // Store the actual number at leaf
}
}
class BitTrie {
constructor() {
this.root = new BitTrieNode();
}
insert(num) {
let current = this.root;
// Process 32 bits from MSB to LSB
for (let i = 31; i >= 0; i--) {
const bit = (num >> i) & 1;
if (current.children[bit] === null) {
current.children[bit] = new BitTrieNode();
}
current = current.children[bit];
}
current.value = num;
}
findMaxXOR(num) {
let current = this.root;
let maxXOR = 0;
for (let i = 31; i >= 0; i--) {
const bit = (num >> i) & 1;
const oppositeBit = 1 - bit;
if (current.children[oppositeBit] !== null) {
maxXOR |= (1 << i);
current = current.children[oppositeBit];
} else {
current = current.children[bit];
}
}
return maxXOR;
}
}
function findMaximumXOR(nums) {
const trie = new BitTrie();
let maxXOR = 0;
// Insert first number
trie.insert(nums[0]);
// For each subsequent number, find max XOR and insert it
for (let i = 1; i < nums.length; i++) {
maxXOR = Math.max(maxXOR, trie.findMaxXOR(nums[i]));
trie.insert(nums[i]);
}
return maxXOR;
}
2. Count Pairs with XOR in Range
function countPairsInRange(nums, low, high) {
const trie = new BitTrie();
function countPairsWithMaxXOR(maxXOR) {
let count = 0;
for (const num of nums) {
count += countPairsHelper(trie.root, num, maxXOR, 31);
trie.insert(num);
}
return count;
}
function countPairsHelper(node, num, maxXOR, bit) {
if (bit < 0) return 1;
const numBit = (num >> bit) & 1;
const maxBit = (maxXOR >> bit) & 1;
let count = 0;
if (maxBit === 1) {
// Can take either path
if (node.children[numBit] !== null) {
count += countPairsHelper(node.children[numBit], num, maxXOR, bit - 1);
}
if (node.children[1 - numBit] !== null) {
count += countPairsHelper(node.children[1 - numBit], num, maxXOR, bit - 1);
}
} else {
// Must take the same bit to stay <= maxXOR
const requiredBit = numBit;
if (node.children[requiredBit] !== null) {
count += countPairsHelper(node.children[requiredBit], num, maxXOR, bit - 1);
}
}
return count;
}
return countPairsWithMaxXOR(high) - countPairsWithMaxXOR(low - 1);
}
Trie Applications
1. Phone Directory (T9 Predictive Text)
class T9Trie {
constructor() {
// Map phone digits to letters
this.digitToLetters = {
'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
'6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'
};
this.letterToDigit = {};
// Build reverse mapping
for (const [digit, letters] of Object.entries(this.digitToLetters)) {
for (const letter of letters) {
this.letterToDigit[letter] = digit;
}
}
this.trie = new TrieMap();
}
addWord(word) {
// Convert word to digit sequence
const digitSequence = word.toLowerCase()
.split('')
.map(char => this.letterToDigit[char] || char)
.join('');
// Store both digit sequence and original word
let current = this.trie.root;
for (const digit of digitSequence) {
if (!current.children.has(digit)) {
current.children.set(digit, new TrieNodeMap());
current.children.get(digit).words = [];
}
current = current.children.get(digit);
}
if (!current.words) current.words = [];
current.words.push(word);
current.isEndOfWord = true;
}
getSuggestions(digitSequence, maxSuggestions = 10) {
let current = this.trie.root;
// Navigate to the digit sequence
for (const digit of digitSequence) {
if (!current.children.has(digit)) {
return [];
}
current = current.children.get(digit);
}
// Collect all words from this point
const suggestions = [];
this.collectWords(current, suggestions);
return suggestions.slice(0, maxSuggestions);
}
collectWords(node, result) {
if (node.words) {
result.push(...node.words);
}
for (const childNode of node.children.values()) {
this.collectWords(childNode, result);
}
}
}
2. Spell Checker with Edit Distance
class SpellChecker {
constructor(dictionary) {
this.trie = new TrieMap();
for (const word of dictionary) {
this.trie.insert(word.toLowerCase());
}
}
// Find words within edit distance k
findSimilarWords(word, maxDistance = 1) {
const results = [];
const wordLower = word.toLowerCase();
this.searchWithDistance(this.trie.root, "", wordLower, maxDistance, results);
return results;
}
searchWithDistance(node, currentWord, targetWord, remainingDistance, results) {
// If we've matched the entire target word
if (targetWord.length === 0) {
if (node.isEndOfWord) {
results.push(currentWord);
}
// Continue to find longer words if we have remaining distance
if (remainingDistance > 0) {
for (const [char, childNode] of node.children) {
this.searchWithDistance(childNode, currentWord + char, "", remainingDistance - 1, results);
}
}
return;
}
const targetChar = targetWord[0];
const remainingTarget = targetWord.substring(1);
for (const [char, childNode] of node.children) {
if (char === targetChar) {
// Exact match - no cost
this.searchWithDistance(childNode, currentWord + char, remainingTarget, remainingDistance, results);
} else if (remainingDistance > 0) {
// Substitution - cost 1
this.searchWithDistance(childNode, currentWord + char, remainingTarget, remainingDistance - 1, results);
}
}
// Insertion - add character from target without moving in trie
if (remainingDistance > 0) {
this.searchWithDistance(node, currentWord + targetChar, remainingTarget, remainingDistance - 1, results);
}
// Deletion - move in trie without consuming target character
if (remainingDistance > 0) {
for (const [char, childNode] of node.children) {
this.searchWithDistance(childNode, currentWord + char, targetWord, remainingDistance - 1, results);
}
}
}
// Simple spell check with suggestions
spellCheck(word) {
const wordLower = word.toLowerCase();
if (this.trie.search(wordLower)) {
return { isCorrect: true, suggestions: [] };
}
const suggestions = this.findSimilarWords(wordLower, 2);
return { isCorrect: false, suggestions };
}
}
3. IP Address Trie (Longest Prefix Match)
class IPTrie {
constructor() {
this.root = new TrieNodeMap();
this.root.route = null; // Store routing information
}
// Convert IP to binary string
ipToBinary(ip) {
return ip.split('.').map(octet => {
return parseInt(octet).toString(2).padStart(8, '0');
}).join('');
}
// Add route with CIDR notation (e.g., "192.168.1.0/24")
addRoute(cidr, routeInfo) {
const [ip, prefixLength] = cidr.split('/');
const binaryIP = this.ipToBinary(ip);
const prefix = binaryIP.substring(0, parseInt(prefixLength));
let current = this.root;
for (const bit of prefix) {
if (!current.children.has(bit)) {
current.children.set(bit, new TrieNodeMap());
current.children.get(bit).route = null;
}
current = current.children.get(bit);
}
current.route = routeInfo;
}
// Find longest prefix match for an IP
longestPrefixMatch(ip) {
const binaryIP = this.ipToBinary(ip);
let current = this.root;
let lastMatchedRoute = current.route;
for (const bit of binaryIP) {
if (!current.children.has(bit)) {
break;
}
current = current.children.get(bit);
if (current.route !== null) {
lastMatchedRoute = current.route;
}
}
return lastMatchedRoute;
}
}
// Usage example
const ipTrie = new IPTrie();
ipTrie.addRoute("192.168.1.0/24", { gateway: "192.168.1.1", interface: "eth0" });
ipTrie.addRoute("192.168.0.0/16", { gateway: "192.168.0.1", interface: "eth1" });
ipTrie.addRoute("0.0.0.0/0", { gateway: "10.0.0.1", interface: "eth2" }); // Default route
console.log(ipTrie.longestPrefixMatch("192.168.1.100")); // Should match /24 route
4. Boggle Game Solver
function findWordsInBoggle(board, dictionary) {
// Build trie from dictionary
const trie = new TrieMap();
for (const word of dictionary) {
trie.insert(word.toUpperCase());
}
const rows = board.length;
const cols = board[0].length;
const directions = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
];
const foundWords = new Set();
function dfs(row, col, node, currentWord, visited) {
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
if (visited[row][col]) return;
const char = board[row][col].toUpperCase();
if (!node.children.has(char)) return;
const childNode = node.children.get(char);
const newWord = currentWord + char;
// Mark as visited
visited[row][col] = true;
// If we found a complete word (and it's at least 3 characters long)
if (childNode.isEndOfWord && newWord.length >= 3) {
foundWords.add(newWord);
}
// Continue searching in all directions
for (const [dr, dc] of directions) {
dfs(row + dr, col + dc, childNode, newWord, visited);
}
// Backtrack
visited[row][col] = false;
}
// Start DFS from each cell
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const visited = Array(rows).fill(null).map(() => Array(cols).fill(false));
dfs(i, j, trie.root, "", visited);
}
}
return Array.from(foundWords).sort();
}
Usage Examples
console.log("=== Trie Data Structure Demo ===");
// Basic Trie Operations
const trie = new TrieMap();
const words = ["apple", "app", "application", "apply", "banana"];
for (const word of words) {
trie.insert(word);
}
console.log("Search 'app':", trie.search("app")); // true
console.log("Search 'appl':", trie.search("appl")); // false
console.log("Starts with 'app':", trie.startsWith("app")); // true
console.log("Words with prefix 'app':", trie.getWordsWithPrefix("app"));
// ["app", "apple", "application", "apply"]
// Wildcard Search
const wildcardTrie = new WildcardTrie();
wildcardTrie.insert("bad");
wildcardTrie.insert("dad");
wildcardTrie.insert("mad");
console.log("Search 'b.d':", wildcardTrie.searchWildcard("b.d")); // true
console.log("Search '.ad':", wildcardTrie.searchWildcard(".ad")); // true
// Auto-complete System
const autoComplete = new AutoComplete();
autoComplete.input("i love you", 5);
autoComplete.input("island", 3);
autoComplete.input("i love leetcode", 2);
console.log("Autocomplete 'i':", autoComplete.search("i", 3));
// ["i love you", "island", "i love leetcode"]
// Word Break
const wordDict = ["apple", "pen", "applepen", "pine", "pineapple"];
console.log("Word break 'pineapplepenapple':",
wordBreakTrie("pineapplepenapple", wordDict)); // true
// Longest Common Prefix
const words2 = ["flower", "flow", "flight"];
console.log("Longest common prefix:", longestCommonPrefix(words2)); // "fl"
// Replace Words
const dictionary = ["cat", "bat", "rat"];
const sentence = "the cattle was rattled by the battery";
console.log("Replace words:", replaceWords(dictionary, sentence));
// "the cat was rat by the bat"
// Maximum XOR
const nums = [3, 10, 5, 25, 2, 8];
console.log("Maximum XOR:", findMaximumXOR(nums)); // 28
// Spell Checker
const spellChecker = new SpellChecker(["hello", "world", "help", "held"]);
console.log("Spell check 'helo':", spellChecker.spellCheck("helo"));
// { isCorrect: false, suggestions: ["hello", "help", "held"] }
// Boggle Solver
const board = [
['E', 'A', 'A', 'N'],
['T', 'T', 'A', 'E'],
['I', 'H', 'K', 'R'],
['I', 'F', 'L', 'V']
];
const boggleDict = ["eat", "rain", "hike", "fire", "lair"];
console.log("Boggle words:", findWordsInBoggle(board, boggleDict));
// IP Routing
const ipTrie = new IPTrie();
ipTrie.addRoute("192.168.1.0/24", { gateway: "192.168.1.1" });
ipTrie.addRoute("0.0.0.0/0", { gateway: "10.0.0.1" });
console.log("Route for 192.168.1.100:", ipTrie.longestPrefixMatch("192.168.1.100"));
Time Complexity Summary
Operation | Trie | Compressed Trie | Hash Map |
---|---|---|---|
Insert | O(m) | O(m) | O(1) avg |
Search | O(m) | O(m) | O(1) avg |
Delete | O(m) | O(m) | O(1) avg |
Prefix Search | O(p) | O(p) | O(n) |
Space | O(ALPHABET × N × M) | O(N × M) | O(N × M) |
Where m = word length, p = prefix length, N = number of words, M = average word length
Common Patterns to Remember
1. Basic Trie Template
class TrieNode {
constructor() {
this.children = new Map(); // or Array for fixed alphabet
this.isEndOfWord = false;
}
}
class Trie {
constructor() {
this.root = new TrieNode();
}
insert(word) {
let current = this.root;
for (const char of word) {
if (!current.children.has(char)) {
current.children.set(char, new TrieNode());
}
current = current.children.get(char);
}
current.isEndOfWord = true;
}
}
2. DFS for Word Collection
function collectWords(node, prefix, result) {
if (node.isEndOfWord) {
result.push(prefix);
}
for (const [char, childNode] of node.children) {
collectWords(childNode, prefix + char, result);
}
}
3. Backtracking with Trie
function dfsWithBacktrack(board, row, col, node, currentWord, visited, result) {
// Boundary checks
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
if (visited[row][col]) return;
const char = board[row][col];
if (!node.children.has(char)) return;
// Mark visited
visited[row][col] = true;
const childNode = node.children.get(char);
if (childNode.isEndOfWord) {
result.add(currentWord + char);
}
// Explore neighbors
for (const [dr, dc] of directions) {
dfsWithBacktrack(board, row + dr, col + dc, childNode, currentWord + char, visited, result);
}
// Backtrack
visited[row][col] = false;
}
4. Bitwise Trie for XOR
class BitTrie {
insert(num) {
let current = this.root;
for (let i = 31; i >= 0; i--) {
const bit = (num >> i) & 1;
if (!current.children[bit]) {
current.children[bit] = new BitTrieNode();
}
current = current.children[bit];
}
}
findMaxXOR(num) {
let current = this.root;
let maxXOR = 0;
for (let i = 31; i >= 0; i--) {
const bit = (num >> i) & 1;
const oppositeBit = 1 - bit;
if (current.children[oppositeBit]) {
maxXOR |= (1 << i);
current = current.children[oppositeBit];
} else {
current = current.children[bit];
}
}
return maxXOR;
}
}
5. Delete with Cleanup
function deleteWord(node, word, index) {
if (index === word.length) {
if (!node.isEndOfWord) return false;
node.isEndOfWord = false;
return node.children.size === 0; // Can delete if no children
}
const char = word[index];
const childNode = node.children.get(char);
if (!childNode) return false;
const shouldDelete = deleteWord(childNode, word, index + 1);
if (shouldDelete) {
node.children.delete(char);
return !node.isEndOfWord && node.children.size === 0;
}
return false;
}
Key Interview Tips
Problem Recognition Checklist
Use Trie when you see:
- ✅ Prefix-based operations (autocomplete, search suggestions)
- ✅ Word games (Boggle, Scrabble, crosswords)
- ✅ Multiple pattern matching in strings
- ✅ Dictionary operations with prefix queries
- ✅ String collections with common prefixes
- ✅ Longest common prefix problems
Use Bitwise Trie for:
- ✅ XOR problems with arrays of numbers
- ✅ Maximum/minimum XOR queries
- ✅ Bit manipulation on collections
Common Mistakes to Avoid
- Forgetting isEndOfWord: Not marking word endings properly
- Memory leaks: Not cleaning up nodes during deletion
- Case sensitivity: Inconsistent handling of upper/lowercase
- Null pointer errors: Not checking if children exist
- Over-engineering: Using trie when hash map would suffice
Space vs Time Tradeoffs
Scenario | Trie Advantage | Hash Map Advantage |
---|---|---|
Prefix operations | O(p) prefix search | O(n) to find all prefixes |
Memory usage | Shared prefixes save space | Each word stored separately |
Insertion/Search | O(m) guaranteed | O(1) average case |
Implementation | More complex | Simpler |
Performance Optimization Tips
- Use arrays for fixed alphabets (26 lowercase letters)
- Use hash maps for arbitrary characters (Unicode, mixed case)
- Consider compressed tries for sparse data
- Add early termination in search algorithms
- Implement lazy deletion for better performance
Advanced Techniques
Trie with Suffix Arrays
// For advanced string algorithms
class SuffixTrie {
constructor(text) {
this.trie = new TrieMap();
this.buildSuffixTrie(text);
}
buildSuffixTrie(text) {
for (let i = 0; i < text.length; i++) {
this.trie.insert(text.substring(i));
}
}
}
Concurrent Trie (Thread-Safe)
class ConcurrentTrie {
constructor() {
this.trie = new TrieMap();
this.locks = new Map(); // Simplified locking mechanism
}
// Add proper synchronization for multi-threaded environments
safeInsert(word) {
// Implementation would include proper locking
this.trie.insert(word);
}
}
This comprehensive guide covers all essential Trie techniques for coding interviews and competitive programming. The combination of trie with various algorithms (DFS, backtracking, bit manipulation) makes it a versatile data structure for many string and prefix-related problems!
Practice Problems by Difficulty
Beginner
- Implement Trie (Prefix Tree)
- Longest Common Prefix
- Design Add and Search Words Data Structure
Intermediate
- Word Search II
- Replace Words
- Top K Frequent Words
- Stream of Characters
- Word Break with Trie
Advanced
- Maximum XOR of Two Numbers in Array
- Count Pairs with XOR in Range
- Palindrome Pairs
- Index Pairs of a String
Expert
- Design Search Autocomplete System
- Word Squares
- Concatenated Words
- Number of Distinct Substrings using Suffix Trie