Design Add and Search Words Data Structure
Problem
Design a data structure that supports adding new words and finding if a string matches any previously added string.
Implement the WordDictionary class:
WordDictionary()Initializes the object.void addWord(word)Addswordto the data structure, it can be matched later.bool search(word)Returnstrueif there is any string in the data structure that matcheswordorfalseotherwise.wordmay contain dots'.'where dots can be matched with any letter.
Examples
Example 1
Input:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
Output:
[null,null,null,null,false,true,true,true]
Explanation
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // return False
wordDictionary.search("bad"); // return True
wordDictionary.search(".ad"); // return True
wordDictionary.search("b.."); // return True
Solution
Video explanation
See linked video for a walk-through of trie search variants.
Method 1 - Brute Force Linear Scan (Store Words in List/Set)
Intuition
Keep every added word in a plain list (or array). To answer a search(pattern) we test the pattern against each stored word. The wildcard . only affects per‑character comparison; no preprocessing is needed. This is the simplest design but scales poorly when the number of words grows.
Approach
- Maintain a dynamic list
words. addWord(word): appendwordtowords.search(pattern):- Let
L = len(pattern). Iterate eachwinwords. - Skip if
len(w) != L. - For each position
ifrom0..L-1:- If
pattern[i] == '.'continue (wildcard matches any single char). - Else if
pattern[i] != w[i]-> not a match; break.
- If
- If all positions match, return
true.
- Let
- If no word matches, return
false.
Complexity
- ⏰ Time complexity:
addWord: O(1)amortized append for list orO(L)for set as we have to calculate hashCode before saving in set.- search
- exact:
O(N * L)for list as scans all words ORO(L)for set search with '.' wildcards: still O(N * L)worst case because every position may need checking against each word.
- exact:
- 🧺 Space complexity:
O(N * L)total characters stored; onlyO(1)auxiliary space per query.
Code
Java
class WordDictionary {
private final List<String> words = new ArrayList<>();
public WordDictionary() {}
public void addWord(String word) {
words.add(word);
}
public boolean search(String pattern) {
int L = pattern.length();
outer: for (String w : words) {
if (w.length() != L) continue;
for (int i = 0; i < L; i++) {
char pc = pattern.charAt(i);
if (pc != '.' && pc != w.charAt(i)) {
continue outer;
}
}
return true;
}
return false;
}
}
Python
class WordDictionary:
def __init__(self) -> None:
self.words: list[str] = []
def addWord(self, word: str) -> None:
self.words.append(word)
def search(self, pattern: str) -> bool:
L = len(pattern)
for w in self.words:
if len(w) != L:
continue
for i in range(L):
if pattern[i] != '.' and pattern[i] != w[i]:
break
else:
return True
return False
Method 2 - Trie with Map Children (Substring-Based DFS)
Intuition
Instead of re-checking unrelated words, a Trie groups words by shared prefixes. Each node represents a prefix; descending one character at a time prunes huge portions of the search space. The wildcard . forces branching: we must explore all child nodes at that position. This version uses word.substring(1) at each recursive step for simplicity, trading speed/memory for cleaner code.
Approach
- Node structure:
TrieNodeholds aMap<Character, TrieNode> childrenand a booleanisEndOfWord. addWord(word): Walk characters; create missing child nodes; mark end node.search(pattern): CallsearchInNode(pattern, node)starting atroot.- Base: Empty
pattern-> returnnode.isEndOfWord. - Let
c = pattern[0]. - If
c == '.': recurse into every child withpattern.substring(1); return true if any succeeds. - Else: follow matching child; if absent return false; recurse with
substring(1).
- Base: Empty
- Return the result of the initial call.
Complexity
- ⏰ Time complexity:
addWord: O(L)traverse/create nodes;search exact: O(L)follow one path;search pattern: O(b^k * L)worst case wherek= number of'.'wildcard positions in the query (each can branch to up tob ≤ 26children). Substring creation addsO(L)per level yieldingO(b^k * L^2)in the pathological case. - 🧺 Space complexity:
O(N * L)nodes across all inserted words (one per unique prefix) plus recursion stackO(L)and transient substrings up toO(L^2)worst case.
Code
Java
public class WordDictionary {
static class TrieNode {
Map<Character, TrieNode> children = new HashMap<Character, TrieNode> ();
boolean isEndOfWord;
}
private final TrieNode root;
public WordDictionary() {
root = new TrieNode();
}
// Adds a word into the data structure.
public void addWord(String word) {
TrieNode curr = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (!curr.children.containsKey(c)) {
curr.children.put(c, new TrieNode());
}
curr = curr.children.get(c);
}
curr.isEndOfWord = true;
}
// Returns if the word is in the data structure. A word could
// contain the dot character '.' to represent any one letter.
public boolean search(String word) {
return searchInNode(word, root);
}
private boolean searchInNode(String word, TrieNode node) {
if (word == null || (word.isEmpty())) {
return node.isEndOfWord;
}
char c = word.charAt(0);
if (c == '.') {
for (TrieNode child : node.children.values()) {
if (searchInNode(word.substring(1), child)) {
return true;
}
}
}
//if character is neither equal to the node nor '.', return false
if (!node.children.containsKey(c)) {
return false;
}
return searchInNode(word.substring(1), node.children.get(c));
}
}
Python
class TrieNode:
def __init__(self):
self.children: dict[str, TrieNode] = {}
self.is_end_of_word = False
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word: str) -> None:
curr = self.root
for c in word:
if c not in curr.children:
curr.children[c] = TrieNode()
curr = curr.children[c]
curr.is_end_of_word = True
def search(self, pattern: str) -> bool:
def searchInNode(node: TrieNode, s: str) -> bool:
if not s:
return node.is_end_of_word
c = s[0]
if c == '.':
for child in node.children.values():
if searchInNode(child, s[1:]):
return True
return False
if c not in node.children:
return False
return searchInNode(node.children[c], s[1:])
return searchInNode(self.root, pattern)
Method 3 - Trie with Map Children (Index-Based DFS, No Substrings)
Intuition
Method 2 pays an extra cost creating substring objects at every recursion level. We can avoid that by passing an index into the original pattern string, reducing both time and memory overhead. Logic is identical; only parameterization changes.
Approach
- Use the same
TrieNodeshape as Method 2. addWordunchanged.search(pattern)invokessearchInNode(node, pattern, i)withi = 0.- Recurrence:
- If
i == len(pattern): returnnode.isEndOfWord. - Let
c = pattern[i]. - If
c == '.': iterate all children; if any recursive call withi+1returns true -> propagate true. - Else: fetch child
node.children.get(c); if missing return false; recurse withi+1.
- If
- Return result of initial call.
Complexity
- ⏰ Time complexity:
addWord: O(L)as insertion is simple down the path;search exact: O(L);search pattern: O(N * L)as we may have to go through whole of trie, but if the trie is complete, then it can beO(a^L), where is alphabet size, which is 26 for this problem.
- 🧺 Space complexity:
O(N * L)for trie nodes; recursion stackO(L); auxiliaryO(1)per frame (no substring allocations).
Code
Java
public class WordDictionary {
static class TrieNode {
Map<Character, TrieNode> children = new HashMap<Character, TrieNode> ();
boolean isEndOfWord;
}
private final TrieNode root;
public WordDictionary() {
root = new TrieNode();
}
// Adds a word into the data structure.
public void addWord(String word) {
TrieNode curr = root;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (!curr.children.containsKey(c)) {
curr.children.put(c, new TrieNode());
}
curr = curr.children.get(c);
}
curr.isEndOfWord = true;
}
// Returns if the word is in the data structure. A word could
// contain the dot character '.' to represent any one letter.
public boolean search(String word) {
return searchInNode(word, 0, root);
}
private boolean searchInNode(String word, int index, TrieNode node) {
if (index == word.length()) {
return node.isEndOfWord;
}
char c = word.charAt(index);
if (c == '.') {
for (TrieNode child : node.children.values()) {
if (searchInNode(word, index+1, child)) {
return true;
}
}
return false;
}
//if character at indexition 'index' is neither equal to the node nor '.', return false
if (!node.children.containsKey(word.charAt(index))) {
return false;
}
return searchInNode(word, index + 1, node.children.get(c));
}
}
Python
class TrieNode:
def __init__(self):
self.children: dict[str, TrieNode] = {}
self.is_end_of_word = False
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word: str) -> None:
curr = self.root
for c in word:
if c not in curr.children:
curr.children[c] = TrieNode()
curr = curr.children[c]
curr.is_end_of_word = True
def search(self, word: str) -> bool:
def search_in_node(index: int, node: TrieNode) -> bool:
if not node:
return False
if index == len(word):
return node.is_end_of_word
char = word[index]
if char == '.':
for child in node.children.values():
if search_in_node(index + 1, child):
return True
return False
else:
if char not in node.children:
return False
return search_in_node(index + 1, node.children[char])
return search_in_node(0, self.root)
Method 4 - Trie with Fixed Array Children (26-Way Branching)
Intuition
When the alphabet is known and small (lowercase English letters), a fixed array of size 26 per node replaces the hash map. This removes hash overhead and can speed up traversal at the cost of allocating 26 slots per node (even if sparse). Memory trade‑off vs Method 2/3: faster constant factors, higher baseline space for few children.
Approach
- Node:
TrieNodeholdsTrieNode[26] childrenandboolean isEndOfWord. addWord(word):- For each character
c, computeidx = c - 'a'. - Create child if null; advance.
- After last char set
isEndOfWord = true.
- For each character
search(pattern)callssearchInNode(node, pattern, i)similar to Method 3.- In recursion:
- If end reached, return
isEndOfWord. - If
pattern[i] == '.'iterate all 26 child slots (skip nulls) and recurse. - Else compute index and follow single child.
- If end reached, return
- Return outcome.
Complexity
- ⏰ Time complexity:
addWord: O(L)as insertion is simple down the path;search exact: O(L);search pattern: O(N * L)as we may have to go through whole of trie, but if the trie is complete, then it can beO(a^L), where is alphabet size, which is 26 for this problem.
- 🧺 Space complexity:
O(N * L)for trie nodes; recursion stackO(L); auxiliaryO(1)per frame (no substring allocations).
Code
Java
class WordDictionary {
private static class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEndOfWord = false;
}
private final TrieNode root;
public WordDictionary() {
root = new TrieNode();
}
public void addWord(String word) {
TrieNode curr = root;
for (char c : word.toCharArray()) {
if (curr.children[c - 'a'] == null) {
curr.children[c - 'a'] = new TrieNode();
}
curr = curr.children[c - 'a'];
}
curr.isEndOfWord = true;
}
public boolean search(String word) {
return searchInNode(word, 0, root);
}
private boolean searchInNode(String word, int index, TrieNode node) {
if (node == null) {
return false;
}
if (index == word.length()) {
return node.isEndOfWord;
}
char c = word.charAt(index);
if (c == '.') {
for (TrieNode child : node.children) {
if (searchInNode(word, index + 1, child)) {
return true;
}
}
return false;
} else {
TrieNode child = node.children[c - 'a'];
return searchInNode(word, index + 1, child);
}
}
}
Python
class TrieNode:
def __init__(self):
self.children: list[TrieNode | None] = [None] * 26
self.is_end_of_word = False
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word: str) -> None:
curr = self.root
for c in word:
idx = ord(c) - ord('a')
if curr.children[idx] is None:
curr.children[idx] = TrieNode()
curr = curr.children[idx]
curr.is_end_of_word = True
def search(self, pattern: str) -> bool:
def searchInNode(node: TrieNode | None, i: int) -> bool:
if node is None:
return False
if i == len(pattern):
return node.is_end_of_word
c = pattern[i]
if c == '.':
for child in node.children:
if child and searchInNode(child, i + 1):
return True
return False
idx = ord(c) - ord('a')
return searchInNode(node.children[idx], i + 1)
return searchInNode(self.root, 0)