Tries (Prefix Trees) for String Processing — Practice Tasks¶
All tasks must be solved in Go, Java, and Python. Each task ships with a precise spec and starter code in all three languages. Implement the trie logic and validate against the evaluation criteria. Always test against a brute-force list-of-strings oracle on small inputs before trusting the trie result — every
search/startsWith/count/autocomplete answer must match a linear scan over the inserted words.
Beginner Tasks (5)¶
Task 1 — Insert and search¶
Problem. Implement a Trie with insert(word) and search(word) (exact match). search returns true only if the exact word was inserted.
Input / Output spec. - A sequence of operations: insert X or search X. - For each search, print true or false.
Constraints. - Words are lowercase a–z, length 1 ≤ L ≤ 100. - Up to 10^5 operations.
Hint. Walk character by character creating missing children; mark isEnd at the last node. search returns node != null && node.isEnd.
Starter — Go.
package main
import "fmt"
type Trie struct {
children map[byte]*Trie
isEnd bool
}
func New() *Trie { return &Trie{children: make(map[byte]*Trie)} }
func (t *Trie) Insert(word string) {
// TODO
}
func (t *Trie) Search(word string) bool {
// TODO
return false
}
func main() {
t := New()
t.Insert("cat")
fmt.Println(t.Search("cat")) // true
fmt.Println(t.Search("ca")) // false
}
Starter — Java.
import java.util.*;
public class Task1 {
static class Trie {
Map<Character, Trie> children = new HashMap<>();
boolean isEnd = false;
void insert(String word) { /* TODO */ }
boolean search(String word) { /* TODO */ return false; }
}
public static void main(String[] args) {
Trie t = new Trie();
t.insert("cat");
System.out.println(t.search("cat")); // true
System.out.println(t.search("ca")); // false
}
}
Starter — Python.
class Trie:
def __init__(self):
self.children = {}
self.is_end = False
def insert(self, word):
pass # TODO
def search(self, word):
return False # TODO
if __name__ == "__main__":
t = Trie()
t.insert("cat")
print(t.search("cat")) # True
print(t.search("ca")) # False
Task 2 — startsWith¶
Problem. Add startsWith(prefix) to the trie: return true if any inserted word begins with prefix.
Input / Output spec. Operations insert X / prefix X; print true/false per prefix.
Constraints. Same as Task 1.
Hint. Walk the prefix's characters; if the path exists, return true — do not check isEnd.
Starter — Go.
func (t *Trie) StartsWith(prefix string) bool {
// TODO: walk prefix; return whether the path exists
return false
}
Starter — Java.
boolean startsWith(String prefix) {
// TODO: walk prefix; return whether the path exists
return false;
}
Starter — Python.
Task 3 — Count words with a given prefix¶
Problem. Support countPrefix(prefix): how many inserted words start with prefix (counting duplicates).
Input / Output spec. Operations insert X / count X; print the count per count.
Constraints. Up to 10^5 ops; words lowercase, L ≤ 100.
Hint. Maintain a passCount integer on each node, incremented at every node during insert. countPrefix = walk(prefix).passCount.
Starter — Go.
type Node struct {
children map[byte]*Node
pass int
isEnd bool
}
// Insert must do: node.pass++ at each node on the path.
// CountPrefix: walk to prefix node, return its pass (0 if path breaks).
Starter — Java.
static class Node {
Map<Character, Node> children = new HashMap<>();
int pass = 0;
boolean isEnd = false;
}
// insert: node.pass++ at each node; countPrefix: walk + return pass
Starter — Python.
class Node:
def __init__(self):
self.children = {}
self.pass_ = 0
self.is_end = False
# insert: node.pass_ += 1 at each node; count_prefix: walk + return pass_
Task 4 — List all words in sorted order¶
Problem. Return all inserted distinct words in lexicographic order using a trie traversal (no external sort).
Input / Output spec. Insert a list of words; print them one per line, sorted ascending.
Constraints. Up to 10^4 words, lowercase, L ≤ 50.
Hint. Pre-order DFS visiting children in sorted key order; emit path at each isEnd node.
Starter — Go.
func (t *Trie) Sorted() []string {
// TODO: DFS with sorted child keys, append path at isEnd nodes
return nil
}
Starter — Java.
List<String> sorted() {
// TODO: DFS with sorted child keys (or use a TreeMap), collect isEnd paths
return new ArrayList<>();
}
Starter — Python.
def sorted_keys(self):
out = []
# TODO: dfs visiting sorted(children), append path at is_end
return out
Task 5 — Basic autocomplete¶
Problem. Implement autocomplete(prefix): return all inserted words starting with prefix, in lexicographic order.
Input / Output spec. Insert words, then for a query prefix print all matching words (sorted), or an empty list.
Constraints. Up to 10^4 words, L ≤ 50.
Hint. Walk to the prefix node; if it exists, DFS collecting isEnd paths (seed path with the prefix).
Starter — Go.
func (t *Trie) Autocomplete(prefix string) []string {
// TODO: walk to prefix node; DFS collect, seeding path = prefix
return nil
}
Starter — Java.
List<String> autocomplete(String prefix) {
// TODO: walk to prefix node; DFS collect, seeding path = prefix
return new ArrayList<>();
}
Starter — Python.
def autocomplete(self, prefix):
# TODO: walk to prefix node; DFS collect, seeding path = prefix
return []
Intermediate Tasks (4)¶
Task 6 — Top-k autocomplete by frequency¶
Problem. Each word has an insertion frequency. topK(prefix, k) returns the k highest-frequency words starting with prefix, ties broken lexicographically.
Input / Output spec. Insert (word, freq) pairs; query (prefix, k) → up to k (word, freq) pairs, highest freq first.
Constraints. Up to 10^5 words; 1 ≤ k ≤ 20.
Hint. Store freq at end-of-word nodes. DFS the prefix subtree into a size-k min-heap by (freq, -lexical); drain highest-first.
Starter — Go.
// Node: children map[byte]*Node; freq int
// TopK: walk to prefix; DFS into a size-k min-heap; return drained desc.
func (t *Trie) TopK(prefix string, k int) []struct{ Word string; Freq int } {
return nil // TODO
}
Starter — Java.
// Use a PriorityQueue of size k ordered by frequency (then lexicographic).
List<Map.Entry<String,Integer>> topK(String prefix, int k) {
return new ArrayList<>(); // TODO
}
Starter — Python.
import heapq
def top_k(self, prefix, k):
# TODO: walk; dfs into size-k min-heap; return sorted desc
return []
Task 7 — Word Dictionary with . wildcard¶
Problem. Implement addWord(word) and search(word) where search may contain . matching any single character.
Input / Output spec. Operations add X / search X; print true/false.
Constraints. Words/queries lowercase + ., L ≤ 25; up to 10^4 ops.
Hint. DFS: at a letter descend that child; at . recurse into all children. Match only when the index reaches the word length and the node is isEnd.
Starter — Go.
func (d *WordDictionary) Search(word string) bool {
// TODO: recursive DFS handling '.' as "try all children"
return false
}
Starter — Java.
boolean search(String word) {
// TODO: recursive DFS handling '.' as "try all children"
return false;
}
Starter — Python.
Task 8 — Replace words with shortest root¶
Problem. Given a dictionary of roots and a sentence, replace every word by the shortest root that is a prefix of it (if any). (LeetCode 648.)
Input / Output spec. Read roots and a sentence; print the transformed sentence.
Constraints. Up to 10^3 roots, sentence up to 10^6 chars.
Hint. Build a trie of roots. For each word, walk it through the trie and stop at the first isEnd node — that is the shortest matching root; otherwise keep the word.
Starter — Go.
func replaceWords(dictionary []string, sentence string) string {
// TODO: build trie of roots; for each word, walk until first isEnd
return ""
}
Starter — Java.
String replaceWords(List<String> dictionary, String sentence) {
// TODO: build trie of roots; for each word, walk until first isEnd
return "";
}
Starter — Python.
def replaceWords(self, dictionary, sentence):
# TODO: build trie of roots; for each word, walk until first is_end
return ""
Task 9 — Delete a word (with pruning)¶
Problem. Add delete(word) that removes a word and prunes nodes that become useless (no children and not an end-of-word), without breaking other words.
Input / Output spec. Operations insert/delete/search; print search results.
Constraints. Up to 10^5 ops; verify other words survive deletion.
Hint. Walk down stacking nodes; clear isEnd at the end. Unwind: remove a node only if it has no children and is not isEnd. A passCount field makes the safety check explicit.
Starter — Go.
func (t *Trie) Delete(word string) bool {
// TODO: clear isEnd; prune empty, non-end nodes on the way up
return false
}
Starter — Java.
boolean delete(String word) {
// TODO: clear isEnd; prune empty, non-end nodes on the way up
return false;
}
Starter — Python.
Advanced Tasks (3)¶
Task 10 — Word Search II (trie + grid backtracking)¶
Problem. Given an m×n board of letters and a list of words, return all words found in the board (adjacent horizontally/vertically, no cell reused per word). (LeetCode 212.)
Input / Output spec. Read the board and words; print all found words.
Constraints. Board up to 12×12, up to 3·10^4 words.
Hint. Build a trie of the words. DFS from each cell, descending the trie in lockstep with the path; when you reach an isEnd node, record the word. Prune branches that leave the trie. Mark isEnd=false (or remove leaf) after finding to dedup.
Starter — Go.
func findWords(board [][]byte, words []string) []string {
// TODO: build trie; DFS each cell in lockstep with trie descent
return nil
}
Starter — Java.
List<String> findWords(char[][] board, String[] words) {
// TODO: build trie; DFS each cell in lockstep with trie descent
return new ArrayList<>();
}
Starter — Python.
def findWords(self, board, words):
# TODO: build trie; dfs each cell in lockstep with trie descent
return []
Task 11 — Longest-prefix match (IP routing)¶
Problem. Given routing rules (prefix, length, nextHop) over 32-bit addresses, answer lookup(addr) returning the next hop of the longest matching prefix (default route /0 is the fallback).
Input / Output spec. Insert rules, then queries; print the next hop per query.
Constraints. Up to 10^5 rules and queries.
Hint. Binary trie over address bits (MSB first). Mark nodes with their next hop. On lookup, descend bit by bit and remember the deepest marked node seen.
Starter — Go.
// RNode { child [2]*RNode; hop string; has bool }
func (rt *RouteTrie) Lookup(addr uint32) (string, bool) {
// TODO: descend 32 bits; keep deepest marked hop (default = root mark)
return "", false
}
Starter — Java.
String lookup(long addr) {
// TODO: descend 32 bits; keep deepest marked hop (default = root mark)
return null;
}
Starter — Python.
def lookup(self, addr):
# TODO: descend 32 bits; keep deepest marked hop (default = root mark)
return None
Task 12 — Maximum XOR of two numbers (binary trie pointer)¶
Problem. Given an array of integers, find the maximum XOR of any two elements using a binary trie over the bits. (LeetCode 421.) This is the bit-trie cousin of the string trie; the structure is identical with a 2-symbol alphabet. See sibling 18-bit-manipulation/06 for full coverage.
Input / Output spec. Read the array; print the maximum pairwise XOR.
Constraints. Up to 2·10^5 numbers, values fit in 32 bits.
Hint. Insert each number's 32 bits (MSB first) into a binary trie. For each number, greedily descend toward the opposite bit at each level to maximize XOR; sum the bit values where you succeed.
Starter — Go.
func findMaximumXOR(nums []int) int {
// TODO: binary trie; for each num greedily take the opposite bit
return 0
}
Starter — Java.
int findMaximumXOR(int[] nums) {
// TODO: binary trie; for each num greedily take the opposite bit
return 0;
}
Starter — Python.
def findMaximumXOR(self, nums):
# TODO: binary trie; for each num greedily take the opposite bit
return 0
Evaluation Criteria¶
| Criterion | Requirement |
|---|---|
| Correctness | Matches a brute-force list-of-strings oracle on random inputs. |
| Complexity | Core ops O(L); autocomplete O(L + subtree); no accidental O(n·L) scans where a trie walk suffices. |
search vs startsWith | Distinguished correctly (the isEnd check). |
| Deletion (Task 9) | Other words survive; no orphaned nodes; counts stay consistent. |
| Memory | Sensible children representation for the alphabet; no Σ-array for Unicode. |
| Edge cases | Empty string, duplicate inserts, prefix-of-another, missing path, wildcard all-dots. |
| Languages | Working solution in Go, Java, and Python. |
Testing Harness Sketch (Python oracle)¶
import random, string
def oracle_search(words, q):
return q in words
def oracle_starts(words, p):
return any(w.startswith(p) for w in words)
def fuzz():
for _ in range(2000):
words = set()
t = Trie()
for _ in range(random.randint(0, 30)):
w = "".join(random.choices("ab", k=random.randint(1, 6)))
words.add(w); t.insert(w)
q = "".join(random.choices("ab", k=random.randint(1, 6)))
assert t.search(q) == oracle_search(words, q)
assert t.starts_with(q) == oracle_starts(words, q)
print("ok")
# fuzz() # run after implementing Trie
Run the fuzzer after each task; mismatches localize bugs faster than any debugger.
Further Reading¶
- LeetCode 208, 211, 212, 421, 648, 720, 1032, 1268 — trie-tagged problems mapped to these tasks.
- Sibling
17-string-algorithms/05— Aho-Corasick (Task 10's multi-pattern cousin). - Sibling
18-bit-manipulation/06— binary/XOR tries (Task 12). - Sibling
21-advanced-structures/24-25— radix tries and TSTs for memory-optimized follow-ups. - Algorithms (Sedgewick & Wayne), Chapter 5 — string symbol tables and tries.