🧠
Trie with DFS Backtracking for Wildcard Search
💡 This approach uses a trie to store words efficiently and applies DFS backtracking to handle '.' wildcards during search. It introduces the core data structure and recursion pattern needed for optimal performance.
Intuition
Build a trie where each node represents a character. For search, traverse the trie matching characters. When encountering '.', recursively search all child nodes.
Algorithm
- Create a trie node class with children and end-of-word flag.
- Add words by inserting characters into the trie nodes.
- Search by traversing trie nodes matching characters.
- If '.' is encountered, recursively search all children nodes.
💡 The key insight is combining trie traversal with recursive branching on '.' wildcards.
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word: str) -> None:
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_end = True
def search(self, word: str) -> bool:
def dfs(j, node):
if j == len(word):
return node.is_end
ch = word[j]
if ch == '.':
for child in node.children.values():
if dfs(j + 1, child):
return True
return False
else:
if ch not in node.children:
return False
return dfs(j + 1, node.children[ch])
return dfs(0, self.root)
# Driver code
wd = WordDictionary()
wd.addWord("bad")
wd.addWord("dad")
wd.addWord("mad")
print(wd.search("pad")) # False
print(wd.search("bad")) # True
print(wd.search(".ad")) # True
print(wd.search("b..")) # True
Line Notes
self.children = {}Use dictionary to store child nodes for each character
if ch not in node.childrenCreate new trie node if character path doesn't exist
node.is_end = TrueMark end of a valid word
if ch == '.'Wildcard triggers DFS over all children
return dfs(j + 1, node.children[ch])Continue traversal for matching character
import java.util.*;
class WordDictionary {
private class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
boolean isEnd = false;
}
private TrieNode root;
public WordDictionary() {
root = new TrieNode();
}
public void addWord(String word) {
TrieNode node = root;
for (char ch : word.toCharArray()) {
node.children.putIfAbsent(ch, new TrieNode());
node = node.children.get(ch);
}
node.isEnd = true;
}
public boolean search(String word) {
return dfs(word, 0, root);
}
private boolean dfs(String word, int index, TrieNode node) {
if (index == word.length()) return node.isEnd;
char ch = word.charAt(index);
if (ch == '.') {
for (TrieNode child : node.children.values()) {
if (dfs(word, index + 1, child)) return true;
}
return false;
} else {
if (!node.children.containsKey(ch)) return false;
return dfs(word, index + 1, node.children.get(ch));
}
}
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
}
}
Line Notes
Map<Character, TrieNode> children = new HashMap<>()Store children nodes in a map for quick lookup
node.children.putIfAbsent(ch, new TrieNode())Create child node if missing
node.isEnd = trueMark node as end of a word
if (ch == '.')Wildcard triggers DFS over all children
return dfs(word, index + 1, node.children.get(ch))Traverse matching child node
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;
class TrieNode {
public:
unordered_map<char, TrieNode*> children;
bool isEnd = false;
};
class WordDictionary {
TrieNode* root;
public:
WordDictionary() {
root = new TrieNode();
}
void addWord(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->isEnd = true;
}
bool dfs(string& word, int index, TrieNode* node) {
if (index == (int)word.size()) return node->isEnd;
char ch = word[index];
if (ch == '.') {
for (auto& p : node->children) {
if (dfs(word, index + 1, p.second)) return true;
}
return false;
} else {
if (node->children.find(ch) == node->children.end()) return false;
return dfs(word, index + 1, node->children[ch]);
}
}
bool search(string word) {
return dfs(word, 0, root);
}
};
int main() {
WordDictionary wd;
wd.addWord("bad");
wd.addWord("dad");
wd.addWord("mad");
cout << boolalpha << wd.search("pad") << "\n"; // false
cout << boolalpha << wd.search("bad") << "\n"; // true
cout << boolalpha << wd.search(".ad") << "\n"; // true
cout << boolalpha << wd.search("b..") << "\n"; // true
return 0;
}
Line Notes
unordered_map<char, TrieNode*> children;Map children characters to trie nodes for fast access
if (node->children.find(ch) == node->children.end())Create new node if path missing
node->isEnd = true;Mark end of word
if (ch == '.')Wildcard triggers DFS over all children
return dfs(word, index + 1, node->children[ch]);Traverse matching child node
class TrieNode {
constructor() {
this.children = new Map();
this.isEnd = false;
}
}
class WordDictionary {
constructor() {
this.root = new TrieNode();
}
addWord(word) {
let node = this.root;
for (const ch of word) {
if (!node.children.has(ch)) {
node.children.set(ch, new TrieNode());
}
node = node.children.get(ch);
}
node.isEnd = true;
}
search(word) {
const dfs = (index, node) => {
if (index === word.length) return node.isEnd;
const ch = word[index];
if (ch === '.') {
for (const child of node.children.values()) {
if (dfs(index + 1, child)) return true;
}
return false;
} else {
if (!node.children.has(ch)) return false;
return dfs(index + 1, node.children.get(ch));
}
};
return dfs(0, this.root);
}
}
// Test
const wd = new WordDictionary();
wd.addWord("bad");
wd.addWord("dad");
wd.addWord("mad");
console.log(wd.search("pad")); // false
console.log(wd.search("bad")); // true
console.log(wd.search(".ad")); // true
console.log(wd.search("b..")); // true
Line Notes
this.children = new Map()Use Map to store children nodes for efficient lookup
if (!node.children.has(ch))Create new child node if missing
node.isEnd = trueMark node as end of a word
if (ch === '.')Wildcard triggers DFS over all children
return dfs(index + 1, node.children.get(ch))Traverse child node matching character
TimeO(L) for addWord, O(26^m) worst-case for search with m wildcards
SpaceO(N * L) for trie storage
Adding words is linear in word length. Searching is linear unless wildcards cause branching, which can be exponential in worst case but usually efficient.
💡 For typical inputs, search is fast, but many '.' characters increase branching exponentially.
Interview Verdict: Accepted and efficient for most cases
This is the standard and optimal approach for this problem in interviews.