Design Add and Search Words Data Structure
Zbuduj słownik wyrazów z dwiema operacjami. addWord w zapisuje wyraz w. search p informuje, czy któryś z zapisanych wyrazów pasuje do wzorca p: ma tę samą długość, a na każdej pozycji wzorzec zawiera tę samą literę lub kropkę ., która oznacza dowolną jedną literę.
Operacja i to ops[i] wykonywana na wyrazie lub wzorcu words[i]. Wykonaj operacje po kolei na pustym słowniku i zwróć po jednym ciągu znaków dla każdej: "null" dla addWord, "true" lub "false" dla search.
Funkcja
- opsstring-array
- operacje, w kolejności ich wykonywania
- wordsstring-array
- słowo do dodania lub wzorzec do wyszukania, po jednym na operację
- Zwracastring-array
- jedna odpowiedź na operację, jako tekst
Ograniczenia
1 ≤ ops.length ≤ 2500words.length == ops.length- Każde
ops[i]toaddWordlubsearch. 1 ≤ words[i].length ≤ 20- Dodawane słowo zawiera wyłącznie małe litery alfabetu angielskiego.
- Wzorzec do wyszukania zawiera małe litery alfabetu angielskiego i maksymalnie 3 kropki.
Przykłady
- Wejście
- ops = ["addWord", "addWord", "search", "search", "search", "search"]words = ["rose", "rise", "r.se", "ros", "rose.", "..s."]
- Wyjście
- ["null", "null", "true", "false", "false", "true"]
- Wyjaśnienie
r.sepasuje do obu słów: kropka zastępuje literę o wrosei literę i wrise. Kropka nigdy nie oznacza brakującej litery, więcrosirose.nie pasują...s.pasuje, ponieważ w obu słowach trzecią literą jest s.
- Wejście
- ops = ["search", "addWord", "search", "search", "search"]words = ["...", "sky", "...", "s.y", "sk"]
- Wyjście
- ["false", "null", "true", "true", "false"]
- Wyjaśnienie
- Słownik jest początkowo pusty, więc
...niczego nie znajduje. Gdy dodaszsky, zarówno..., jak is.ypasują do tego słowa, askjest za krótkie.
- Wejście
- ops = ["addWord", "addWord", "search", "search", "search"]words = ["maple", "mango", "ma..o", "m...y", "m.p.e"]
- Wyjście
- ["null", "null", "true", "false", "true"]
- Wyjaśnienie
- Pierwsza kropka w
ma..omoże oznaczać p wmaplealbo n wmango;maplekończy się na e, alemangopasuje. Żadne słowo nie kończy się na y, więcm...ynie pasuje, am.p.epasuje domaple.
Pytanie dodatkowe
Jak rozszerzyć search, jeśli wzorzec może zawierać również *, oznaczającą dowolny ciąg liter, także pusty?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Bez kropek zbiór haszujący przechowywanych słów odpowiadałby na każde wyszukiwanie za pomocą jednego sprawdzenia. Kropka to zmienia:
c.tmoże oznaczaćcat,cotlubcut. Która struktura pozwala przechodzić przez słowo litera po literze i sprawdzać każdą literę, która może wystąpić jako następna?Przechowuj słowa w trie, oznaczając flagą każdy węzeł, na którym kończy się zapisane słowo. Litera we wzorcu prowadzi do jednego węzła potomnego. Kropka może oznaczać dowolną literę, więc trzeba sprawdzić każde dziecko bieżącego węzła, a gdy gałąź prowadzi donikąd, cofnąć się.
Napisz rekurencyjną funkcję
match(node, i). Na końcu wzorca zwróć flagę końca węzła. Dla litery przejdź do tego dziecka albo zakończ niepowodzeniem, jeśli go brakuje. Dla kropki zwróć true, gdy tylkomatch(child, i+1)powiedzie się dla któregoś dziecka, a false, jeśli żadne się nie powiedzie.
Rozwiązanie
Kropka oznacza, że wyszukiwanie nie może określić szukanego słowa: ma..o odpowiada 676 możliwym słowom, a znaczenie mają tylko te zapisane. W trie słowa zaczynające się tak samo współdzielą jedną ścieżkę. Litera we wzorcu prowadzi jednym łączem, kropka rozgałęzia wyszukiwanie tylko na istniejące dzieci, a wyszukiwanie w głąb cofa się z martwych gałęzi.
Porównaj wzorzec z każdym zapisanym słowem
Intuicja
Przechowuj dodane słowa na liście i sprawdzaj każde z nich pod kątem wzorca: tej samej długości oraz kropki lub tej samej litery na każdej pozycji. To definicja dopasowania, sprawdzanego względem każdego zapisanego słowa, więc odpowiedź jest poprawna.
Każde wyszukiwanie przeszukuje całą listę: O(n·L) dla n słów o długości do L liter, nawet jeśli wzorzec nie zawiera kropki. 1,250 dodanych słów, a następnie 1,250 wyszukiwań oznacza sprawdzenie 1,250 × 1,250 = 1,562,500 słów.
Algorytm
- Przechowuj dodane słowa na liście. Dla
addWord wdodajwi zapisz"null". - Dla
search pporównajpz każdym słowem o tej samej długości, pozycja po pozycji, pozwalając, aby kropka oznaczała dowolną literę. - Zapisz
"true"przy pierwszym dopasowaniu lub"false", jeśli nie ma żadnego.
def matches(pattern, word):
# Same length, and every position holds a dot or the same letter.
if len(pattern) != len(word):
return False
for p, w in zip(pattern, word):
if p != "." and p != w:
return False
return True
def wordDictionaryOps(ops, words):
stored = []
result = []
for op, word in zip(ops, words):
if op == "addWord":
stored.append(word)
result.append("null")
else:
found = any(matches(word, s) for s in stored)
result.append("true" if found else "false")
return resultTrie z przeszukiwaniem w głąb po kropkach
Intuicja
Umieść słowa w trie: rose i rise współdzielą węzeł r, a następnie rozdzielają się na o i i. Flaga końca oznacza każdy węzeł, w którym kończy się zapisane słowo. Wzorzec liter to jedno przejście w dół trie. Kropka może oznaczać dowolną literę, więc przy kropce wypróbuj każde istniejące dziecko i zwróć true, gdy tylko jedno z nich prowadzi do dopasowania.
Dla ma..o węzeł dla ma ma dzieci p i n. Ścieżka przez p prowadzi od drugiej kropki do l, które nie ma dziecka o, więc wyszukiwanie cofa się i próbuje n: g, a następnie o, gdzie kończy się mango.
Dopasowanie musi zużyć cały wzorzec i zakończyć się na oflagowanym węźle, co rozstrzyga kwestię długości: ros zatrzymuje się na nieoflagowanym węźle, a rose. nie ma już dalszych krawędzi. Kropka otwiera najwyżej 26 gałęzi, więc d kropek odwiedza najwyżej 26^d · L węzłów, a wzorzec bez kropek ma koszt O(L).
Algorytm
- Każdy węzeł ma łącza do dzieci i flagę końca, początkowo wyłączoną.
- Dla
addWord w: przejdź w dół od korzenia, podążając zaw, utwórz brakujące węzły i ustaw flagę końca w miejscu, w którym kończy się przejście. match(node, i): jeśliijest równe długości wzorca, zwróć flagę końca węzła.- Jeśli
p[i]jest literą, zwróć false, jeśli brakuje odpowiadającego jej dziecka; w przeciwnym razie zwróćmatch(child, i+1). Jeśli jest kropką, zwróć true, jeślimatch(child, i+1)powiedzie się dla dowolnego istniejącego dziecka; w przeciwnym razie zwróć false. - Zapisuj wynik
match(root, 0)dla każdego wyszukiwania, a"null"dla każdego dodawania.
class TrieNode:
def __init__(self):
self.children = {} # letter -> TrieNode
self.is_end = False # does a stored word end at this node?
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def add_word(self, word):
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, pattern):
return self._match(self.root, pattern, 0)
def _match(self, node, pattern, i):
# Can the path below node spell pattern[i:] and end on a stored word?
if i == len(pattern):
return node.is_end
ch = pattern[i]
if ch == ".":
# A dot stands for any letter: try each child until one works.
for child in node.children.values():
if self._match(child, pattern, i + 1):
return True
return False
child = node.children.get(ch)
return child is not None and self._match(child, pattern, i + 1)
def wordDictionaryOps(ops, words):
dictionary = WordDictionary()
result = []
for op, word in zip(ops, words):
if op == "addWord":
dictionary.add_word(word)
result.append("null")
else:
result.append("true" if dictionary.search(word) else "false")
return result
Pułapki i przypadki brzegowe
- Pozwolenie, by pierwsze dziecko decydowało o kropce. W
ma..ogałąź p kończy się niepowodzeniem, a gałąź n kończy się powodzeniem, więc kod zwracający wynik pierwszego dziecka odpowiada false, gdy najpierw sprawdza p. Odpowiedz false dopiero wtedy, gdy wszystkie dzieci zakończą się niepowodzeniem. - Odpowiadanie true, gdy wzorzec kończy się na dowolnym węźle. Po dodaniu
roserosdociera do węzła, ale nie kończy się tam żadne słowo. Sprawdź flagę końca. - Pozwolenie, by kropka pasowała do brakującej litery.
card.pasuje docards, ale nie docard. - Traktowanie kropki jak litery. Indeksowanie 26-elementowej tablicy za pomocą
'.' - 'a'odczytuje wartość spoza tablicy, więc najpierw sprawdź, czy to kropka.
Najczęstsze pytania3
Jaka jest złożoność czasowa funkcji Add i Search Words?
Dodanie słowa o długości L zajmuje O(L), podobnie jak wyszukiwanie bez kropek, niezależnie od liczby przechowywanych słów. Wyszukiwanie z d kropkami zajmuje w najgorszym przypadku O(26^d · L). Pamięć wynosi O(T) węzłów dla T dodanych liter.
Czy potrafisz rozwiązać to za pomocą zbioru haszującego zamiast drzewa trie?
Tak, ale wiąże się to z kompromisami. Zbiór dopasowuje wzorzec bez kropek w jednym wyszukiwaniu. W przypadku kropek możesz albo sprawdzić każde zapisane słowo o danej długości, O(n·L), albo zastąpić każdą kropkę każdą z 26 liter i wyszukać każdy wynik — 17 576 wyszukiwań dla trzech kropek.
Czy wyszukiwanie w trie powinno być rekurencyjne czy iteracyjne?
Oba sposoby działają. Rekurencja schodzi o jeden poziom dla każdej litery wzorca, tutaj maksymalnie o 20, więc stos wywołań pozostaje niewielki. Wersja iteracyjna przechowuje stos par (węzeł, pozycja) i umieszcza na nim każde dziecko przy kropce.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
def wordDictionaryOps(ops, words):
# Napisz kod tutajTest #1
Test #2
Test #3
Argumenty
ops = ["addWord", "addWord", "search", "search", "search", "search"] words = ["rose", "rise", "r.se", "ros", "rose.", "..s."]
Oczekiwany wynik
["null", "null", "true", "false", "false", "true"]