Search Autocomplete
Type “ja”, see the best 10 words: a trie that keeps each prefix's top list ready.
the brief
What you are building
Open any shopping app's search box and type ja. Before you finish, it suggests jacket, jam, java book…
Those suggestions are the most-searched words that start with what you typed.
You will build that engine. Words come in with a weight (how many times people searched them). For a prefix, return the best k.
must have
Requirements
addWord(word, weight)addsweightto that word's total. A new word starts at 0. Weights only grow.suggest(prefix, k)returns the bestkwords that start withprefix.- "Best" means a higher total weight first. Equal weights go in alphabetical order.
- A word is a prefix of itself:
suggest("java", …)can return"java". - The empty prefix
""matches every word. No match returns an empty list.
the contract
The Java API: class Autocomplete
public Autocomplete()- An empty dictionary.
public void addWord(String word, int weight)worduses onlya–z;weight≥ 1.public List<String> suggest(String prefix, int k)- Up to
kwords, best first.1 ≤ k ≤ 10.
- 1 ≤ word length ≤ 20, letters
a–zonly - 1 ≤ weight ≤ 1,000; 1 ≤ k ≤ 10
- up to 10,000 calls
- suggest is called far more often than addWord in real life — make it fast
example session
Calls in, answers out
Four words, then a weight update.
| call | returns | why |
|---|---|---|
new Autocomplete() | — | |
addWord("java", 5) | — | |
addWord("javascript", 3) | — | |
addWord("jar", 5) | — | |
addWord("json", 2) | — | |
suggest("ja", 2) | ["jar", "java"] | both 5; "jar" < "java" alphabetically |
suggest("j", 10) | ["jar", "java", "javascript", "json"] | fewer than k matches |
addWord("json", 4) | — | json is now 6 |
suggest("j", 2) | ["json", "jar"] | |
suggest("py", 3) | [] | |
suggest("java", 3) | ["java", "javascript"] | the prefix itself is a word |
time plan
Your 90 minutes
- 10 minclarify — How are ties broken? Does adding a word twice add the weights? Is
""a valid prefix? What is the biggest k? - 10 mindesign — Draw the trie for java/jar/json. Decide: search the subtree on every query, or keep a ready top list at each node?
- 40 mincore code — Trie insert and walk-down first. Then the simple version (DFS + heap). Run the example. Upgrade to cached lists if time allows.
- 20 minedge cases — Empty prefix, no match, prefix longer than every word, ties, a word that climbs after its weight grows.
- 10 minpolish — Name the trade-off: faster
suggestcosts memory and sloweraddWord.
Practise under real pressure: a 90-minute clock with the phases above. It keeps running if you reload the page.
your turn
Build it here
Write the whole class in the editor. Run replays the example session; Submit adds the hidden tests (edge cases and large random sessions). Stuck for 20 minutes? Open one hint at a time.
Build Autocomplete in the browser
Design: implement every operation. Compiled by the real javac (Java 17) and judged right here — nothing is sent anywhere.
The reference, the data-structure reasoning and the rubric are hidden so you can build it first. Even a half-working attempt teaches you more than reading a perfect answer.
Opening all three hints also shows it.
reference
One clean way to build it
The version to get working first: a plain trie, a DFS over the prefix's subtree, and a size-k heap. Each suggest is O(p + S + m log k), where S is the number of trie nodes under the prefix and m the words among them.
class AutocompleteDfs { static class Node { final Node[] next = new Node[26]; // 'a'..'z' int weight; // > 0 only where a word ends } private final Node root = new Node(); public void addWord(String word, int weight) { Node node = root; for (char ch : word.toCharArray()) { int i = ch - 'a'; if (node.next[i] == null) node.next[i] = new Node(); node = node.next[i]; } node.weight += weight; // the same word again: the weights add up } public List<String> suggest(String prefix, int k) { Node node = root; for (char ch : prefix.toCharArray()) { // walk down the prefix: O(p) node = node.next[ch - 'a']; if (node == null) return new ArrayList<>(); } // Collect every word under the prefix, keep the best k in a min-heap (worst of the best on top). Comparator<Map.Entry<String, Integer>> better = (a, b) -> !a.getValue().equals(b.getValue()) ? Integer.compare(b.getValue(), a.getValue()) : a.getKey().compareTo(b.getKey()); PriorityQueue<Map.Entry<String, Integer>> best = new PriorityQueue<>(better.reversed()); collect(node, new StringBuilder(prefix), best, k); List<String> result = new ArrayList<>(); while (!best.isEmpty()) result.add(best.poll().getKey()); // worst first… Collections.reverse(result); // …so flip it return result; } /** Visits the whole subtree: O(size of the subtree · log k). Recursion depth = word length. */ private void collect(Node node, StringBuilder path, PriorityQueue<Map.Entry<String, Integer>> best, int k) { if (node.weight > 0) { best.add(Map.entry(path.toString(), node.weight)); if (best.size() > k) best.poll(); // drop the worst } for (int i = 0; i < 26; i++) { if (node.next[i] == null) continue; path.append((char) ('a' + i)); collect(node.next[i], path, best, k); path.deleteCharAt(path.length() - 1); } }}Every node keeps its best 10 words, so suggest is O(p + k). addWord pays O(L × 10) to keep the lists right.
class Autocomplete { static final int MAX_K = 10; // the spec promises k <= 10, so each node keeps its best 10 static class Node { final Node[] next = new Node[26]; final List<String> top = new ArrayList<>(); // best words under this node, best first, at most MAX_K } private final Node root = new Node(); private final Map<String, Integer> weights = new HashMap<>(); // word → total weight public void addWord(String word, int weight) { weights.merge(word, weight, Integer::sum); Node node = root; update(node.top, word); // the root answers the empty prefix "" for (char ch : word.toCharArray()) { int i = ch - 'a'; if (node.next[i] == null) node.next[i] = new Node(); node = node.next[i]; update(node.top, word); // every prefix of the word may now rank it differently } } /** * Puts word in its right place in one node's list, then trims it to MAX_K. O(MAX_K). * Safe because weights only grow: a word that fell off a list can come back only when its own weight * grows, and that call walks this same path. */ private void update(List<String> top, String word) { top.remove(word); int w = weights.get(word); int at = 0; while (at < top.size() && isBetter(top.get(at), weights.get(top.get(at)), word, w)) at++; top.add(at, word); if (top.size() > MAX_K) top.remove(top.size() - 1); } private static boolean isBetter(String a, int wa, String b, int wb) { return wa != wb ? wa > wb : a.compareTo(b) < 0; } public List<String> suggest(String prefix, int k) { Node node = root; for (char ch : prefix.toCharArray()) { // O(p), then the answer is already waiting node = node.next[ch - 'a']; if (node == null) return new ArrayList<>(); } return new ArrayList<>(node.top.subList(0, Math.min(k, node.top.size()))); }}the choices
Which data structures, and why
Node[26] children)All words that share a prefix share one path. Walking a prefix is O(p), no matter how many words exist.
Memory: one node per distinct prefix.
While visiting the subtree, keeps only the best k seen so far; the worst of them sits on top, ready to be dropped.
suggest = O(p + S + m log k): the DFS visits all S trie nodes under the prefix, and each of the m words there passes through the heap. Fine when the subtree is small; slow for "a" on a big dictionary.
The answer for each prefix is precomputed. suggest just copies the first k.
suggest O(p + k). addWord O(L × 10) for a word of length L. Memory: up to 10 references per node.
HashMap<String, Integer>Word → total weight, so ordering inside the lists can look weights up.
O(1) average per lookup.
self-check
Mark yourself like the interviewer would
Tick only what your code really does. Nothing is saved or sent — this is just for you.
your honest self-score: 0 / 100
traps
Common mistakes
- Looping over every word with
startsWithon each keystroke. With lakhs of words that is lakhs of string checks per letter typed. - Forgetting the root's list, so the empty prefix
""returns nothing. - Updating only the word's last node, so shorter prefixes never see the new weight.
- Keeping the word twice in a top list after its weight changes: remove it before re-inserting.
- Building the result with the heap's order (worst first) and forgetting to reverse it.
what they ask next
Follow-ups and extensions
Weights can now go down (a word becomes less popular). What breaks?
A trimmed list may now need a word that fell off earlier, and nothing brings it back. Options: keep more than 10 per node as a buffer, or rebuild a node's list from its subtree when its 10th word drops.
Support typos: jvaa should still suggest java.
Search the trie while allowing up to 1 or 2 edits (insert, delete, change a letter) — a DFS that carries an edit budget. It is the edit-distance DP done along trie paths.
The dictionary is 1 crore words. Memory?
A Node[26] per node wastes space. Use a HashMap of children, or compress chains of single children (a radix tree).
Keep top lists only for short prefixes (length ≤ 3), where queries are most common.
Personalise: Rahul's own searches should rank higher for him.
Keep a tiny per-user map of recent words. At query time, merge his matches with the global top list using a small heap.
revise