Skip to main content

Design Add and Search Words Data Structure

How to use a Trie with DFS to support the . wildcard through controlled branching.

Design a structure that supports:

  • addWord(word)
  • search(word)

search must accept . as a wildcard that matches exactly one character.

In the original problem, this is usually a class. In the SeniorPath playground, use:

  • operations: the list of operations
  • values: the word associated with each operation, or null when there is none

Return an array with the result of each operation. Operations that do not return a value should return null.

Example:

Input:
  operations = ["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
  values     = [null,"bad","dad","mad","pad","bad",".ad","b.."]

Output:
  [null,null,null,null,false,true,true,true]

What to notice before coding

Common mistakes

Step 1 - Understand why an array is a weak baseline

If you store all words in a list, addWord is easy.

But search becomes:

  • loop through every word
  • compare character by character
  • treat . as a special case

It works, but search gets worse as the dictionary grows.

Step 2 - Ask what the problem wants to reuse

Words share prefixes all the time:

  • bad
  • bag
  • bat

They all share the prefix ba.

If you store that prefix once, the structure gets smarter.

Step 3 - Arrive at the trie

In a Trie:

  • each edge represents one character
  • each node represents a prefix
  • a marker tells you whether that node closes a complete word

addWord walks character by character and creates children when needed.

Step 4 - Understand why wildcard needs DFS

When the character is normal, search knows exactly which child to follow.

When the character is ., it must try every possible child from that node.

That turns search into a small DFS over the trie.

The interactive editor needs JavaScript. You can still read the prompt and copy the starter code below.

Starter code

export function runWordDictionary(operations, values) {
  // operations example: ["WordDictionary", "addWord", "search"]
  // values example: [null, "bad", ".ad"]
  // your solution here
  return []
}
solution.ts
Stuck? Hints available.

Haven't solved it yet?

Viewing the solution now may reduce learning. It's worth trying a bit more.

Open the reference solution

Without JavaScript, the reference solution is shown inline instead of in a dialog.

Solution

Final complexity

Time: O(word.length) for addWord; search depends on how many trie nodes wildcards force you to explore

Space: O(total inserted characters)

Good baseline because it solves the problem without changing the data structure.

export function runWordDictionary(operations, values) {
  const words = []
  const result = []

  function matches(pattern, word) {
    if (pattern.length !== word.length) {
      return false
    }

    for (let index = 0; index < pattern.length; index += 1) {
      if (pattern[index] !== "." && pattern[index] !== word[index]) {
        return false
      }
    }

    return true
  }

  for (let index = 0; index < operations.length; index += 1) {
    const operation = operations[index]
    const value = values[index]

    if (operation === "WordDictionary") {
      result.push(null)
    } else if (operation === "addWord") {
      words.push(value)
      result.push(null)
    } else if (operation === "search") {
      result.push(words.some((word) => matches(value, word)))
    }
  }

  return result
}

It works.

But every search scans the whole list, even when many words share prefixes.

Now the structure matches the problem:

  • addWord walks down the trie creating nodes
  • search is guided by the pattern
  • . branches, but only from the current node
type TrieNode = {
  children: Map<string, TrieNode>
  isWord: boolean
}

function createNode(): TrieNode {
  return {
    children: new Map(),
    isWord: false,
  }
}

export function runWordDictionary(operations, values) {
  const root = createNode()
  const result = []

  function addWord(word: string) {
    let node = root

    for (const char of word) {
      if (!node.children.has(char)) {
        node.children.set(char, createNode())
      }

      node = node.children.get(char)!
    }

    node.isWord = true
  }

  function search(word: string): boolean {
    function dfs(node: TrieNode, index: number): boolean {
      if (index === word.length) {
        return node.isWord
      }

      const char = word[index]

      if (char !== ".") {
        const next = node.children.get(char)
        return next ? dfs(next, index + 1) : false
      }

      for (const next of node.children.values()) {
        if (dfs(next, index + 1)) {
          return true
        }
      }

      return false
    }

    return dfs(root, 0)
  }

  for (let index = 0; index < operations.length; index += 1) {
    const operation = operations[index]
    const value = values[index]

    if (operation === "WordDictionary") {
      result.push(null)
    } else if (operation === "addWord") {
      addWord(value)
      result.push(null)
    } else if (operation === "search") {
      result.push(search(value))
    }
  }

  return result
}

The strong point is not only “use a trie”.

It is noticing that wildcard does not mean “compare everything”.

It means “branch only when the pattern actually forces branching”.

What to say in the interview

A good short explanation would be:

The baseline keeps words in an array and compares everything character by character on each search. That works, but it ignores shared prefixes. The stronger version uses a trie to store those prefixes once. In search, a normal character follows one child and . becomes a DFS across the children of the current node. That way the structure matches the rule of the problem.

That shows that you:

  • picked the right structure for prefix sharing
  • understood the cost of wildcard branching
  • explained why DFS appears
  • adapted a class-style API to the playground format

When this pattern shows up again

The same idea comes back when the problem asks for:

  • prefix lookup
  • autocomplete
  • word dictionaries
  • single-character wildcard matching

If the central question is “how do I reuse common prefixes?”, trie becomes a strong candidate.

Previous challenge Combination Sum

Related challenges

Related articles