DEV Community

Timevolt
Timevolt

Posted on

Autocomplete Like a Jedi: Building a Trie from Scratch

The Quest Begins (The "Why")

Ever stared at a search box, typed “jav” and watched the suggestions pop up instantly—“JavaScript”, “Java”, “JAVA_HOME”—and wondered how the heck it knows what you want before you’ve even finished typing? I had that moment while building a side‑project that needed instant keyword hints. My first attempt was a naive loop: for every keystroke I filtered the entire dictionary with word.startswith(prefix). It worked… until the dictionary hit 100 k words. Each keystroke felt like waiting for a dial‑up modem to load a webpage. I was stuck in a loop, literally, and the UI felt sluggish.

I needed a data structure that could skip over words that don’t share the prefix, not scan them all. That’s when the Trie (pronounced “try”) showed up on my radar like a hidden shortcut in a video game—once you know it’s there, the level becomes trivial.

The Revelation (The Insight)

A Trie is basically a tree where each node represents a single character, and the path from the root to a node spells out a prefix. The magic lies in two simple facts:

  1. Prefix sharing – words that start with the same letters share the same nodes.
  2. Early termination – if a node doesn’t exist for the next character, you know no word in the set can have that prefix, so you can stop immediately.

Because of (1), inserting n words of total length L touches each character exactly once → O(L) time. Because of (2), checking whether any word matches a prefix of length p also touches at most p nodes → O(p). No scanning of unrelated words, no wasted work.

Think of it like the Sorting Hat in Harry Potter: it doesn’t ask every student every question; it looks at the traits (letters) you’ve shown so far and instantly knows which house (set of words) you belong to.

Wielding the Power (Code & Examples)

The Struggle – Naïve Filtering

def autocomplete_naive(words, prefix):
    return [w for w in words if w.startswith(prefix)]
Enter fullscreen mode Exit fullscreen mode

Problem: O(|words| * avg_len) per query. With a large dictionary it’s a performance trap—like trying to defeat a boss by swinging at every enemy in the room instead of targeting the weak spot.

The Victory – Trie‑Powered Autocomplete

First, the node definition:

class TrieNode:
    __slots__ = ("children", "is_word")
    def __init__(self):
        self.children = {}          # char -> TrieNode
        self.is_word = False
Enter fullscreen mode Exit fullscreen mode

Now the Trie itself, with insert, search, and the all‑important prefix collection:

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_word = True

    def _collect(self, node, prefix, results):
        """DFS from node, adding completed words to results."""
        if node.is_word:
            results.append(prefix)
        for ch, child in node.children.items():
            self._collect(child, prefix + ch, results)

    def starts_with(self, prefix):
        """Return all words that begin with prefix."""
        node = self.root
        for ch in prefix:
            if ch not in node.children:
                return []               # early exit – no matches
            node = node.children[ch]
        results = []
        self._collect(node, prefix, results)
        return results
Enter fullscreen mode Exit fullscreen mode

Usage:

dictionary = ["apple", "app", "application", "apt", "banana", "band", "bandana"]
trie = Trie()
for w in dictionary:
    trie.insert(w)

print(trie.starts_with("ap"))  # ['app', 'apple', 'application', 'apt']
Enter fullscreen mode Exit fullscreen mode

Common Traps to Avoid

  • Forgetting to mark the end of a word (is_word). Without it, you’d return incomplete prefixes as words.
  • Mutating the shared prefix string in the DFS (e.g., doing prefix += ch and not backtracking). Using prefix + ch creates a new string each call, keeping the recursion clean.
  • Using a list for children when the alphabet is large; a dict gives O(1) average lookup per character.

Real‑World Interview Flavors

  1. LeetCode 208 – Implement Trie (Prefix Tree) – The classic: build insert, search, and startsWith. My Trie above solves it in ~30 lines.
  2. Autocomplete Suggestion Engine – Given a list of queries, return the top k suggestions for a prefix. After gathering matches with starts_with, you can sort by frequency or length and slice the first k. The core work remains O(P + M) where P is prefix length and M is total characters in the matched subtree—still linear, not quadratic.

Why This New Power Matters

With a Trie in your toolbox, you go from “my UI freezes on every keystroke” to “instant, snappy suggestions that delight users.” You can build:

  • Search bars that feel like Google’s autocomplete.
  • IDEs that offer real‑time code completion.
  • Phone‑keypad predictive text (think T9).

And the best part? The structure is tiny—just nodes and pointers—and the operations are embarrassingly simple to reason about. Once you grasp the prefix‑sharing idea, you’ll start seeing tries everywhere: IP routing tables, spell checkers, even biology (DNA sequence indexing).

Your Turn – The Challenge

Grab a list of your favorite movie titles (or any set of strings) and build a Trie that returns all titles starting with a user‑typed prefix. Then, add a frequency counter so the most‑popular titles appear first. Share your code snippet in the comments—let’s see who can make the most lightning‑fast autocomplete!

May the force of prefix sharing be with you. 🚀

Top comments (0)