Trees - Trie
All about Trie Trees
A Trie (pronounced “try”) is a tree-based data structure that is used to efficiently store and retrieve strings — typically when dealing with large dictionaries or prefix-based queries.
Instead of storing words independently, Trie organizes them by their common prefixes, allowing fast lookups for both complete words and prefixes.
This structure is perfect for problems like autocomplete systems, spell checkers, prefix-based searches, building dictionaries.
Approach
We define a TrieNode class with the following properties:
- letter:
string - isEndOfWord:
boolean - children:
Map<letter, TrieNode>(a map of the next letters as Nodes)
We define a Trie class with three main operations:
- Insert(word)
- Start from the root node.
- For each character in the word, move down the Trie — if a node for that character doesn’t exist, create it.
- After inserting all characters, mark the final node as endOfWord = True to indicate the completion of a valid word.
- Search(word)
- Traverse the Trie character by character.
- If a character doesn’t exist in the current node’s children, return False.
- If all characters are found, return True only if the last node marks the end of a word.
- startsWith(prefix)
- Similar to search, but we only check whether all characters in the prefix exist in the Trie.
- We don’t require the final node to be an end of a complete word.
- Each Trie node is represented by a dictionary (children) mapping characters to child nodes and a boolean flag (endOfWord) indicating if a word ends there.
Time Complexity
Time complexity: L is the length of the word or prefix.
- insert(word) → O(L)
- search(word) → O(L)
- startsWith(prefix) → O(L)
Space complexity: N is the number of words inserted and L is their average length (for storing Trie nodes).
- O(N×L),
Implemmentation with Nodes
Each node stores:
- A map of child nodes (for each character)
- A flag
isEndOfWordindicating whether the path forms a complete word.
(root)
└── c
└── a
├── r (end)
│ └── t (end)
└── t (end)class TrieNode {
constructor(word) {
this.word = word
this.isEndOfWord = false;
this.children = new Map(); // Map<char, TrieNode>
}
}class Trie {
constructor() {
this.root = new TrieNode("");
}
insert(word) {
let currentNode = this.root
for (const letter of word) {
if (!(currentNode.children.has(letter))) {
currentNode.children.set(letter, new TrieNode(letter))
}
let nextNode = currentNode.children.get(letter)
currentNode = nextNode
}
currentNode.isEndWord = true
}
search(word) {
let currentNode = this.root
for (const letter of word) {
if (!currentNode.children.has(letter)) return false
let nextNode = currentNode.children.get(letter)
currentNode = nextNode
}
return currentNode.isEndWord
}
delete(word) {
let currentNode = this.root;
for (const letter of word) {
if (!currentNode.children.has(letter)) return false;
currentNode = currentNode.children.get(letter);
}
if (currentNode.isEndOfWord === false) return false
currentNode.isEndOfWord = false
return true
}
}Implementation with Objects
Each key stores:
- An object with keys for each character
- A flag key
*indicating whether the path forms a complete word.
(root) {
b: {
e: {
a: { r: { *: true }}, // bear
l: { l: { *: true }} // bell
}
i: { d: { *: true }} // bid
u: {
l: { l: { *: true }}, // bull
y: { *: true } // buy
}
},
s: {
e: {
l: { l: { *: true}} // sell
},
t: {
o: {
p: { *: true }, // stop
c: { k: { *: true }} // stock
},
}
}
}class Trie {
constructor() {
this.root = {}
// end of word character = "*"
}
insert(word: string): void {
let node = this.root;
for (const char of word) {
if (!(char in node)) {
node[char] = {};
}
node = node[char];
}
node['*'] = true;
}
search(word: string): boolean {
let node = this.root;
for (const char of word) {
if (!(char in node)) return false;
node = node[char];
}
return !!node['*'];
}
startsWith(prefix: string): boolean {
let node = this.root;
for (const char of prefix) {
if (!(char in node)) return false;
node = node.children.get(char);
}
return true;
}
getWordsWithPrefix(prefix: string): string[] {
let node = this.root;
for (const char of prefix) {
if (!(char in node)) return [];
node = node[char];
}
const results = [];
const dfs = (current, path) => {
if (current['*']) results.push(path);
for (const key in current) {
if (key !== '*') dfs(curreny[key], path + key);
}
};
dfs(node, prefix);
return results;
}
delete(word: string): boolean {
let node = this.root;
for (const char of word) {
if (!(char in node)) return false;
node = node[char];
}
if (!('*' in node)) return false
node['*'] = false
return true
}
// inputs:
// -> 'car'
// -> 'c.r'
isWordInTrie(pattern: string): boolean {
const dfs = (node, i) => {
if (i === pattern.length) {
return !!node['*']; // reached end of pattern
}
const char = pattern[i];
// Wildcard case: try all possible children
if (char === '.') {
for (const key in node) {
if (key === '*') continue; // skip word-end marker
if (dfs(node[key], i + 1)) return true;
}
return false;
}
// Normal character match
if (!(char in node)) return false;
return dfs(node[char], i + 1);
};
return dfs(this.root, 0);
}
}Time Complexity
| Operation | Worst | Description |
|---|---|---|
| Insert | O(L) | L = length of word |
| Search | O(L) | L = length of word |
| Prefix Search | O(L) | L = length of word |
| Insert | O(L) | L = length of word |
| isWordInTrie | O(∑^w * L) | ∑ = alphabet 26 characters, w = wildcards, L = length of word |