</> MAANG.io
coding interview · 201

Intermediate

Advanced coding interview preparation covering complex algorithms, system design, and optimization techniques for senior-level positions.

0/182 solved 0% complete

Trie Design

What is this?

A trie is a filing cabinet where the drawers are letters. To file card you open drawer c, then a, then r, then d, and mark that last drawer "a word ends here". Everything beginning car shares the first three drawers, which is why the structure is compact and why asking "is there anything filed under car?" takes three steps regardless of how many words exist.

This chapter builds that, then stretches it twice: once by allowing a wildcard in the query, and once by noticing that the drawers do not have to be letters.

Trie node anatomy: children and is_end enabling search, wildcards, filesystems

💡 Fun fact: Design In-Memory File System is a trie in disguise, and seeing that collapses it from a fiddly design problem into about twenty lines. A path is a list of components, a directory is a node whose children are its entries, and a file is a node carrying content instead of children. ls then becomes "return this node's children, sorted" — and because the sorting requirement is stated in the problem, using an ordered map or sorting on read is a deliberate choice worth mentioning.

🔓 The 3 problems in this chapter are free. Sign in with Google or Microsoft to start solving.


The one-line idea: a node holds a map from label to child plus a flag saying whether a key ends here. Exact lookup follows one label per step; a wildcard follows all of them; and the labels can be any unit you like.


1. The structure

class Node:
    def __init__(self):
        self.children = {}      # label -> Node
        self.is_end = False     # a complete key finishes here

def insert(root, word):
    node = root
    for ch in word:
        node = node.children.setdefault(ch, Node())
    node.is_end = True

is_end is not optional. Without it a trie containing card would report car as present, because the path exists — the flag is what distinguishes "a key ends here" from "a key passes through here".

2. Wildcards mean branching

def search(node, word, i):
    if i == len(word):
        return node.is_end
    ch = word[i]
    if ch == '.':
        return any(search(child, word, i + 1) for child in node.children.values())
    return ch in node.children and search(node.children[ch], word, i + 1)

A concrete character narrows to one child; a . must try them all. That is why the cost is O(26^d · L) for d wildcards — each dot multiplies the branching. A query of all dots inspects the entire trie, which is worth stating when the interviewer asks about complexity.

3. The labels need not be characters

For a filesystem, split the path and use the components:

def resolve(root, path):
    node = root
    for part in path.split('/'):
        if part:                                   # skip the empty leading segment
            node = node.children.setdefault(part, Node())
    return node

mkdir is resolve and stop. addContentToFile is resolve then append to that node's content. ls is: resolve, and if the node is a file return its own name, otherwise return its children's names sorted. The whole API is three lines of trie walking plus that one branch.


4. A 30-second worked example

Insert cat, car, card; then query c.r:

Worked example: search 'c.r' over a trie of cat, car, card

Three words, six nodes — the shared prefix ca is stored once. And note search("ca") returns False despite that path existing, because no key ends there.


5. Where you'll actually meet this

  • Autocomplete. Each keystroke descends one node; the subtree below is the candidate set.
  • Spell-check with wildcards. Matching patterns with unknown characters is the . branch.
  • Filesystems and object stores. Path hierarchies, and prefix-based listing APIs.
  • Configuration and routing tables. Hierarchical keys where the longest matching prefix wins.
  • Contact and command lookup. Any interface where partial input must narrow a set of options live.

6. Problems in this chapter

▶ Add and Search Word (Wildcard)

Support adding words and searching with . matching any single character. Exact characters descend; wildcards branch to every child.
Pattern: trie with DFS on wildcards. Target: O(L) add, O(26^d · L) search.

▶ Design In-Memory File System

Implement ls, mkdir, addContentToFile and readContentFromFile. A trie over path components, where files carry content and directories carry children.
Pattern: trie with non-character labels. Target: O(path length) per operation, plus sorting for ls.


7. Common pitfalls 🚫

  • Omitting is_end. Every prefix of a stored word would report as present.
  • Treating a wildcard as a single wrong guess. It must try every child, which means recursion rather than iteration.
  • Not handling the empty path segment. Splitting "/a/b" yields a leading empty string that must be skipped.
  • Returning unsorted ls output. The problem requires lexicographic order; decide whether to sort on write or on read.
  • Confusing a file with a directory. ls on a file returns the file's own name, not its (nonexistent) children.
  • A 26-slot array for path components. Fine for lowercase letters, useless for arbitrary names — use a dictionary.
  • Recursing per character on a very long word. Deep tries can exhaust the stack; iterate for exact search and recurse only for wildcards.
  • Longest Word in Dictionary — the longest word buildable one character at a time, every prefix also being a word. Insert everything, then walk the trie only through nodes marked as complete words — the prefix property does the filtering.

8. Key takeaways

  1. A node is children plus an end-flag. The flag is what makes membership different from prefix existence.
  2. Exact search narrows; wildcards branch. That difference is the whole cost model.
  3. Labels can be anything — characters, path components, bits.
  4. A filesystem is a trie, and recognising that turns a design question into a walk.
  5. Sorted listing is a stated requirement, so choose where to pay for it.
  6. Dictionaries beat fixed arrays for anything but a small known alphabet.
  7. Why interviewers like it: the structure takes five minutes, leaving the whole interview for the extensions — wildcards, deletion, sorted listing, memory trade-offs.

Order: Add and Search Word (Wildcard) → Design In-Memory File System → Longest Word in Dictionary.

Core idea: This is a trie (101's Implement Trie) with one twist in the query: the search string may contain ., a wildcard that matches any single character. A normal character still follows exactly one child, so the walk stays a single path. But a . has no fixed letter to follow — so the search forks, recursing into every child of the current node. Search stops being a straight walk down one path and becomes a DFS over a branching set of paths, succeeding if any branch reaches a word-end at the right length.

Problem, rephrased

You're building the lookup engine behind a crossword assistant: the word list loads first, then solvers probe it with partially filled rows. You're given words one at a time via addWord(word), and pattern queries via search(word), where a solver types the letters already placed in a row and a . for each blank square. Both operations live in one structure, WordDictionary.

search(word) returns True if any stored word matches the pattern: a letter matches only itself, a . matches any single letter, and the match must be exact length — the answer has to fill the row precisely, so a stored word fits only if it ends exactly where the pattern does. All strings are non-empty and use lowercase a–z; . appears only in search calls.

Strip the story away and this is Add and Search Word (LeetCode 211): design a data structure that supports adding words and searching with patterns in which . is a wildcard matching exactly one character.

Here's a transcript on a fresh dictionary, top to bottom:

Call Returns Why
addWord("bad") — store bad
addWord("dad") — store dad
addWord("mad") — store mad
search("pad") False no stored word is pad
search("bad") True exact word bad exists
search(".ad") True . matches b/d/m; bad, dad, mad all fit
search("b..") True b then any two letters — bad matches

Rows 6 and 7 are the whole problem: a . doesn't commit to a letter, so the search has to try every branch and accept if even one lands on a word-end.

Sign in to continue reading

The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.

Sign in to read the full lesson

Sign in to MAANG.io

Use your Google or Microsoft account — no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.