
A Trie, also known as a Prefix Tree, is a specialized tree-like data structure designed to efficiently store and retrieve strings. Unlike a binary search tree, which organizes data based on comparisons between elements, a Trie organizes data based on the characters of the keys themselves, making it exceptionally well-suited for string search, autocomplete, and dictionary-like operations.
The primary idea behind a Trie is simple: each node represents a single character of a string, and the path from the root to a node corresponds to a prefix of one or more stored strings. By traversing the tree along these paths, we can quickly determine whether a string exists in the collection or retrieve all strings sharing a common prefix.
As we said before, at the heart of a Trie is the node, which represents a single character and links to its child nodes. Understanding the structure of a Trie node is crucial, as it directly affects time and space efficiency.
A typical Trie node contains two essential elements:
These elements allow the Trie to efficiently answer queries such as search or prefix lookup, because traversal only involves moving from one character node to the next along the path of the target string.
Below you can find a TypeScript implementation demonstrating a Trie node and basic operations: insert, search, and prefix search.
class TrieNode {
children: Map<string, TrieNode>;
isEndOfWord: boolean;
constructor() {
this.children = new Map();
this.isEndOfWord = false;
}
}
class Trie {
root: TrieNode;
constructor() {
this.root = new TrieNode();
}
insert(word: string) {
let node = this.root;
for (const char of word) {
if (!node.children.has(char)) {
node.children.set(char, new TrieNode());
}
node = node.children.get(char)!;
}
node.isEndOfWord = true;
}
search(word: string): boolean {
let node = this.root;
for (const char of word) {
if (!node.children.has(char)) return false;
node = node.children.get(char)!;
}
return node.isEndOfWord;
}
startsWith(prefix: string): boolean {
let node = this.root;
for (const char of prefix) {
if (!node.children.has(char)) return false;
node = node.children.get(char)!;
}
return true;
}
}
// Example usage
const trie = new Trie();
trie.insert("apple");
trie.insert("app");
console.log(trie.search("apple")); // true
console.log(trie.search("app")); // true
console.log(trie.search("apricot"));// false
console.log(trie.startsWith("ap")); // true
Understanding the time and space complexity of Trie operations is crucial for evaluating their efficiency and deciding when to use them over other data structures such as hash maps or binary search trees.
Given:
| Operation | Time Complexity | Space Complexity | Explanation |
|---|---|---|---|
| Insert word | O(L) | O(L) | Each character requires a node if it does not exist, hence additional space proportional to the word length. |
| Search word | O(L) | O(1) | Traversal follows the characters of the word; no extra memory is allocated beyond traversal pointers. |
| Search prefix | O(P) | O(1) | Traversal stops at the end of the prefix. |
| Memory usage (overall) | O(AL) | O(AL) | Each node can have up to A children. Sparse Tries with hash maps reduce memory overhead compared to dense arrays. |