Trie Exercises: Answer Sheet & Worked Solutions
This is the answer sheet for the trie exercises. Each solution gives the key idea, the optimal complexity, the code in all four languages (use the switcher), and common pitfalls. Throughout, L is the length of a word or prefix and n is the number of stored words. Try the problems first — then check your work here.
Q1. Implement insert and search
Idea: Store a TrieNode with a children map and an isEnd flag. insert walks the word, creating missing nodes, and sets isEnd on the last node. search walks the word and checks isEnd; startsWith only checks that the path exists.
Complexity: O(L) time per operation, O(1) extra space (insert allocates up to L new nodes).
class Trie {
struct Node {
std::unordered_map<char, Node*> children;
bool isEnd = false;
};
Node* root = new Node();
Node* walk(const std::string& s) {
Node* node = root;
for (char ch : s) {
auto it = node->children.find(ch);
if (it == node->children.end()) return nullptr;
node = it->second;
}
return node;
}
public:
void insert(const std::string& word) {
Node* node = root;
for (char ch : word) {
if (!node->children.count(ch)) node->children[ch] = new Node();
node = node->children[ch];
}
node->isEnd = true;
}
bool search(const std::string& word) {
Node* node = walk(word);
return node != nullptr && node->isEnd;
}
bool startsWith(const std::string& prefix) {
return walk(prefix) != nullptr;
}
};
class Trie {
private static class Node {
Map<Character, Node> children = new HashMap<>();
boolean isEnd = false;
}
private final Node root = new Node();
public void insert(String word) {
Node node = root;
for (char ch : word.toCharArray()) {
node.children.putIfAbsent(ch, new Node());
node = node.children.get(ch);
}
node.isEnd = true;
}
public boolean search(String word) {
Node node = walk(word);
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) {
return walk(prefix) != null;
}
private Node walk(String s) {
Node node = root;
for (char ch : s.toCharArray()) {
node = node.children.get(ch);
if (node == null) return null;
}
return node;
}
}
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._walk(word);
return node !== null && node.isEnd;
}
startsWith(prefix) {
return this._walk(prefix) !== null;
}
_walk(s) {
let node = this.root;
for (const ch of s) {
if (!node.children.has(ch)) return null;
node = node.children.get(ch);
}
return node;
}
}
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
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):
node = self._walk(word)
return node is not None and node.is_end
def starts_with(self, prefix):
return self._walk(prefix) is not None
def _walk(self, s):
node = self.root
for ch in s:
if ch not in node.children:
return None
node = node.children[ch]
return node
Pitfall: Returning
truefromsearchjust because the path exists is the classic bug. If onlycardwas inserted,search("car")must befalse— always checkisEnd.
Q2. Count words and count prefixes
Idea: Augment each node with two counters: prefixCount (how many inserted words pass through this node) and wordCount (how many words end exactly here). insert bumps prefixCount on every node it visits and wordCount on the last. The two queries are just a walk returning the relevant counter.
Complexity: O(L) per operation, O(1) extra space per query.
class WordTrie {
struct Node {
std::unordered_map<char, Node*> children;
int wordCount = 0; // words ending here
int prefixCount = 0; // words passing through
};
Node* root = new Node();
Node* walk(const std::string& s) {
Node* node = root;
for (char ch : s) {
auto it = node->children.find(ch);
if (it == node->children.end()) return nullptr;
node = it->second;
}
return node;
}
public:
void insert(const std::string& w) {
Node* node = root;
for (char ch : w) {
if (!node->children.count(ch)) node->children[ch] = new Node();
node = node->children[ch];
node->prefixCount++;
}
node->wordCount++;
}
int countWordsEqualTo(const std::string& w) {
Node* n = walk(w);
return n ? n->wordCount : 0;
}
int countWordsStartingWith(const std::string& p) {
Node* n = walk(p);
return n ? n->prefixCount : 0;
}
};
class WordTrie {
private static class Node {
Map<Character, Node> children = new HashMap<>();
int wordCount = 0; // words ending here
int prefixCount = 0; // words passing through
}
private final Node root = new Node();
public void insert(String w) {
Node node = root;
for (char ch : w.toCharArray()) {
node.children.putIfAbsent(ch, new Node());
node = node.children.get(ch);
node.prefixCount++;
}
node.wordCount++;
}
public int countWordsEqualTo(String w) {
Node n = walk(w);
return n == null ? 0 : n.wordCount;
}
public int countWordsStartingWith(String p) {
Node n = walk(p);
return n == null ? 0 : n.prefixCount;
}
private Node walk(String s) {
Node node = root;
for (char ch : s.toCharArray()) {
node = node.children.get(ch);
if (node == null) return null;
}
return node;
}
}
class Node {
constructor() {
this.children = new Map();
this.wordCount = 0; // words ending here
this.prefixCount = 0; // words passing through
}
}
class WordTrie {
constructor() { this.root = new Node(); }
insert(w) {
let node = this.root;
for (const ch of w) {
if (!node.children.has(ch)) node.children.set(ch, new Node());
node = node.children.get(ch);
node.prefixCount++;
}
node.wordCount++;
}
countWordsEqualTo(w) {
const n = this._walk(w);
return n ? n.wordCount : 0;
}
countWordsStartingWith(p) {
const n = this._walk(p);
return n ? n.prefixCount : 0;
}
_walk(s) {
let node = this.root;
for (const ch of s) {
if (!node.children.has(ch)) return null;
node = node.children.get(ch);
}
return node;
}
}
class Node:
def __init__(self):
self.children = {}
self.word_count = 0 # words ending here
self.prefix_count = 0 # words passing through
class WordTrie:
def __init__(self):
self.root = Node()
def insert(self, w):
node = self.root
for ch in w:
if ch not in node.children:
node.children[ch] = Node()
node = node.children[ch]
node.prefix_count += 1
node.word_count += 1
def count_words_equal_to(self, w):
n = self._walk(w)
return n.word_count if n else 0
def count_words_starting_with(self, p):
n = self._walk(p)
return n.prefix_count if n else 0
def _walk(self, s):
node = self.root
for ch in s:
if ch not in node.children:
return None
node = node.children[ch]
return node
Pitfall: Incrementing
prefixCounton the root makes “words starting with empty string” wrong relative to other prefixes. Increment only on nodes you step into, never the root.
Q3. Delete a word from a trie
Idea: Recurse to the end of the word. Unset isEnd; then on the way back up, prune a child only if it has no other children and is not itself a word end. The recursion returns “should my parent delete me?” to drive the pruning. Add this to the Trie from Q1.
Complexity: O(L) time, O(L) recursion stack.
// returns true if `node` can be removed by its parent
bool removeHelper(Node* node, const std::string& word, int i) {
if (i == (int)word.size()) {
if (!node->isEnd) return false; // word not present
node->isEnd = false;
return node->children.empty(); // prune if it is now a leaf
}
char ch = word[i];
auto it = node->children.find(ch);
if (it == node->children.end()) return false;
if (removeHelper(it->second, word, i + 1)) {
delete it->second;
node->children.erase(it);
return !node->isEnd && node->children.empty();
}
return false;
}
void remove(const std::string& word) { removeHelper(root, word, 0); }
public void delete(String word) { deleteHelper(root, word, 0); }
// returns true if `node` can be removed by its parent
private boolean deleteHelper(Node node, String word, int i) {
if (i == word.length()) {
if (!node.isEnd) return false; // word not present
node.isEnd = false;
return node.children.isEmpty(); // prune if it is now a leaf
}
char ch = word.charAt(i);
Node child = node.children.get(ch);
if (child == null) return false;
if (deleteHelper(child, word, i + 1)) {
node.children.remove(ch);
return !node.isEnd && node.children.isEmpty();
}
return false;
}
delete(word) { this._delete(this.root, word, 0); }
// returns true if `node` can be removed by its parent
_delete(node, word, i) {
if (i === word.length) {
if (!node.isEnd) return false; // word not present
node.isEnd = false;
return node.children.size === 0; // prune if it is now a leaf
}
const ch = word[i];
const child = node.children.get(ch);
if (!child) return false;
if (this._delete(child, word, i + 1)) {
node.children.delete(ch);
return !node.isEnd && node.children.size === 0;
}
return false;
}
def delete(self, word):
self._delete(self.root, word, 0)
# returns True if `node` can be removed by its parent
def _delete(self, node, word, i):
if i == len(word):
if not node.is_end:
return False # word not present
node.is_end = False
return len(node.children) == 0 # prune if it is now a leaf
ch = word[i]
child = node.children.get(ch)
if child is None:
return False
if self._delete(child, word, i + 1):
del node.children[ch]
return not node.is_end and len(node.children) == 0
return False
Pitfall: Deleting nodes unconditionally destroys shared prefixes. If
cardandcarboth exist, deletingcarmust only flip itsisEndflag — thecarpath is still needed bycard. Only prune leaves with noisEnd.
Q4. Autocomplete — all words with a prefix
Idea: Walk to the prefix node, then DFS its subtree, rebuilding each word and collecting nodes with isEnd. Iterating children in sorted order yields suggestions alphabetically.
Complexity: O(L + k) time, where k is the size of the matched subtree; O(k) for the output.
class Autocomplete {
struct Node {
std::map<char, Node*> children; // ordered -> sorted suggestions
bool isEnd = false;
};
Node* root = new Node();
void collect(Node* node, std::string& path, std::vector<std::string>& out) {
if (node->isEnd) out.push_back(path);
for (auto& [ch, child] : node->children) {
path.push_back(ch);
collect(child, path, out);
path.pop_back();
}
}
public:
void insert(const std::string& word) {
Node* node = root;
for (char ch : word) {
if (!node->children.count(ch)) node->children[ch] = new Node();
node = node->children[ch];
}
node->isEnd = true;
}
std::vector<std::string> suggest(const std::string& prefix) {
std::vector<std::string> out;
Node* node = root;
for (char ch : prefix) {
auto it = node->children.find(ch);
if (it == node->children.end()) return out;
node = it->second;
}
std::string path = prefix;
collect(node, path, out);
return out;
}
};
class Autocomplete {
private static class Node {
Map<Character, Node> children = new TreeMap<>(); // sorted
boolean isEnd = false;
}
private final Node root = new Node();
public void insert(String word) {
Node node = root;
for (char ch : word.toCharArray()) {
node.children.putIfAbsent(ch, new Node());
node = node.children.get(ch);
}
node.isEnd = true;
}
public List<String> suggest(String prefix) {
List<String> out = new ArrayList<>();
Node node = root;
for (char ch : prefix.toCharArray()) {
node = node.children.get(ch);
if (node == null) return out;
}
collect(node, new StringBuilder(prefix), out);
return out;
}
private void collect(Node node, StringBuilder path, List<String> out) {
if (node.isEnd) out.add(path.toString());
for (Map.Entry<Character, Node> e : node.children.entrySet()) {
path.append(e.getKey());
collect(e.getValue(), path, out);
path.deleteCharAt(path.length() - 1);
}
}
}
class Autocomplete {
constructor() { this.root = { children: new Map(), isEnd: false }; }
insert(word) {
let node = this.root;
for (const ch of word) {
if (!node.children.has(ch))
node.children.set(ch, { children: new Map(), isEnd: false });
node = node.children.get(ch);
}
node.isEnd = true;
}
suggest(prefix) {
let node = this.root;
for (const ch of prefix) {
if (!node.children.has(ch)) return [];
node = node.children.get(ch);
}
const out = [];
const collect = (n, path) => {
if (n.isEnd) out.push(path);
for (const ch of [...n.children.keys()].sort())
collect(n.children.get(ch), path + ch);
};
collect(node, prefix);
return out;
}
}
class Autocomplete:
def __init__(self):
self.root = {"children": {}, "is_end": False}
def insert(self, word):
node = self.root
for ch in word:
node = node["children"].setdefault(
ch, {"children": {}, "is_end": False})
node["is_end"] = True
def suggest(self, prefix):
node = self.root
for ch in prefix:
if ch not in node["children"]:
return []
node = node["children"][ch]
out = []
self._collect(node, prefix, out)
return out
def _collect(self, node, path, out):
if node["is_end"]:
out.append(path)
for ch in sorted(node["children"]):
self._collect(node["children"][ch], path + ch, out)
Pitfall: If the prefix path does not exist, return an empty list — do not start collecting from the root, which would dump the entire dictionary.
Q5. Longest common prefix of a set of words
Idea: Insert all words into a trie, then walk down from the root as long as the current node has exactly one child and is not itself a word end. The characters you pass form the longest common prefix.
Complexity: O(total characters) to build, O(answer length) to walk.
std::string longestCommonPrefix(const std::vector<std::string>& words) {
if (words.empty()) return "";
struct Node { std::unordered_map<char, Node*> children; bool isEnd = false; };
Node* root = new Node();
for (const auto& w : words) {
Node* node = root;
for (char ch : w) {
if (!node->children.count(ch)) node->children[ch] = new Node();
node = node->children[ch];
}
node->isEnd = true;
}
std::string lcp;
Node* node = root;
while (node->children.size() == 1 && !node->isEnd) {
auto it = node->children.begin();
lcp += it->first;
node = it->second;
}
return lcp;
}
class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
boolean isEnd = false;
}
String longestCommonPrefix(String[] words) {
if (words.length == 0) return "";
TrieNode root = new TrieNode();
for (String w : words) {
TrieNode node = root;
for (char ch : w.toCharArray()) {
node.children.putIfAbsent(ch, new TrieNode());
node = node.children.get(ch);
}
node.isEnd = true;
}
StringBuilder lcp = new StringBuilder();
TrieNode node = root;
while (node.children.size() == 1 && !node.isEnd) {
char ch = node.children.keySet().iterator().next();
lcp.append(ch);
node = node.children.get(ch);
}
return lcp.toString();
}
function longestCommonPrefix(words) {
if (words.length === 0) return "";
const root = { children: new Map(), isEnd: false };
for (const w of words) {
let node = root;
for (const ch of w) {
if (!node.children.has(ch))
node.children.set(ch, { children: new Map(), isEnd: false });
node = node.children.get(ch);
}
node.isEnd = true;
}
let lcp = "";
let node = root;
while (node.children.size === 1 && !node.isEnd) {
const [ch, child] = node.children.entries().next().value;
lcp += ch;
node = child;
}
return lcp;
}
def longest_common_prefix(words):
if not words:
return ""
END = "$"
root = {}
for w in words:
node = root
for ch in w:
node = node.setdefault(ch, {})
node[END] = True
lcp = []
node = root
while len(node) == 1 and END not in node:
ch, child = next(iter(node.items()))
lcp.append(ch)
node = child
return "".join(lcp)
Pitfall: Forgetting the
isEndcheck makes the prefix run past a complete shorter word — e.g. for["car", "card"]the LCP is"car", not"card". Stop at any word end.
Q6. Wildcard search with .
Idea: Insert words normally. For search, recurse character by character: a normal character follows the single matching child; a . tries every child. Success means reaching the word’s end with isEnd set.
Complexity: Normal queries are O(L). With d dots the worst case is roughly O(branching^d · L); for a query of all dots it visits every node, so O(N) where N is the number of trie nodes.
class WordDictionary {
struct Node {
std::unordered_map<char, Node*> children;
bool isEnd = false;
};
Node* root = new Node();
bool dfs(Node* node, const std::string& word, int i) {
if (i == (int)word.size()) return node->isEnd;
char ch = word[i];
if (ch == '.') {
for (auto& [c, child] : node->children)
if (dfs(child, word, i + 1)) return true;
return false;
}
auto it = node->children.find(ch);
if (it == node->children.end()) return false;
return dfs(it->second, word, i + 1);
}
public:
void addWord(const std::string& word) {
Node* node = root;
for (char ch : word) {
if (!node->children.count(ch)) node->children[ch] = new Node();
node = node->children[ch];
}
node->isEnd = true;
}
bool search(const std::string& word) { return dfs(root, word, 0); }
};
class WordDictionary {
private static class Node {
Map<Character, Node> children = new HashMap<>();
boolean isEnd = false;
}
private final Node root = new Node();
public void addWord(String word) {
Node node = root;
for (char ch : word.toCharArray()) {
node.children.putIfAbsent(ch, new Node());
node = node.children.get(ch);
}
node.isEnd = true;
}
public boolean search(String word) { return dfs(root, word, 0); }
private boolean dfs(Node node, String word, int i) {
if (i == word.length()) return node.isEnd;
char ch = word.charAt(i);
if (ch == '.') {
for (Node child : node.children.values())
if (dfs(child, word, i + 1)) return true;
return false;
}
Node next = node.children.get(ch);
return next != null && dfs(next, word, i + 1);
}
}
class WordDictionary {
constructor() { this.root = { children: new Map(), isEnd: false }; }
addWord(word) {
let node = this.root;
for (const ch of word) {
if (!node.children.has(ch))
node.children.set(ch, { children: new Map(), isEnd: false });
node = node.children.get(ch);
}
node.isEnd = true;
}
search(word) {
const dfs = (node, i) => {
if (i === word.length) return node.isEnd;
const ch = word[i];
if (ch === '.') {
for (const child of node.children.values())
if (dfs(child, i + 1)) return true;
return false;
}
const next = node.children.get(ch);
return next !== undefined && dfs(next, i + 1);
};
return dfs(this.root, 0);
}
}
class WordDictionary:
def __init__(self):
self.root = {} # nested dict; "$" marks a word end
def add_word(self, word):
node = self.root
for ch in word:
node = node.setdefault(ch, {})
node["$"] = True
def search(self, word):
def dfs(node, i):
if i == len(word):
return "$" in node
ch = word[i]
if ch == ".":
for key, child in node.items():
if key != "$" and dfs(child, i + 1):
return True
return False
return ch in node and dfs(node[ch], i + 1)
return dfs(self.root, 0)
Pitfall: On a
., iterate the children only — never the end-of-word marker — and remember the dot consumes exactly one character. Treating.as “zero or more” turns this into a different (regex) problem.
Q7. Word search in a grid
Idea: Build a trie of all target words, storing the full word at each end node. Run a DFS from every cell, descending the board and the trie together. When the current trie node holds a word, record it (and clear it to avoid duplicates). Mark visited cells during the path and restore them on backtrack. The trie prunes any direction whose prefix is not in the dictionary.
Complexity: O(W·L) to build the trie over W words, and O(R·C·4^maxLen) worst case for the search over an R×C board, heavily pruned in practice.
struct TNode {
std::unordered_map<char, TNode*> children;
std::string word; // non-empty marks a complete word
};
class Solution {
TNode* root = new TNode();
std::vector<std::string> result;
void insert(const std::string& w) {
TNode* node = root;
for (char ch : w) {
if (!node->children.count(ch)) node->children[ch] = new TNode();
node = node->children[ch];
}
node->word = w;
}
void dfs(std::vector<std::vector<char>>& b, int r, int c, TNode* node) {
if (r < 0 || c < 0 || r >= (int)b.size() || c >= (int)b[0].size()) return;
char ch = b[r][c];
if (ch == '#') return;
auto it = node->children.find(ch);
if (it == node->children.end()) return;
TNode* next = it->second;
if (!next->word.empty()) {
result.push_back(next->word);
next->word.clear(); // dedupe
}
b[r][c] = '#'; // mark visited
dfs(b, r + 1, c, next);
dfs(b, r - 1, c, next);
dfs(b, r, c + 1, next);
dfs(b, r, c - 1, next);
b[r][c] = ch; // restore
}
public:
std::vector<std::string> findWords(std::vector<std::vector<char>>& board,
std::vector<std::string>& words) {
for (auto& w : words) insert(w);
for (int r = 0; r < (int)board.size(); r++)
for (int c = 0; c < (int)board[0].size(); c++)
dfs(board, r, c, root);
return result;
}
};
class Solution {
private static class TNode {
Map<Character, TNode> children = new HashMap<>();
String word = null; // non-null marks a complete word
}
private final TNode root = new TNode();
private List<String> result;
public List<String> findWords(char[][] board, String[] words) {
for (String w : words) insert(w);
result = new ArrayList<>();
for (int r = 0; r < board.length; r++)
for (int c = 0; c < board[0].length; c++)
dfs(board, r, c, root);
return result;
}
private void insert(String w) {
TNode node = root;
for (char ch : w.toCharArray()) {
node.children.putIfAbsent(ch, new TNode());
node = node.children.get(ch);
}
node.word = w;
}
private void dfs(char[][] b, int r, int c, TNode node) {
if (r < 0 || c < 0 || r >= b.length || c >= b[0].length) return;
char ch = b[r][c];
if (ch == '#') return;
TNode next = node.children.get(ch);
if (next == null) return;
if (next.word != null) {
result.add(next.word);
next.word = null; // dedupe
}
b[r][c] = '#'; // mark visited
dfs(b, r + 1, c, next);
dfs(b, r - 1, c, next);
dfs(b, r, c + 1, next);
dfs(b, r, c - 1, next);
b[r][c] = ch; // restore
}
}
function findWords(board, words) {
const root = { children: new Map(), word: null };
for (const w of words) {
let node = root;
for (const ch of w) {
if (!node.children.has(ch))
node.children.set(ch, { children: new Map(), word: null });
node = node.children.get(ch);
}
node.word = w;
}
const rows = board.length, cols = board[0].length;
const result = [];
function dfs(r, c, node) {
if (r < 0 || c < 0 || r >= rows || c >= cols) return;
const ch = board[r][c];
if (ch === '#') return;
const next = node.children.get(ch);
if (!next) return;
if (next.word !== null) {
result.push(next.word);
next.word = null; // dedupe
}
board[r][c] = '#'; // mark visited
dfs(r + 1, c, next);
dfs(r - 1, c, next);
dfs(r, c + 1, next);
dfs(r, c - 1, next);
board[r][c] = ch; // restore
}
for (let r = 0; r < rows; r++)
for (let c = 0; c < cols; c++)
dfs(r, c, root);
return result;
}
def find_words(board, words):
END = "$" # "$" key stores the complete word
root = {}
for w in words:
node = root
for ch in w:
node = node.setdefault(ch, {})
node[END] = w
rows, cols = len(board), len(board[0])
result = []
def dfs(r, c, node):
if not (0 <= r < rows and 0 <= c < cols):
return
ch = board[r][c]
if ch not in node: # '#' (visited) or no such prefix
return
nxt = node[ch]
if END in nxt:
result.append(nxt[END])
del nxt[END] # dedupe
board[r][c] = "#" # mark visited
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
dfs(r + dr, c + dc, nxt)
board[r][c] = ch # restore
for r in range(rows):
for c in range(cols):
dfs(r, c, root)
return result
Pitfall: Forgetting to mark and restore the visited cell lets a path reuse the same letter, producing words the board cannot actually spell. Always set the cell to a sentinel before recursing and restore it after.
That’s the full answer sheet. Back to the trie exercises, revisit trie applications, or continue to graphs.