The 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)
Addsword
to the data structure; it can be matched later.bool search(word)
Returnstrue
if there is any string in the data structure that matchesword
orfalse
otherwise.word
may contain dots ('.'
), where dots can be matched with any letter.
Examples
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
Constraints
1 <= word.length <= 25
word
inaddWord
consists of lowercase English letters.word
insearch
consists of'.'
or lowercase English letters.- There will be at most 2 dots in
word
forsearch
queries. - At most
10^4
calls will be made toaddWord
andsearch
.
Brute force solution
class WordDictionary {
private var store: [String]
init() {
self.store = []
}
func addWord(_ word: String) {
self.store.append(word)
}
func search(_ word: String) -> Bool {
let wordArray = Array(word)
let wordCount = wordArray.count
for w in self.store {
let wArray = Array(w)
let wCount = wArray.count
if wCount != wordCount {
continue
}
var i = 0
while i < wCount {
if wArray[i] == wordArray[i] || wordArray[i] == "." {
i += 1
} else {
break
}
if i == wCount {
return true
}
}
}
return false
}
}
Explanation
We can solve this problem in a brute-force way by using an array.
It is not the most efficient solution because the search
operation will take O(m * n) time. We can optimize it by using a Trie (Prefix Tree) data structure.
- For the
addWord
method, we will use theappend
operation, which takes O(1) time. - For
search
, we need a little more effort because we cannot only search for a specificword
, but the input may contain dots ('.'
), which can be matched with any letter. For example, if we have input["c", "d"]
and we are looking forโ.dโ
, we will iterate through each element instore
and compare it with the inputword
character by character, also checking if a character can be.
.
Time/space complexity
- Time complexity: O(1) for
addWord
, and O(m * n) forsearch
. - Space complexity: O(m * n),
wherem
is the number of words added to the array andn
is the length of the word.
Trie (Prefix Tree) solution
final class TrieNode {
var children: [Character: TrieNode?]
var endOfWord: Bool
init() {
self.children = [:]
self.endOfWord = false
}
}
class WordDictionary {
private let root: TrieNode
init() {
self.root = TrieNode()
}
func addWord(_ word: String) {
var curr = self.root
for c in word {
if curr.children[c] == nil {
curr.children[c] = TrieNode()
}
curr = curr.children[c]!!
}
curr.endOfWord = true
}
func search(_ word: String) -> Bool {
func dfs(_ j: Int, _ node: TrieNode) -> Bool {
var curr = node
let wordArray = Array(word)
let wordCount = wordArray.count
for i in j ..< wordCount {
let c = wordArray[i]
if c == "." {
for child in curr.children.values {
if let childNode = child, dfs(i + 1, childNode) {
return true
}
}
return false
} else {
if curr.children[c] == nil {
return false
}
curr = curr.children[c]!!
}
}
return curr.endOfWord
}
return dfs(0, self.root)
}
}
Explanation
We can solve this problem in a more optimal way by using a Trie (Prefix Tree) data structure. It will help us decrease the time complexity for search
from O(m * n) to O(n).
To do that, we need an additional class TrieNode
that will store children
nodes by character and an endOfWord
property that will tell us when a sequence of characters forms a complete word.
- For
addWord
, we iterate through all characters in the inputword
and mark it as theendOfWord
. - For
search
, we need to add a depth-first search (DFS) helper (backtracking). This allows us to search for a match in every node when we have.
in the input because.
matches any character.
For example, when wesearch
for.ad
and we have["bad", "dad", "mad"]
as added words, we returntrue
in all these cases because.ad
matches all of them.
This solution is more efficient because when we find a match, we return true
and stop iterating, using recursion (DFS).
Time/space complexity
- Time complexity: O(n) for
addWord
andsearch
. - Space complexity: O(t + n),
wheren
is the length of the word andt
is the total number ofTrieNode
s created in the Trie.