Wildcard searches
Searching a trie with patterns: '.' wildcards, prefix-and-suffix filters, depth-first enumeration, and Word Search II, where the trie prunes a board search.
Learning goals
- Collect all words that match a pattern with dots
- Answer prefix-and-suffix queries with a single trie lookup
- Enumerate a trie depth-first with an explicit stack
- Use a trie to prune the DFS in Word Search II
Prerequisites
Crossword helpers match 'c.t' against a dictionary, and a word-game solver tries every path on a board. Both look at far too many candidates unless the trie says 'no word continues this way' as early as possible.
Dot wildcard
Where the dictionary lesson asked 'is there any match?', this version collects every match. The recursion carries the path built so far: a letter extends it by one edge, a dot extends it along each child in turn, and reaching the end of the pattern at a word end adds the path to the result. Words come out in alphabetical order because the children are visited in letter order.
The cost depends on the pattern. A pattern with no dots follows one path. A pattern of m dots can visit every node at depth m, which is why the search is O(nodes) in the worst case. Replacing the leading letters of a pattern by dots makes the search wider; keep real letters early in the pattern when you can.
1class PatternTrie {2private static class Node {3Node[] next = new Node[26];4boolean end;5}6private final Node root = new Node();7void add(String word) {8Node n = root;9for (char c : word.toCharArray()) {10int i = c - 'a';11if (n.next[i] == null) n.next[i] = new Node();12n = n.next[i];13}14n.end = true;15}16List<String> matches(String pattern) {17List<String> out = new ArrayList<>();18walk(root, pattern, 0, new StringBuilder(), out);19return out;20}21private void walk(Node n, String p, int i, StringBuilder sb, List<String> out) {22if (n == null) return;23if (i == p.length()) { if (n.end) out.add(sb.toString()); return; }24char c = p.charAt(i);25for (int k = 0; k < 26; k++) {26if (n.next[k] == null || (c != '.' && c - 'a' != k)) continue; // a letter keeps one branch27sb.append((char) ('a' + k));28walk(n.next[k], p, i + 1, sb, out);29sb.deleteCharAt(sb.length() - 1);30}31}32}
Wildcards: '.' search by DFS, and the prefix + suffix filter
Interactive · edit the inputtrie · 13 nodes
word
word
word
word
Search "b.d". A letter follows exactly one edge; a dot tries every child in turn, so the search becomes a DFS on the trie.
1boolean search(Node n, String p, int i) { // does any word below n match p[i..]?2if (n == null) return false;3if (i == p.length()) return n.end;4char c = p.charAt(i);5if (c != '.') return search(n.next[c - 'a'], p, i + 1); // a letter: one way only6for (Node child : n.next) // a dot: try every branch7if (child != null && search(child, p, i + 1)) return true;8return false;9}10void addWord(String w) { for (int i = 0; i <= w.length(); i++) insert(w.substring(i) + "{" + w); }11boolean hasPrefixSuffix(String pre, String suf) { return startsWith(suf + "{" + pre); }
Live state
pattern
"b.d"
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | pattern="b.d" | Search "b.d". A letter follows exactly one edge; a dot tries every child in turn, so the search becomes a DFS on the trie. |
| 2 | L5 | i=1 char=b | The letter 'b' leaves one way: follow it. |
| 3 | L6 | i=1 char=. | A dot at position 1: try each of the 1 child in alphabetical order. |
| 4 | L7 | i=2 branch=a | Try the branch 'a'. |
| 5 | L5 | i=3 char=d | The letter 'd' leaves one way: follow it. |
| 6 | L3 | i=3 end=true | The pattern is used up at a node where a word ends: match. |
| 7 | L7 | return=true nodes visited=4 | search("b.d") = true after 4 node visits. It stops at the first match. |
Prefix/suffix filters
A filter such as a*e asks for words that start with a and end with e. A trie answers prefix questions only, so store each word under every suffix: for the word apple, insert apple{apple, pple{apple, ple{apple, le{apple, e{apple and {apple. The separator { sorts after z and never occurs in a word. The query pre*suf then becomes the prefix query suf{pre.
Choose the preset "Prefix + suffix" (a*e over apple apply ample): the animation lists every key and highlights those that start with e{a, which belong to apple and ample. The price is space: a word of length L adds L + 1 keys, so the trie can grow by about 1.5 × L² nodes per word. Store the index of the word at the end of each key to return the largest index, as the practice problem asks.
1class WordFilter {2private static class Node {3Node[] next = new Node[27]; // 'a'..'z' and '{'4int index = -1; // the largest word index that passes through5}6private final Node root = new Node();7WordFilter(String[] words) {8for (int w = 0; w < words.length; w++) {9String word = words[w];10for (int i = 0; i <= word.length(); i++) insert(word.substring(i) + "{" + word, w);11}12}13private void insert(String key, int w) {14Node n = root;15for (char c : key.toCharArray()) {16int i = c - 'a'; // '{' is 'a' + 2617if (n.next[i] == null) n.next[i] = new Node();18n = n.next[i];19n.index = w; // words are inserted in index order20}21}22int f(String prefix, String suffix) { // largest index of a word with this prefix and suffix, or -123Node n = root;24for (char c : (suffix + "{" + prefix).toCharArray()) {25n = n.next[c - 'a'];26if (n == null) return -1;27}28return n.index;29}30}
Wildcards: '.' search by DFS, and the prefix + suffix filter
Interactive · edit the inputtrie · 13 nodes
word
word
word
word
Search "b.d". A letter follows exactly one edge; a dot tries every child in turn, so the search becomes a DFS on the trie.
1boolean search(Node n, String p, int i) { // does any word below n match p[i..]?2if (n == null) return false;3if (i == p.length()) return n.end;4char c = p.charAt(i);5if (c != '.') return search(n.next[c - 'a'], p, i + 1); // a letter: one way only6for (Node child : n.next) // a dot: try every branch7if (child != null && search(child, p, i + 1)) return true;8return false;9}10void addWord(String w) { for (int i = 0; i <= w.length(); i++) insert(w.substring(i) + "{" + w); }11boolean hasPrefixSuffix(String pre, String suf) { return startsWith(suf + "{" + pre); }
Live state
pattern
"b.d"
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | pattern="b.d" | Search "b.d". A letter follows exactly one edge; a dot tries every child in turn, so the search becomes a DFS on the trie. |
| 2 | L5 | i=1 char=b | The letter 'b' leaves one way: follow it. |
| 3 | L6 | i=1 char=. | A dot at position 1: try each of the 1 child in alphabetical order. |
| 4 | L7 | i=2 branch=a | Try the branch 'a'. |
| 5 | L5 | i=3 char=d | The letter 'd' leaves one way: follow it. |
| 6 | L3 | i=3 end=true | The pattern is used up at a node where a word ends: match. |
| 7 | L7 | return=true nodes visited=4 | search("b.d") = true after 4 node visits. It stops at the first match. |
DFS on trie
Every trie operation beyond a single path is a depth-first search: autocomplete, wildcards, counting words, finding the longest word. The recursive form is short, but an explicit stack avoids deep recursion and makes it easy to stop early. Push the root with an empty path; pop a node, report it if it ends a word, then push its children in reverse alphabetical order so that the smallest letter is popped first.
Pushing in reverse is the one trick: a stack pops the last item pushed, so to visit a before b you push b first. The stack holds at most one entry per child of the nodes on the current path, so its size is bounded by the depth times the alphabet.
1class WordLister {2private static class Node {3Node[] next = new Node[26];4boolean end;5}6private final Node root = new Node();7void add(String word) {8Node n = root;9for (char c : word.toCharArray()) {10int i = c - 'a';11if (n.next[i] == null) n.next[i] = new Node();12n = n.next[i];13}14n.end = true;15}16List<String> allWords() {17List<String> out = new ArrayList<>();18Deque<Node> nodes = new ArrayDeque<>();19Deque<String> paths = new ArrayDeque<>();20nodes.push(root);21paths.push("");22while (!nodes.isEmpty()) {23Node n = nodes.pop();24String path = paths.pop();25if (n.end) out.add(path);26for (int c = 25; c >= 0; c--) { // reverse order: the smallest letter is popped first27if (n.next[c] == null) continue;28nodes.push(n.next[c]);29paths.push(path + (char) ('a' + c));30}31}32return out;33}34}
Wildcards: '.' search by DFS, and the prefix + suffix filter
Interactive · edit the inputtrie · 13 nodes
word
word
word
word
Search "b.d". A letter follows exactly one edge; a dot tries every child in turn, so the search becomes a DFS on the trie.
1boolean search(Node n, String p, int i) { // does any word below n match p[i..]?2if (n == null) return false;3if (i == p.length()) return n.end;4char c = p.charAt(i);5if (c != '.') return search(n.next[c - 'a'], p, i + 1); // a letter: one way only6for (Node child : n.next) // a dot: try every branch7if (child != null && search(child, p, i + 1)) return true;8return false;9}10void addWord(String w) { for (int i = 0; i <= w.length(); i++) insert(w.substring(i) + "{" + w); }11boolean hasPrefixSuffix(String pre, String suf) { return startsWith(suf + "{" + pre); }
Live state
pattern
"b.d"
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | pattern="b.d" | Search "b.d". A letter follows exactly one edge; a dot tries every child in turn, so the search becomes a DFS on the trie. |
| 2 | L5 | i=1 char=b | The letter 'b' leaves one way: follow it. |
| 3 | L6 | i=1 char=. | A dot at position 1: try each of the 1 child in alphabetical order. |
| 4 | L7 | i=2 branch=a | Try the branch 'a'. |
| 5 | L5 | i=3 char=d | The letter 'd' leaves one way: follow it. |
| 6 | L3 | i=3 end=true | The pattern is used up at a node where a word ends: match. |
| 7 | L7 | return=true nodes visited=4 | search("b.d") = true after 4 node visits. It stops at the first match. |
Word search II
Word Search II gives a letter board and a list of words and asks which words can be traced through adjacent cells without reusing a cell. Searching the board once per word repeats the same work for words that share a prefix. Instead put all the words in one trie and run a single DFS from every cell, moving in the trie at the same time: a branch is abandoned the moment the letters so far are not a prefix of any word.
The animation shows the board on top and the trie below. In the preset "Three words, one on the board" (oaa eta ihk with oath eat tea) the search enters 7 cells, cuts 20 branches whose letters continue no word, and finds only oath. Storing the finished word at its last node (instead of the end flag) avoids rebuilding the string, and the visited mark # is restored on the way back so other paths can use the cell.
1class WordSearch2 {2private static class Node {3Node[] next = new Node[26];4String word; // the whole word, set at its last node5}6List<String> findWords(char[][] board, String[] words) {7Node root = new Node();8for (String w : words) {9Node n = root;10for (char c : w.toCharArray()) {11int i = c - 'a';12if (n.next[i] == null) n.next[i] = new Node();13n = n.next[i];14}15n.word = w;16}17Set<String> found = new TreeSet<>();18for (int r = 0; r < board.length; r++)19for (int c = 0; c < board[0].length; c++)20dfs(board, r, c, root, found);21return new ArrayList<>(found);22}23private void dfs(char[][] b, int r, int c, Node n, Set<String> found) {24if (r < 0 || c < 0 || r >= b.length || c >= b[0].length) return;25char ch = b[r][c];26if (ch == '#' || n.next[ch - 'a'] == null) return; // used cell, or no word continues this way27n = n.next[ch - 'a'];28if (n.word != null) found.add(n.word); // a word ends here29b[r][c] = '#'; // mark visited30dfs(b, r + 1, c, n, found); dfs(b, r - 1, c, n, found);31dfs(b, r, c + 1, n, found); dfs(b, r, c - 1, n, found);32b[r][c] = ch; // restore33}34}
Word Search II: DFS on a board guided by a trie
Interactive · edit the inputgrid · 3 × 3
trie of the words · 11 nodes
eat
oath
tea
words found
All 3 words go into one trie. The board is searched once from every cell, and a branch is abandoned the moment the letters so far are not a prefix of any word.
1List<String> findWords(char[][] board, String[] words) {2Trie trie = new Trie(words); // all words in one trie, word stored at its last node3Set<String> found = new TreeSet<>();4for (int r = 0; r < board.length; r++)5for (int c = 0; c < board[0].length; c++)6dfs(board, r, c, trie.root, found);7return new ArrayList<>(found);8}9void dfs(char[][] b, int r, int c, Node n, Set<String> found) {10if (r < 0 || c < 0 || r >= b.length || c >= b[0].length) return;11char ch = b[r][c];12if (ch == '#' || n.next[ch - 'a'] == null) return; // used cell, or no word continues this way13n = n.next[ch - 'a'];14if (n.word != null) found.add(n.word); // a word ends here15b[r][c] = '#'; // mark visited16dfs(b, r + 1, c, n, found); dfs(b, r - 1, c, n, found);17dfs(b, r, c + 1, n, found); dfs(b, r, c - 1, n, found);18b[r][c] = ch; // restore19}
Live state
words
3
board
3×3
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | words=3 board=3×3 | All 3 words go into one trie. The board is searched once from every cell, and a branch is abandoned the moment the letters so far are not a prefix of any word. |
| 2 | L13 | cell=(0, 0) letter=o path="o" | Cell (0, 0) holds 'o', and the trie has a 'o' edge here: step in. The path reads "o". |
| 3 | L13 | cell=(0, 1) letter=a path="oa" | Cell (0, 1) holds 'a', and the trie has a 'a' edge here: step in. The path reads "oa". |
| 4 | L13 | cell=(1, 1) letter=t path="oat" | Cell (1, 1) holds 't', and the trie has a 't' edge here: step in. The path reads "oat". |
| 5 | L13 | cell=(2, 1) letter=h path="oath" | Cell (2, 1) holds 'h', and the trie has a 'h' edge here: step in. The path reads "oath". |
| 6 | L14 | word="oath" found=1 | The trie node ends the word "oath": it is on the board. Record it. |
| 7 | L13 | cell=(1, 0) letter=e path="e" | Cell (1, 0) holds 'e', and the trie has a 'e' edge here: step in. The path reads "e". |
| 8 | L13 | cell=(1, 1) letter=t path="t" | Cell (1, 1) holds 't', and the trie has a 't' edge here: step in. The path reads "t". |
| 9 | L13 | cell=(1, 0) letter=e path="te" | Cell (1, 0) holds 'e', and the trie has a 'e' edge here: step in. The path reads "te". |
| 10 | L7 | return=["oath"] cells entered=7 pruned=20 | 1 word found: "oath". The search entered 7 cells and cut 20 branches. |
Practice ladder
Now try it yourself
Easy
- Design Add and Search Words Data Structure
- Longest Word in Dictionary
Medium
- Prefix and Suffix Search
- Replace Words
- Word Break II
Advanced
- Word Search II
- Word Squares
- Stream of Characters
Key takeaways
- A dot makes the search branch over all children; letters keep it on one path.
- Store suffix + '{' + word for every suffix to turn prefix-and-suffix queries into prefix queries.
- In Word Search II, walking the trie and the board together prunes every branch that no word continues.