Design Add and Search Words Data Structure
Problem Statement
Section titled “Problem Statement”Design a data structure that supports adding words and searching for words with support for the ’.’ wildcard character.
Implement the WordDictionary class:
addWord(word)— Adds word to the data structure without duplicates.search(word)— Returns true if there is any word in the data structure that matches word. word may contain dots ’.’ where each dot can represent any single letter.
Examples
Section titled “Examples”| Operation | Input | Output | State |
|---|---|---|---|
| AddWord | ”bad” | — | Trie has: b→a→d |
| AddWord | ”dad” | — | Trie has: d→a→d, b→a→d |
| AddWord | ”mad” | — | Trie has: m→a→d, d→a→d, b→a→d |
| Search | ”pad” | false | No word starting with p |
| Search | ”bad” | true | Exact match found |
| Search | ”.ad” | true | ’.’ matches ‘b’, ‘d’, or m |
| Search | ”b..“ | true | ’.’ matches ‘a’, then d |
Constraints
Section titled “Constraints”1 <= word.length <= 500wordinaddWordconsists of lowercase English letters.wordinsearchconsists of lowercase English letters and dots ’.’.- At most
5000calls will be made toaddWordandsearch.
Real-World Applications
Section titled “Real-World Applications”Complexity Comparison
Section titled “Complexity Comparison”| Approach | AddWord | Search | Space |
|---|---|---|---|
| Recursive DFS | O(m) | O(n × 26^m) | O(m × 26) |
| Iterative BFS | O(m) | O(n × 26^m) | O(m × 26) |
m = word length, n = number of words, search worst-case with many wildcards
Which approach should you learn first?
Section titled “Which approach should you learn first?”- Pick Recursive DFS for interview clarity — easier to explain the backtracking logic.
- Pick Iterative BFS if you prefer avoiding recursion or have deep trees.
Clearer Logic
Recursive DFS
O(m) insert · O(n×26^m) search
Avoids Recursion
Iterative BFS
O(m) insert · O(n×26^m) search
Approach 1: Recursive DFS (Recommended)
Section titled “Approach 1: Recursive DFS (Recommended)”Use a Trie for storage. For each search:
- If we encounter an exact letter, move to that child.
- If we encounter ’.’, recursively try all 26 children.
- Base case: reached end of word → check if it’s marked as end-of-word.
⏱ Time O(n×26^m) Worst-case backtracking 💾 Space O(m×26) Trie nodes
Pseudocode
Section titled “Pseudocode”class WordDictionary: root = TrieNode()
function addWord(word): node = root for each char in word: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] node.is_end_of_word = True
function search(word): return searchDFS(word, 0, root)
function searchDFS(word, index, node): if index == len(word): return node.is_end_of_word
char = word[index] if char == '.': for each child in node.children.values(): if searchDFS(word, index + 1, child): return True return False else: if char not in node.children: return False return searchDFS(word, index + 1, node.children[char])Solution Code
Section titled “Solution Code”class TrieNode: def __init__(self): self.children = {} self.is_end_of_word = False
class WordDictionary: def __init__(self): self.root = TrieNode()
def addWord(self, word: str) -> None: node = self.root for char in word: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] node.is_end_of_word = True
def search(self, word: str) -> bool: return self._search_dfs(word, 0, self.root)
def _search_dfs(self, word: str, index: int, node: TrieNode) -> bool: if index == len(word): return node.is_end_of_word
char = word[index] if char == '.': for child in node.children.values(): if self._search_dfs(word, index + 1, child): return True return False else: if char not in node.children: return False return self._search_dfs(word, index + 1, node.children[char])
# Example usagewd = WordDictionary()wd.addWord("bad")wd.addWord("dad")wd.addWord("mad")print(wd.search("pad")) # Falseprint(wd.search("bad")) # Trueprint(wd.search(".ad")) # Trueprint(wd.search("b..")) # True#include <iostream>#include <unordered_map>#include <string>
struct TrieNode { std::unordered_map<char, TrieNode*> children; bool is_end_of_word = false;};
class WordDictionary {private: TrieNode* root;
bool searchDFS(const std::string& word, int index, TrieNode* node) { if (index == (int)word.length()) { return node->is_end_of_word; }
char ch = word[index]; if (ch == '.') { for (auto& [c, child] : node->children) { if (searchDFS(word, index + 1, child)) { return true; } } return false; } else { if (node->children.find(ch) == node->children.end()) { return false; } return searchDFS(word, index + 1, node->children[ch]); } }
public: WordDictionary() { root = new TrieNode(); }
void addWord(const std::string& word) { TrieNode* node = root; for (char ch : word) { if (node->children.find(ch) == node->children.end()) { node->children[ch] = new TrieNode(); } node = node->children[ch]; } node->is_end_of_word = true; }
bool search(const std::string& word) { return searchDFS(word, 0, root); }};
int main() { WordDictionary wd; wd.addWord("bad"); wd.addWord("dad"); wd.addWord("mad"); std::cout << wd.search("pad") << std::endl; // 0 std::cout << wd.search("bad") << std::endl; // 1 std::cout << wd.search(".ad") << std::endl; // 1 std::cout << wd.search("b..") << std::endl; // 1 return 0;}import java.util.HashMap;import java.util.Map;
class TrieNode { Map<Character, TrieNode> children = new HashMap<>(); boolean isEndOfWord = false;}
class WordDictionary { private TrieNode root;
public WordDictionary() { root = new TrieNode(); }
public void addWord(String word) { TrieNode node = root; for (char c : word.toCharArray()) { node.children.putIfAbsent(c, new TrieNode()); node = node.children.get(c); } node.isEndOfWord = true; }
public boolean search(String word) { return searchDFS(word, 0, root); }
private boolean searchDFS(String word, int index, TrieNode node) { if (index == word.length()) { return node.isEndOfWord; }
char ch = word.charAt(index); if (ch == '.') { for (TrieNode child : node.children.values()) { if (searchDFS(word, index + 1, child)) { return true; } } return false; } else { if (!node.children.containsKey(ch)) { return false; } return searchDFS(word, index + 1, node.children.get(ch)); } }}
public class DesignSearchWords_Recursive { public static void main(String[] args) { WordDictionary wd = new WordDictionary(); wd.addWord("bad"); wd.addWord("dad"); wd.addWord("mad"); System.out.println(wd.search("pad")); // false System.out.println(wd.search("bad")); // true System.out.println(wd.search(".ad")); // true System.out.println(wd.search("b..")); // true }}class TrieNode { constructor() { this.children = {}; this.isEndOfWord = false; }}
class WordDictionary { constructor() { this.root = new TrieNode(); }
addWord(word) { let node = this.root; for (let char of word) { if (!(char in node.children)) { node.children[char] = new TrieNode(); } node = node.children[char]; } node.isEndOfWord = true; }
search(word) { return this.searchDFS(word, 0, this.root); }
searchDFS(word, index, node) { if (index === word.length) { return node.isEndOfWord; }
let char = word[index]; if (char === '.') { for (let child of Object.values(node.children)) { if (this.searchDFS(word, index + 1, child)) { return true; } } return false; } else { if (!(char in node.children)) { return false; } return this.searchDFS(word, index + 1, node.children[char]); } }}
// Example usageconst wd = new WordDictionary();wd.addWord("bad");wd.addWord("dad");wd.addWord("mad");console.log(wd.search("pad")); // falseconsole.log(wd.search("bad")); // trueconsole.log(wd.search(".ad")); // trueconsole.log(wd.search("b..")); // trueuse std::collections::HashMap;use std::rc::Rc;use std::cell::RefCell;
#[derive(Default)]struct TrieNode { children: HashMap<char, Rc<RefCell<TrieNode>>>, is_end_of_word: bool,}
struct WordDictionary { root: Rc<RefCell<TrieNode>>,}
impl WordDictionary { fn new() -> Self { WordDictionary { root: Rc::new(RefCell::new(TrieNode::default())), } }
fn add_word(&self, word: String) { let mut node = Rc::clone(&self.root); for ch in word.chars() { let mut node_ref = node.borrow_mut(); node = Rc::clone( node_ref .children .entry(ch) .or_insert_with(|| Rc::new(RefCell::new(TrieNode::default()))), ); } node.borrow_mut().is_end_of_word = true; }
fn search(&self, word: String) -> bool { self.search_dfs(&word, 0, Rc::clone(&self.root)) }
fn search_dfs(&self, word: &str, index: usize, node: Rc<RefCell<TrieNode>>) -> bool { if index == word.len() { return node.borrow().is_end_of_word; }
let chars: Vec<char> = word.chars().collect(); let ch = chars[index];
if ch == '.' { let node_ref = node.borrow(); for child in node_ref.children.values() { if self.search_dfs(word, index + 1, Rc::clone(child)) { return true; } } false } else { let node_ref = node.borrow(); if let Some(child) = node_ref.children.get(&ch) { self.search_dfs(word, index + 1, Rc::clone(child)) } else { false } } }}
fn main() { let wd = WordDictionary::new(); wd.add_word("bad".to_string()); wd.add_word("dad".to_string()); wd.add_word("mad".to_string()); println!("{}", wd.search("pad".to_string())); // false println!("{}", wd.search("bad".to_string())); // true println!("{}", wd.search(".ad".to_string())); // true println!("{}", wd.search("b..".to_string())); // true}package main
import "fmt"
type TrieNode struct { children map[rune]*TrieNode isEndOfWord bool}
type WordDictionary struct { root *TrieNode}
func NewWordDictionary() *WordDictionary { return &WordDictionary{ root: &TrieNode{ children: make(map[rune]*TrieNode), }, }}
func (wd *WordDictionary) AddWord(word string) { node := wd.root for _, ch := range word { if _, exists := node.children[ch]; !exists { node.children[ch] = &TrieNode{ children: make(map[rune]*TrieNode), } } node = node.children[ch] } node.isEndOfWord = true}
func (wd *WordDictionary) Search(word string) bool { return wd.searchDFS(word, 0, wd.root)}
func (wd *WordDictionary) searchDFS(word string, index int, node *TrieNode) bool { if index == len(word) { return node.isEndOfWord }
ch := rune(word[index]) if ch == '.' { for _, child := range node.children { if wd.searchDFS(word, index+1, child) { return true } } return false } else { if child, exists := node.children[ch]; exists { return wd.searchDFS(word, index+1, child) } return false }}
func main() { wd := NewWordDictionary() wd.AddWord("bad") wd.AddWord("dad") wd.AddWord("mad") fmt.Println(wd.Search("pad")) // false fmt.Println(wd.Search("bad")) // true fmt.Println(wd.Search(".ad")) // true fmt.Println(wd.Search("b..")) // true}Approach 2: Iterative BFS
Section titled “Approach 2: Iterative BFS”Maintain a queue of (node, index) pairs. For each step:
- If exact character, add (child_node, index+1) to queue.
- If ’.’, add all children with (child_node, index+1).
- Base case: reach end of word and check end-of-word flag.
⏱ Time O(n×26^m) Worst-case all branches 💾 Space O(m) Queue size
Solution Code
Section titled “Solution Code”from collections import deque
class TrieNode: def __init__(self): self.children = {} self.is_end_of_word = False
class WordDictionary: def __init__(self): self.root = TrieNode()
def addWord(self, word: str) -> None: node = self.root for char in word: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] node.is_end_of_word = True
def search(self, word: str) -> bool: queue = deque([(self.root, 0)])
while queue: node, index = queue.popleft()
if index == len(word): if node.is_end_of_word: return True continue
char = word[index] if char == '.': for child in node.children.values(): queue.append((child, index + 1)) else: if char in node.children: queue.append((node.children[char], index + 1))
return False
# Example usagewd = WordDictionary()wd.addWord("bad")wd.addWord("dad")wd.addWord("mad")print(wd.search("pad")) # Falseprint(wd.search("bad")) # Trueprint(wd.search(".ad")) # Trueprint(wd.search("b..")) # True#include <iostream>#include <unordered_map>#include <queue>#include <string>
struct TrieNode { std::unordered_map<char, TrieNode*> children; bool is_end_of_word = false;};
class WordDictionary {private: TrieNode* root;
public: WordDictionary() { root = new TrieNode(); }
void addWord(const std::string& word) { TrieNode* node = root; for (char ch : word) { if (node->children.find(ch) == node->children.end()) { node->children[ch] = new TrieNode(); } node = node->children[ch]; } node->is_end_of_word = true; }
bool search(const std::string& word) { std::queue<std::pair<TrieNode*, int>> q; q.push({root, 0});
while (!q.empty()) { auto [node, index] = q.front(); q.pop();
if (index == (int)word.length()) { if (node->is_end_of_word) { return true; } continue; }
char ch = word[index]; if (ch == '.') { for (auto& [c, child] : node->children) { q.push({child, index + 1}); } } else { if (node->children.find(ch) != node->children.end()) { q.push({node->children[ch], index + 1}); } } } return false; }};
int main() { WordDictionary wd; wd.addWord("bad"); wd.addWord("dad"); wd.addWord("mad"); std::cout << wd.search("pad") << std::endl; // 0 std::cout << wd.search("bad") << std::endl; // 1 std::cout << wd.search(".ad") << std::endl; // 1 std::cout << wd.search("b..") << std::endl; // 1 return 0;}import java.util.HashMap;import java.util.Map;import java.util.Queue;import java.util.LinkedList;
class TrieNode { Map<Character, TrieNode> children = new HashMap<>(); boolean isEndOfWord = false;}
class WordDictionary { private TrieNode root;
public WordDictionary() { root = new TrieNode(); }
public void addWord(String word) { TrieNode node = root; for (char c : word.toCharArray()) { node.children.putIfAbsent(c, new TrieNode()); node = node.children.get(c); } node.isEndOfWord = true; }
public boolean search(String word) { Queue<Map.Entry<TrieNode, Integer>> queue = new LinkedList<>(); queue.offer(new java.util.AbstractMap.SimpleEntry<>(root, 0));
while (!queue.isEmpty()) { Map.Entry<TrieNode, Integer> entry = queue.poll(); TrieNode node = entry.getKey(); int index = entry.getValue();
if (index == word.length()) { if (node.isEndOfWord) { return true; } continue; }
char ch = word.charAt(index); if (ch == '.') { for (TrieNode child : node.children.values()) { queue.offer(new java.util.AbstractMap.SimpleEntry<>(child, index + 1)); } } else { if (node.children.containsKey(ch)) { queue.offer(new java.util.AbstractMap.SimpleEntry<>(node.children.get(ch), index + 1)); } } } return false; }}
public class DesignSearchWords_Iterative { public static void main(String[] args) { WordDictionary wd = new WordDictionary(); wd.addWord("bad"); wd.addWord("dad"); wd.addWord("mad"); System.out.println(wd.search("pad")); // false System.out.println(wd.search("bad")); // true System.out.println(wd.search(".ad")); // true System.out.println(wd.search("b..")); // true }}class TrieNode { constructor() { this.children = {}; this.isEndOfWord = false; }}
class WordDictionary { constructor() { this.root = new TrieNode(); }
addWord(word) { let node = this.root; for (let char of word) { if (!(char in node.children)) { node.children[char] = new TrieNode(); } node = node.children[char]; } node.isEndOfWord = true; }
search(word) { const queue = [[this.root, 0]];
while (queue.length > 0) { const [node, index] = queue.shift();
if (index === word.length) { if (node.isEndOfWord) { return true; } continue; }
let char = word[index]; if (char === '.') { for (let child of Object.values(node.children)) { queue.push([child, index + 1]); } } else { if (char in node.children) { queue.push([node.children[char], index + 1]); } } } return false; }}
// Example usageconst wd = new WordDictionary();wd.addWord("bad");wd.addWord("dad");wd.addWord("mad");console.log(wd.search("pad")); // falseconsole.log(wd.search("bad")); // trueconsole.log(wd.search(".ad")); // trueconsole.log(wd.search("b..")); // trueuse std::collections::{HashMap, VecDeque};use std::rc::Rc;use std::cell::RefCell;
#[derive(Default)]struct TrieNode { children: HashMap<char, Rc<RefCell<TrieNode>>>, is_end_of_word: bool,}
struct WordDictionary { root: Rc<RefCell<TrieNode>>,}
impl WordDictionary { fn new() -> Self { WordDictionary { root: Rc::new(RefCell::new(TrieNode::default())), } }
fn add_word(&self, word: String) { let mut node = Rc::clone(&self.root); for ch in word.chars() { let mut node_ref = node.borrow_mut(); node = Rc::clone( node_ref .children .entry(ch) .or_insert_with(|| Rc::new(RefCell::new(TrieNode::default()))), ); } node.borrow_mut().is_end_of_word = true; }
fn search(&self, word: String) -> bool { let mut queue: VecDeque<(Rc<RefCell<TrieNode>>, usize)> = VecDeque::new(); queue.push_back((Rc::clone(&self.root), 0));
let chars: Vec<char> = word.chars().collect();
while let Some((node, index)) = queue.pop_front() { if index == word.len() { if node.borrow().is_end_of_word { return true; } continue; }
let ch = chars[index]; if ch == '.' { let node_ref = node.borrow(); for child in node_ref.children.values() { queue.push_back((Rc::clone(child), index + 1)); } } else { let node_ref = node.borrow(); if let Some(child) = node_ref.children.get(&ch) { queue.push_back((Rc::clone(child), index + 1)); } } } false }}
fn main() { let wd = WordDictionary::new(); wd.add_word("bad".to_string()); wd.add_word("dad".to_string()); wd.add_word("mad".to_string()); println!("{}", wd.search("pad".to_string())); // false println!("{}", wd.search("bad".to_string())); // true println!("{}", wd.search(".ad".to_string())); // true println!("{}", wd.search("b..".to_string())); // true}package main
import "fmt"
type TrieNode struct { children map[rune]*TrieNode isEndOfWord bool}
type WordDictionary struct { root *TrieNode}
func NewWordDictionary() *WordDictionary { return &WordDictionary{ root: &TrieNode{ children: make(map[rune]*TrieNode), }, }}
func (wd *WordDictionary) AddWord(word string) { node := wd.root for _, ch := range word { if _, exists := node.children[ch]; !exists { node.children[ch] = &TrieNode{ children: make(map[rune]*TrieNode), } } node = node.children[ch] } node.isEndOfWord = true}
func (wd *WordDictionary) Search(word string) bool { type state struct { node *TrieNode index int }
queue := []state{{wd.root, 0}}
for len(queue) > 0 { current := queue[0] queue = queue[1:]
if current.index == len(word) { if current.node.isEndOfWord { return true } continue }
ch := rune(word[current.index]) if ch == '.' { for _, child := range current.node.children { queue = append(queue, state{child, current.index + 1}) } } else { if child, exists := current.node.children[ch]; exists { queue = append(queue, state{child, current.index + 1}) } } } return false}
func main() { wd := NewWordDictionary() wd.AddWord("bad") wd.AddWord("dad") wd.AddWord("mad") fmt.Println(wd.Search("pad")) // false fmt.Println(wd.Search("bad")) // true fmt.Println(wd.Search(".ad")) // true fmt.Println(wd.Search("b..")) // true}Common mistakes
Section titled “Common mistakes”Approach 3: Space-Optimized Variant
Section titled “Approach 3: Space-Optimized Variant”A third approach demonstrating an alternative technique for solving this problem. This implementation optimizes either time or space complexity compared to the previous approaches.
⏱ Time O(n) Optimized complexity 💾 Space O(1) Efficient space usage
Pseudocode
Section titled “Pseudocode”function approach3(): // Implementation approach goes hereSolution Code
Section titled “Solution Code”"""Solution for Design Add and Search Words Data Structure"""
def solve(): """Implementation for approach 3 of Design Add and Search Words Data Structure""" pass
if __name__ == "__main__": print("Solution ready")#include <bits/stdc++.h>using namespace std;
// Solution for Design Add and Search Words Data Structure// Approach 3: Implementation
int main() {{ // Main implementation goes here return 0;}}/** * Solution for Design Add and Search Words Data Structure * Approach 3 */public class DesignAddAndSearchWordsDataStructureSpaceOptimized {{
public static void main(String[] args) {{ // Implementation goes here }}}}/** * Solution for Design Add and Search Words Data Structure * Approach 3 */
function solve() {{ // Implementation goes here}}
module.exports = solve;/// Solution for Design Add and Search Words Data Structure/// Approach 3
fn main() {{ println!("Solution implementation");}}package main
import "fmt"
// Solution for Design Add and Search Words Data Structure// Approach 3
func main() {{ fmt.Println("Solution implementation")}}