Skip to content

Commit 49fe30a

Browse files
committed
Design add and search words data structure
1 parent f2be809 commit 49fe30a

File tree

2 files changed

+345
-0
lines changed

2 files changed

+345
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# 211. Design add and search words data structure
2+
# Topics: 'String', 'Depth-First Search', 'Design', 'Trie'
3+
4+
# Design a data structure that supports adding new words and finding if a string matches any previously added string.
5+
6+
# Implement the WordDictionary class:
7+
8+
# WordDictionary() Initializes the object.
9+
# void addWord(word) Adds word to the data structure, it can be matched later.
10+
# bool search(word) Returns true if there is any string in the data structure that matches word or false otherwise. word may contain dots '.' where dots can be matched with any letter.
11+
12+
13+
# Example:
14+
15+
# Input
16+
# ["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
17+
# [[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
18+
# Output
19+
# [null,null,null,null,false,true,true,true]
20+
21+
# Explanation
22+
# WordDictionary wordDictionary = new WordDictionary();
23+
# wordDictionary.addWord("bad");
24+
# wordDictionary.addWord("dad");
25+
# wordDictionary.addWord("mad");
26+
# wordDictionary.search("pad"); // return False
27+
# wordDictionary.search("bad"); // return True
28+
# wordDictionary.search(".ad"); // return True
29+
# wordDictionary.search("b.."); // return True
30+
31+
32+
33+
# Constraints:
34+
35+
# 1 <= word.length <= 25
36+
# word in addWord consists of lowercase English letters.
37+
# word in search consist of '.' or lowercase English letters.
38+
# There will be at most 2 dots in word for search queries.
39+
# At most 104 calls will be made to addWord and search.
40+
41+
class Node:
42+
def __init__(self):
43+
self.children = {}
44+
self.word = False
45+
46+
class WordDictionary:
47+
def __init__(self):
48+
self.root = Node()
49+
50+
def addWord(self, word: str) -> None:
51+
cur = self.root
52+
for c in word:
53+
if c not in cur.children:
54+
cur.children[c] = Node()
55+
cur = cur.children[c]
56+
cur.word = True
57+
58+
def search(self, word: str) -> bool:
59+
return self.pattern_search(self.root, 0, word)
60+
61+
def pattern_search(self, next: Node, i: int, word: str) -> bool:
62+
if i == len(word):
63+
return next.word
64+
if word[i] == '.':
65+
for val in next.children.values():
66+
if self.pattern_search(val, i+1, word):
67+
return True
68+
return False
69+
if word[i] not in next.children:
70+
return False
71+
return self.pattern_search(next.children[word[i]], i+1, word)
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
from search import WordDictionary
2+
3+
def test_example_from_problem():
4+
"""Test the exact example from the problem statement"""
5+
wd = WordDictionary()
6+
7+
wd.addWord("bad")
8+
wd.addWord("dad")
9+
wd.addWord("mad")
10+
11+
assert wd.search("pad") == False, "Should not find 'pad'"
12+
assert wd.search("bad") == True, "Should find 'bad'"
13+
assert wd.search(".ad") == True, "Should match '.ad' with 'bad', 'dad', 'mad'"
14+
assert wd.search("b..") == True, "Should match 'b..' with 'bad'"
15+
16+
print("✓ Problem example test passed")
17+
18+
19+
def test_empty_dictionary():
20+
"""Test operations on empty dictionary"""
21+
wd = WordDictionary()
22+
23+
assert wd.search("test") == False, "Empty dictionary should not find any word"
24+
assert wd.search(".") == False, "Empty dictionary should not match single dot"
25+
assert wd.search("...") == False, "Empty dictionary should not match dots"
26+
27+
print("✓ Empty dictionary test passed")
28+
29+
30+
def test_exact_match():
31+
"""Test exact word matching without wildcards"""
32+
wd = WordDictionary()
33+
34+
wd.addWord("hello")
35+
wd.addWord("world")
36+
37+
assert wd.search("hello") == True, "Should find 'hello'"
38+
assert wd.search("world") == True, "Should find 'world'"
39+
assert wd.search("hell") == False, "Should not find 'hell'"
40+
assert wd.search("worlds") == False, "Should not find 'worlds'"
41+
42+
print("✓ Exact match test passed")
43+
44+
45+
def test_single_wildcard():
46+
"""Test patterns with single wildcard"""
47+
wd = WordDictionary()
48+
49+
wd.addWord("cat")
50+
wd.addWord("bat")
51+
wd.addWord("rat")
52+
53+
assert wd.search(".at") == True, "Should match '.at'"
54+
assert wd.search("c.t") == True, "Should match 'c.t'"
55+
assert wd.search("ca.") == True, "Should match 'ca.'"
56+
assert wd.search(".a.") == True, "Should match '.a.'"
57+
58+
print("✓ Single wildcard test passed")
59+
60+
61+
def test_multiple_wildcards():
62+
"""Test patterns with multiple wildcards"""
63+
wd = WordDictionary()
64+
65+
wd.addWord("abc")
66+
wd.addWord("xyz")
67+
68+
assert wd.search("...") == True, "Should match '...' with 'abc' or 'xyz'"
69+
assert wd.search("a..") == True, "Should match 'a..' with 'abc'"
70+
assert wd.search(".b.") == True, "Should match '.b.' with 'abc'"
71+
assert wd.search("..c") == True, "Should match '..c' with 'abc'"
72+
assert wd.search("..d") == False, "Should not match '..d'"
73+
74+
print("✓ Multiple wildcards test passed")
75+
76+
77+
def test_all_wildcards():
78+
"""Test pattern with all wildcards"""
79+
wd = WordDictionary()
80+
81+
wd.addWord("test")
82+
83+
assert wd.search("....") == True, "Should match '....' with 'test'"
84+
assert wd.search(".....") == False, "Should not match '.....' (length mismatch)"
85+
assert wd.search("...") == False, "Should not match '...' (length mismatch)"
86+
87+
print("✓ All wildcards test passed")
88+
89+
90+
def test_single_character():
91+
"""Test single character words"""
92+
wd = WordDictionary()
93+
94+
wd.addWord("a")
95+
wd.addWord("b")
96+
97+
assert wd.search("a") == True, "Should find 'a'"
98+
assert wd.search("b") == True, "Should find 'b'"
99+
assert wd.search(".") == True, "Should match '.' with 'a' or 'b'"
100+
assert wd.search("c") == False, "Should not find 'c'"
101+
102+
print("✓ Single character test passed")
103+
104+
105+
def test_overlapping_words():
106+
"""Test words with common prefixes"""
107+
wd = WordDictionary()
108+
109+
wd.addWord("car")
110+
wd.addWord("card")
111+
wd.addWord("care")
112+
wd.addWord("careful")
113+
114+
assert wd.search("car") == True, "Should find 'car'"
115+
assert wd.search("card") == True, "Should find 'card'"
116+
assert wd.search("car.") == True, "Should match 'car.' with 'card' or 'care'"
117+
assert wd.search("car..") == False, "Should not match 'car..' (length mismatch with card/care)"
118+
assert wd.search("car....") == True, "Should match 'car....' with 'careful'"
119+
120+
print("✓ Overlapping words test passed")
121+
122+
123+
def test_wildcard_beginning():
124+
"""Test wildcard at the beginning"""
125+
wd = WordDictionary()
126+
127+
wd.addWord("apple")
128+
wd.addWord("apply")
129+
130+
assert wd.search(".pple") == True, "Should match '.pple' with 'apple'"
131+
assert wd.search(".pply") == True, "Should match '.pply' with 'apply'"
132+
assert wd.search(".pp..") == True, "Should match '.pp..' with both words"
133+
134+
print("✓ Wildcard beginning test passed")
135+
136+
137+
def test_wildcard_end():
138+
"""Test wildcard at the end"""
139+
wd = WordDictionary()
140+
141+
wd.addWord("test")
142+
wd.addWord("text")
143+
144+
assert wd.search("tes.") == True, "Should match 'tes.' with 'test'"
145+
assert wd.search("tex.") == True, "Should match 'tex.' with 'text'"
146+
assert wd.search("te..") == True, "Should match 'te..' with both words"
147+
148+
print("✓ Wildcard end test passed")
149+
150+
151+
def test_no_match():
152+
"""Test patterns that should not match"""
153+
wd = WordDictionary()
154+
155+
wd.addWord("hello")
156+
157+
assert wd.search("world") == False, "Should not find 'world'"
158+
assert wd.search("h.....") == False, "Should not match 'h....' (length mismatch)"
159+
assert wd.search("x....") == False, "Should not match 'x....' (wrong prefix)"
160+
assert wd.search(".ello") == True, "Should match '.ello' with 'hello'"
161+
assert wd.search(".ellx") == False, "Should not match '.ellx'"
162+
163+
print("✓ No match test passed")
164+
165+
166+
def test_duplicate_words():
167+
"""Test adding duplicate words"""
168+
wd = WordDictionary()
169+
170+
wd.addWord("test")
171+
wd.addWord("test")
172+
wd.addWord("test")
173+
174+
assert wd.search("test") == True, "Should find 'test' after multiple additions"
175+
assert wd.search("....") == True, "Should match '....' with 'test'"
176+
177+
print("✓ Duplicate words test passed")
178+
179+
180+
def test_long_words():
181+
"""Test with longer words"""
182+
wd = WordDictionary()
183+
184+
long_word = "abcdefghij"
185+
wd.addWord(long_word)
186+
187+
assert wd.search(long_word) == True, "Should find long word"
188+
assert wd.search("abcdefghi.") == True, "Should match with wildcard at end"
189+
assert wd.search(".bcdefghij") == True, "Should match with wildcard at beginning"
190+
assert wd.search("abcd.fghij") == True, "Should match with wildcard in middle"
191+
192+
print("✓ Long words test passed")
193+
194+
195+
def test_wildcard_no_alternatives():
196+
"""Test wildcard when there are no alternatives"""
197+
wd = WordDictionary()
198+
199+
wd.addWord("aaa")
200+
201+
assert wd.search(".aa") == True, "Should match '.aa' with 'aaa'"
202+
assert wd.search("a.a") == True, "Should match 'a.a' with 'aaa'"
203+
assert wd.search("aa.") == True, "Should match 'aa.' with 'aaa'"
204+
assert wd.search("...") == True, "Should match '...' with 'aaa'"
205+
206+
print("✓ Wildcard no alternatives test passed")
207+
208+
209+
def test_different_lengths():
210+
"""Test words of different lengths"""
211+
wd = WordDictionary()
212+
213+
wd.addWord("a")
214+
wd.addWord("ab")
215+
wd.addWord("abc")
216+
wd.addWord("abcd")
217+
218+
assert wd.search(".") == True, "Should match '.' with 'a'"
219+
assert wd.search("..") == True, "Should match '..' with 'ab'"
220+
assert wd.search("...") == True, "Should match '...' with 'abc'"
221+
assert wd.search("....") == True, "Should match '....' with 'abcd'"
222+
assert wd.search(".....") == False, "Should not match '.....' (no word that long)"
223+
224+
print("✓ Different lengths test passed")
225+
226+
227+
def test_complex_patterns():
228+
"""Test complex wildcard patterns"""
229+
wd = WordDictionary()
230+
231+
wd.addWord("at")
232+
wd.addWord("and")
233+
wd.addWord("an")
234+
wd.addWord("add")
235+
236+
assert wd.search("a") == False, "Should not match 'a'"
237+
assert wd.search(".at") == False, "Should not match '.at' (length mismatch)"
238+
assert wd.search("an.") == True, "Should match 'an.' with 'and'"
239+
assert wd.search(".nd") == True, "Should match '.nd' with 'and'"
240+
assert wd.search("a.d") == True, "Should match 'a.d' with 'and' or 'add'"
241+
assert wd.search("a.") == True, "Should match 'a.' with 'at' or 'an'"
242+
assert wd.search(".") == False, "Should not match '.' (no single char words except none exist)"
243+
244+
print("✓ Complex patterns test passed")
245+
246+
247+
def run_all_tests():
248+
"""Run all test cases"""
249+
print("Running WordDictionary tests...\n")
250+
251+
test_example_from_problem()
252+
test_empty_dictionary()
253+
test_exact_match()
254+
test_single_wildcard()
255+
test_multiple_wildcards()
256+
test_all_wildcards()
257+
test_single_character()
258+
test_overlapping_words()
259+
test_wildcard_beginning()
260+
test_wildcard_end()
261+
test_no_match()
262+
test_duplicate_words()
263+
test_long_words()
264+
test_wildcard_no_alternatives()
265+
test_different_lengths()
266+
test_complex_patterns()
267+
268+
print("\n" + "="*50)
269+
print("All tests passed! ✓")
270+
print("="*50)
271+
272+
273+
if __name__ == "__main__":
274+
run_all_tests()

0 commit comments

Comments
 (0)