Design Add and Search Words Data Structure
Hãy xây dựng một từ điển từ gồm hai thao tác. addWord w lưu từ w. search p cho biết có từ nào đã lưu khớp với mẫu p hay không: các từ có cùng độ dài và tại mỗi vị trí, mẫu chứa cùng chữ cái hoặc dấu chấm ., đại diện cho một chữ cái bất kỳ.
Thao tác i là ops[i], áp dụng lên từ hoặc mẫu words[i]. Thực hiện các thao tác theo thứ tự trên một từ điển rỗng và trả về một chuỗi cho mỗi thao tác: "null" cho addWord, "true" hoặc "false" cho search.
Hàm
- opsstring-array
- các phép toán, theo thứ tự chúng chạy
- wordsstring-array
- từ cần thêm hoặc mẫu cần tìm kiếm, mỗi thao tác một từ hoặc mẫu
- Trả vềstring-array
- một câu trả lời cho mỗi phép toán, dưới dạng văn bản
Ràng buộc
1 ≤ ops.length ≤ 2500words.length == ops.length- Mỗi
ops[i]làaddWordhoặcsearch. 1 ≤ words[i].length ≤ 20- Một từ cần thêm chỉ chứa các chữ cái tiếng Anh viết thường.
- Mẫu cần tìm chứa các chữ cái tiếng Anh viết thường và tối đa 3 dấu chấm.
Ví dụ
- Đầu vào
- ops = ["addWord", "addWord", "search", "search", "search", "search"]words = ["rose", "rise", "r.se", "ros", "rose.", "..s."]
- Đầu ra
- ["null", "null", "true", "false", "false", "true"]
- Giải thích
r.sekhớp với cả hai từ: dấu chấm khớp với chữ o trongrosevà chữ i trongrise. Dấu chấm không bao giờ đại diện cho một chữ cái bị thiếu, vì vậyrosvàrose.không khớp...s.khớp vì cả hai từ đều có chữ s ở vị trí thứ ba.
- Đầu vào
- ops = ["search", "addWord", "search", "search", "search"]words = ["...", "sky", "...", "s.y", "sk"]
- Đầu ra
- ["false", "null", "true", "true", "false"]
- Giải thích
- Từ điển ban đầu trống nên
...không tìm thấy gì. Khi thêmsky, cả...vàs.yđều khớp với nó, cònskthì quá ngắn.
- Đầu vào
- ops = ["addWord", "addWord", "search", "search", "search"]words = ["maple", "mango", "ma..o", "m...y", "m.p.e"]
- Đầu ra
- ["null", "null", "true", "false", "true"]
- Giải thích
- Dấu chấm đầu tiên trong
ma..ocó thể là chữ p trongmaplehoặc chữ n trongmango;maplekết thúc bằng chữ e, nhưngmangophù hợp. Không có từ nào kết thúc bằng chữ y, nênm...ykhông khớp, cònm.p.ekhớp vớimaple.
Câu hỏi mở rộng
Bạn sẽ mở rộng search như thế nào nếu một mẫu cũng có thể chứa *, đại diện cho bất kỳ dãy chữ cái nào, kể cả dãy rỗng?
Gợi ý
Mở từng gợi ý một. Mỗi gợi ý tiết lộ thêm một chút.
Nếu không có dấu chấm, tập hợp băm chứa các từ đã lưu sẽ trả lời mỗi lần tìm kiếm chỉ với một lần tra cứu. Dấu chấm làm thay đổi điều đó:
c.tcó thể làcat,cothoặccut. Cấu trúc nào cho phép bạn lần theo một từ từng chữ cái một và xem mọi chữ cái có thể xuất hiện tiếp theo?Lưu các từ trong một trie, với một cờ ở mỗi nút đánh dấu nơi một từ được lưu kết thúc. Một chữ cái trong mẫu sẽ đi theo một liên kết đến nút con. Dấu chấm có thể đại diện cho bất kỳ chữ cái nào, nên cần thử từng nút con của nút hiện tại và quay lui khi một nhánh đi vào ngõ cụt.
Viết hàm đệ quy
match(node, i). Khi đến cuối mẫu, trả lời bằng cờ kết thúc của nút. Với một chữ cái, đi theo nút con tương ứng hoặc thất bại nếu không có nút con đó. Với dấu chấm, trả về true ngay khimatch(child, i+1)thành công với một nút con nào đó, và trả về false nếu không có nút con nào thành công.
Lời giải
Dấu chấm có nghĩa là phép tìm kiếm không thể chỉ rõ từ cần tìm: ma..o đại diện cho 676 từ có thể có, nhưng chỉ những từ được lưu mới quan trọng. Trong trie, các từ có phần đầu giống nhau dùng chung một đường đi. Một chữ cái trong mẫu sẽ đi theo một liên kết, dấu chấm mở rộng tìm kiếm theo các nút con hiện có, còn tìm kiếm theo chiều sâu sẽ quay lui từ các nhánh cụt.
So sánh mẫu với từng từ đã lưu
Ý tưởng
Lưu các từ đã thêm vào một danh sách rồi kiểm tra từng từ theo mẫu: cùng độ dài, và ở mỗi vị trí là dấu chấm hoặc cùng một chữ cái. Đó là định nghĩa của một kết quả khớp, được kiểm tra với mọi từ đã lưu, để tìm ra đáp án chính xác.
Mỗi lần tìm kiếm đều phải kiểm tra toàn bộ danh sách, O(n·L) với n từ có độ dài tối đa L chữ cái, kể cả khi mẫu không có dấu chấm. 1,250 lần thêm, tiếp theo là 1,250 lần tìm kiếm, sẽ kiểm tra 1,250 × 1,250 = 1,562,500 từ.
Thuật toán
- Giữ các từ đã thêm trong một danh sách. Với
addWord w, thêmwvào danh sách và ghi lại"null". - Với
search p, so sánhpvới từng từ có cùng độ dài, lần lượt theo từng vị trí, trong đó dấu chấm chấp nhận bất kỳ chữ cái nào. - Ghi lại
"true"khi tìm thấy kết quả khớp đầu tiên, hoặc"false"nếu không có kết quả nào.
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 với thuật toán tìm kiếm theo chiều sâu trên các dấu chấm
Ý tưởng
Đưa các từ vào một trie: rose và rise dùng chung nút r, rồi tách thành o và i. Cờ kết thúc đánh dấu mỗi nút nơi một từ đã lưu kết thúc. Một mẫu gồm các chữ cái tương ứng với một đường đi xuống trie. Dấu chấm có thể là bất kỳ chữ cái nào, nên tại dấu chấm, hãy thử mọi nút con hiện có và trả về true ngay khi một nhánh dẫn đến kết quả khớp.
Với ma..o, nút ma có các nút con p và n. Qua p, dấu chấm thứ hai dẫn đến l, nút này không có nút con o, nên quá trình tìm kiếm quay lui và thử n: g, rồi o, nơi kết thúc từ mango.
Kết quả khớp phải dùng hết toàn bộ mẫu tại một nút có cờ, điều này giải quyết vấn đề độ dài: ros dừng ở một nút không có cờ, còn rose. đi đến cuối các nhánh. Một dấu chấm mở ra tối đa 26 nhánh, nên d dấu chấm duyệt tối đa 26^d · L nút, còn mẫu không có dấu chấm có độ phức tạp O(L).
Thuật toán
- Mỗi nút có các liên kết đến nút con và một cờ kết thúc, ban đầu tắt.
- Với
addWord w: đi theo từng ký tự củawtừ nút gốc, tạo các nút còn thiếu và bật cờ kết thúc tại nút dừng lại. match(node, i): nếuibằng độ dài của mẫu, trả về cờ kết thúc của nút.- Nếu
p[i]là một chữ cái, trả về false nếu thiếu nút con tương ứng, nếu không thì gọimatch(child, i+1). Nếu đó là dấu chấm, trả về true nếumatch(child, i+1)thành công với bất kỳ nút con nào hiện có, nếu không thì trả về false. - Ghi lại
match(root, 0)cho mỗi lần tìm kiếm,"null"cho mỗi lần thêm.
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
Lỗi thường gặp và trường hợp biên
- Để nút con đầu tiên quyết định kết quả của dấu chấm. Với
ma..o, nhánh p thất bại còn nhánh n thành công, nên code trả về kết quả của nút con đầu tiên sẽ cho kết quả false khi thử nhánh p trước. Chỉ trả về false sau khi tất cả các nút con đều thất bại. - Trả về true khi mẫu kết thúc ở bất kỳ nút nào. Sau khi thêm
rose,rosđến được một nút, nhưng không có từ nào kết thúc tại đó. Hãy kiểm tra cờ kết thúc. - Để dấu chấm khớp với một chữ cái bị thiếu.
card.khớp vớicardsnhưng không khớp vớicard. - Xem dấu chấm là một chữ cái. Dùng
'.' - 'a'để lấy chỉ số trong mảng gồm 26 ô sẽ đọc bên ngoài mảng, vì vậy hãy kiểm tra dấu chấm trước.
Câu hỏi thường gặp3
Độ phức tạp thời gian của Add và Search Words là bao nhiêu?
Thêm một từ có độ dài L mất O(L), tìm kiếm không có dấu chấm cũng vậy, bất kể có bao nhiêu từ được lưu trữ. Trong trường hợp xấu nhất, tìm kiếm có d dấu chấm mất O(26^d · L). Bộ nhớ cần O(T) nút cho T chữ cái đã thêm.
Bạn có thể giải bài này bằng tập hợp băm thay vì trie không?
Có, nhưng có sự đánh đổi. Tập hợp có thể khớp một mẫu không có dấu chấm chỉ bằng một lần tra cứu. Với các dấu chấm, bạn có thể duyệt qua mọi từ đã lưu có độ dài đó, O(n·L), hoặc thay mỗi dấu chấm bằng cả 26 chữ cái rồi tra cứu từng kết quả, cần 17,576 lần tra cứu cho ba dấu chấm.
Nên tìm kiếm trong trie bằng đệ quy hay lặp?
Cả hai cách đều được. Đệ quy đi sâu một cấp cho mỗi chữ cái trong mẫu, tối đa là 20 cấp ở đây, nên ngăn xếp lời gọi vẫn nhỏ. Phiên bản lặp dùng một ngăn xếp gồm các cặp (node, position) và đưa mọi nút con vào ngăn xếp tại mỗi dấu chấm.
Bài tập tương tự
Các bài tập dùng cùng ý tưởng. Giải hai hoặc ba bài là bạn sẽ nhớ được mẫu này.
def wordDictionaryOps(ops, words):
# Viết code ở đâyKiểm thử #1
Kiểm thử #2
Kiểm thử #3
Đối số
ops = ["addWord", "addWord", "search", "search", "search", "search"] words = ["rose", "rise", "r.se", "ros", "rose.", "..s."]
Đầu ra mong đợi
["null", "null", "true", "false", "false", "true"]