Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Trees, Heaps & Tries  ›  Lesson

Tries & Search-as-you-type

Tries 25 minA tree of characters where titles that start alike share a path
You're building a piece ofTunebox — discovery
This piece — suggest_titles(): Completes a title from its first few letters.
Scenario As a listener types into Tunebox's search box, suggestions update on every keystroke. The titles live in a trie, so each keystroke walks only as far as the letters typed.
Your task
Build suggest_titles(titles, prefix, limit). Insert every title into a trie, walk down to the prefix, and return up to limit titles that start with it, in alphabetical order, each title once. Example: suggest_titles(["neon", "nebula", "night drive", "needle", "nectar"], "ne", 3) → ["nebula", "nectar", "needle"].

Tries & Search-as-you-type

Type "ne" into Tunebox's search box and suggestions should appear before the next keystroke. Checking title.startswith("ne") against every title works for a few hundred songs, but with millions, every keystroke rescans the lot. A trie (usually said "try", from retrieval) lets titles that start alike share the same path, so a prefix is found in as many steps as it has characters.

One character per edge

A trie is a tree whose edges are characters. Every title is a path down from the root, and titles that begin the same way share the start of that path. Each node holds a dict of children and a flag saying whether a title ends there:

root ─ n ─ e ─┬─ o● ─ n●         ● = a title ends here
              └─ t●              titles: neo, neon, net
python

❓ Cross-question — "Why the is_end flag? If the path exists, isn't it a title?" No. Inserting "neon" creates the path n-e-o on its way down, but "neo" is only a title if someone inserted it. Without the flag, every prefix of every title would pass as a title too.

search and starts_with

Both walk down one character at a time and give up the moment a character is missing. They differ only in the question they ask at the end:

What does this print?
class TrieNode:
    def __init__(self):
        self.children, self.is_end = {}, False

def walk(root, text):
    node = root
    for ch in text:
        if ch not in node.children:
            return None
        node = node.children[ch]
    return node

root = TrieNode()
node = root
for ch in "neon":
    node = node.children.setdefault(ch, TrieNode())
node.is_end = True

found = walk(root, "neo")
print(found is not None and found.is_end, found is not None)

So search(word) is node is not None and node.is_end, while starts_with(prefix) is just node is not None. Each costs O(m) for m characters, no matter how many titles the trie holds.

Tries as nested dicts

Python's dicts make a very compact trie: every node is a dict, and a special key marks the end of a title. Step through two inserts and watch the structure grow:

step through it
1trie = {}
2for title in ["neo", "net"]:
3 node = trie
4 for ch in title:
5 node = node.setdefault(ch, {})
6 node["$"] = True
7print(trie)

setdefault(ch, {}) returns the child if it exists, and otherwise creates it and returns that — a whole insert in three lines. The class version is clearer and easier to extend with extra fields; you'll see the dict version in quick scripts and interviews.

Collecting completions

To suggest titles, walk to the prefix's node, then go depth-first through everything below it, adding a letter at each step. Visit children in sorted order and the titles come out alphabetically, with no sort at the end. This version loses titles:

broken — fix it

Every title starting with "ne" should be listed, but "neon" is missing, because the walk stops too early.

Why is that DFS order alphabetical? A title ending at a node is recorded before anything longer below it, and Python compares strings the same way:

What does this print?
print(sorted(["neon", "neo lights", "neo"]))

Coming from Java/JS: a Map<Character, TrieNode>, or a JS Map per node, is the same design. Many Java tutorials use a fixed TrieNode[26] array instead — faster for lowercase a–z, and useless the moment a title contains a space, a digit or "é".

Tries vs a sorted list with bisect

A trie isn't the only way to answer prefix questions. Sort the titles once, and all the titles starting with "ne" sit in one unbroken block — bisect finds where it begins:

python

That costs O(p log n) per query and almost no extra memory. The price is insertion: adding a title shifts the list, O(n). A trie flips the trade-off. An insert is only O(m), but every character may get its own node with its own dict — easily tens of times the memory of the strings themselves.

❓ Cross-question — "So when is a trie actually worth it?" When titles change often; when nodes carry useful data, like how many titles pass through or the best few suggestions; or when queries branch, as in wildcards and spell-checking. For a static list you search now and then, bisect wins.

Operation Trie Sorted list + bisect
Insert a title of length m O(m) O(n) — items shift
Exact search O(m) O(m log n)
Find the block for a prefix of length p O(p) O(p log n)
List the k matches O(size of that subtree) O(k · p)
Memory a node per distinct prefix just the strings

Idioms & real-world patterns

Counting at every node

Keep a counter on each node and add 1 on the way down during insert. "How many titles start with ne?" then costs O(p), with no subtree walk:

python

Real autocomplete goes one step further and keeps the top few suggestions on each node, updated as titles are inserted, so a keystroke returns ranked results immediately. Routers use tries to match IP address prefixes, and spell-checkers use them to explore near-miss words.

⚡ Advanced — radix trees

Long chains of single-child nodes waste memory. A radix tree, or compressed trie, merges each such chain into one edge labelled with a whole string, such as "eon". Lookups work the same way, with far fewer nodes.


🎯 Your turn

Write suggest_titles(titles, prefix, limit). Build a trie from titles (Tunebox stores them lowercase), walk to prefix, and return up to limit titles that start with it, alphabetically, each title once:

  • suggest_titles(["neon", "nebula", "night drive", "needle", "nectar"], "ne", 3)["nebula", "nectar", "needle"]
  • suggest_titles(["neon", "nebula"], "x", 5)[]

Hint — insert every title, then walk the prefix and return [] if a character is missing. Finish with a DFS over sorted children that stops once it has limit titles.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of Tunebox is built. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upIs it a title, or just a prefix?

Write has_title(titles, query). Insert every title into a trie, then return True only if query is a whole title — not just the start of one. TrieNode is provided.

trie
DrillHow many titles start with it?

Write count_prefix(titles, prefix) returning how many of the (distinct) titles start with prefix. Store a count on every trie node during insert, so the query itself only walks len(prefix) steps. An empty prefix matches every title.

prefix-treedict
BuildThe longest shared prefix

Write shared_prefix(titles) returning the longest prefix that every title starts with, using a trie. ["neon", "nebula", "needle"]"ne". No titles → "". TrieNode is provided.

trie
BossCollapse tags to their stems

Tunebox groups listener tags by stem. Write shorten_tags(stems, tags) returning a new list where each tag is replaced by the shortest stem it starts with; a tag that starts with no stem is kept unchanged. shorten_tags(["remix", "live"], ["remixed", "lives", "cover"])["remix", "live", "cover"].

trieprefix-tree
CapstoneHalf-remembered titles

Write match_pattern(titles, pattern) returning every title that matches pattern, where a . matches any one character and every other character must match exactly. A match must be the same length as the pattern. Return the matches alphabetically, each once. match_pattern(["neon", "noon", "moon"], "n..n")["neon", "noon"].

trierecursion
suggest_titles.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
suggest_titles(titles, prefix, limit) → listUp to limit titles starting with prefix, alphabetically.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.