Design Add and Search Words Data Structure
أنشئ قاموسًا للكلمات يتضمن عمليتين. تخزّن addWord w الكلمة w. تحدد search p ما إذا كانت هناك كلمة مخزّنة تطابق النمط p: بالطول نفسه، وفي كل موضع يحتوي النمط على الحرف نفسه أو نقطة .، التي ترمز إلى أي حرف واحد.
العملية i هي ops[i] على الكلمة أو النمط words[i]. نفّذ العمليات بالترتيب على قاموس فارغ، وأعِد سلسلة واحدة لكل عملية: "null" لعملية addWord، و"true" أو "false" لعملية search.
الدالة
- opsstring-array
- العمليات، بالترتيب الذي تُنفَّذ به
- wordsstring-array
- الكلمة المراد إضافتها أو النمط المراد البحث عنه، واحد لكل عملية
- تُرجعstring-array
- إجابة واحدة لكل عملية، كنص
القيود
1 ≤ ops.length ≤ 2500words.length == ops.length- كل عنصر من
ops[i]هو إماaddWordأوsearch. 1 ≤ words[i].length ≤ 20- الكلمة المراد إضافتها تحتوي فقط على أحرف إنجليزية صغيرة.
- يحتوي النمط المراد البحث عنه على أحرف إنجليزية صغيرة، وبحد أقصى 3 نقاط.
أمثلة
- المدخلات
- ops = ["addWord", "addWord", "search", "search", "search", "search"]words = ["rose", "rise", "r.se", "ros", "rose.", "..s."]
- المخرجات
- ["null", "null", "true", "false", "false", "true"]
- الشرح
- يطابق
r.seكلتا الكلمتين: فالنقطة تحل محل الحرف o فيroseوالحرف i فيrise. لا تحلّ النقطة أبدًا محل حرف مفقود، لذا يفشل كلٌّ منrosوrose.. يطابق..s.الكلمتين، لأن الحرف s يأتي ثالثًا في كلتيهما.
- المدخلات
- ops = ["search", "addWord", "search", "search", "search"]words = ["...", "sky", "...", "s.y", "sk"]
- المخرجات
- ["false", "null", "true", "true", "false"]
- الشرح
- يبدأ القاموس فارغًا، لذا لا يعثر
...على شيء. بعد إضافةsky، يطابقه كلٌّ من...وs.y، بينماskقصير جدًا.
- المدخلات
- ops = ["addWord", "addWord", "search", "search", "search"]words = ["maple", "mango", "ma..o", "m...y", "m.p.e"]
- المخرجات
- ["null", "null", "true", "false", "true"]
- الشرح
- يمكن أن تكون النقطة الأولى في
ma..oهي الحرف p منmapleأو الحرف n منmango؛ تنتهيmapleبالحرف e، لكنmangoتطابق النمط. لا تنتهي أي كلمة بالحرف y، لذا يفشلm...y، ويطابقm.p.eالكلمةmaple.
سؤال إضافي
كيف ستوسّع search إذا كان من الممكن أن يحتوي النمط أيضًا على *، للدلالة على أي سلسلة من الأحرف، بما في ذلك سلسلة فارغة؟
تلميحات
افتحها واحدًا تلو الآخر. كل تلميح يكشف أكثر قليلًا.
من دون نقاط، ستجيب مجموعة تجزئة للكلمات المخزنة عن كل عملية بحث من خلال عملية بحث واحدة. لكن النقطة تغيّر ذلك: قد تكون
c.tهيcatأوcotأوcut. ما البنية التي تتيح لك تتبّع كلمة حرفًا واحدًا في كل مرة ورؤية كل حرف يمكن أن يأتي بعدها؟خزّن الكلمات في شجرة بادئة (trie)، مع علامة على كل عقدة تنتهي عندها كلمة مخزّنة. يتبع الحرف في النمط رابطًا واحدًا إلى ابن. يمكن أن تمثل النقطة أي حرف، لذا يجب تجربة كل ابن للعقدة الحالية، والرجوع عند الوصول إلى طريق مسدود في أحد الفروع.
اكتب دالة تكرارية
match(node, i). عند نهاية النمط، أجب بعلامة نهاية العقدة. عند وجود حرف، اتبع ذلك الفرع، أو أعد الفشل إن لم يكن موجودًا. عند وجود نقطة، أعد true فور نجاحmatch(child, i+1)لأي فرع، وأعد false إن لم ينجح أي فرع.
الحل
تعني النقطة أن البحث لا يستطيع تحديد الكلمة التي يريدها: تشير ma..o إلى 676 كلمة محتملة، ولا يهم سوى الكلمات المخزّنة. في الشجرة الجذرية (trie)، تشترك الكلمات التي تبدأ بالطريقة نفسها في مسار واحد. يتبع حرف في النمط رابطًا واحدًا، وتتفرع النقطة إلى الأبناء الموجودين فقط، ويتراجع البحث بالعمق أولًا عن الفروع المسدودة.
قارن النمط بكل كلمة مخزنة
الفكرة
احتفظ بالكلمات المضافة في قائمة، واختبر كل كلمة منها بمقارنتها بالنمط: الطول نفسه، وفي كل موضع نقطة أو الحرف نفسه. هذا هو تعريف التطابق، ويُتحقَّق منه بمقارنة النمط بكل كلمة مخزنة، لذا تكون الإجابة صحيحة.
تتطلب كل عملية بحث فحص القائمة بأكملها، O(n·L) لـ n كلمة، طول كل منها حتى L حرفًا، حتى عندما لا يحتوي النمط على نقطة. تؤدي 1,250 عملية إضافة تليها 1,250 عملية بحث إلى اختبار 1,250 × 1,250 = 1,562,500 كلمة.
الخوارزمية
- احتفِظ بالكلمات المضافة في قائمة. عند
addWord w، ألحِقwوسجّل"null". - بالنسبة إلى
search p، قارِنpبكل كلمة لها الطول نفسه، موضعًا بموضع، مع السماح للنقطة بقبول أي حرف. - سجّل
"true"عند أول تطابق، أو"false"إذا لم يوجد أي تطابق.
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 resultشجرة Trie مع بحث بالعمق أولًا على النقاط
الفكرة
ضع الكلمات في شجرة بادئة: تشترك rose وrise في العقدة r، ثم تتفرع إلى o وi. تشير علامة النهاية إلى كل عقدة تنتهي عندها كلمة مخزنة. نمط من الأحرف هو مسار واحد عبر شجرة البادئة. قد تمثل النقطة أي حرف، لذا جرّب عند النقطة كل ابن موجود، وأعِد true فور أن يؤدي أحدها إلى تطابق.
في ma..o، للعقدة المقابلة لـ ma ابنان هما p وn. عبر p، تصل النقطة الثانية إلى l، التي ليس لها ابن o، لذا يتراجع البحث ويجرّب n: ثم g، ثم o، حيث تنتهي mango.
يجب أن يستهلك التطابق النمط بأكمله عند عقدة تحمل علامة، وهذا يحسم مسألة الطول: يتوقف ros عند عقدة بلا علامة، وينفد rose. من الروابط. تفتح النقطة 26 فرعًا كحد أقصى، لذا تزور d نقاط ما يصل إلى 26^d · L عقدة، وتكون كلفة نمط بلا نقاط O(L).
الخوارزمية
- لكل عقدة روابط للأبناء وعلامة نهاية، تكون معطّلة في البداية.
- بالنسبة إلى
addWord w: اتبعwانطلاقًا من الجذر، وأنشئ العقد الناقصة، ثم اضبط علامة النهاية عند موضع توقف المسار. match(node, i): إذا كانiيساوي طول النمط، فأعِد علامة النهاية للعقدة.- إذا كان
p[i]حرفًا، فأعِد false إذا كان ابنه مفقودًا، وإلا فاستدعِmatch(child, i+1). وإذا كانت نقطة، فأعِد true إذا نجحmatch(child, i+1)لأي ابن موجود، وإلا فأعِد false. - سجّل
match(root, 0)لكل عملية بحث، و"null"لكل عملية إضافة.
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
أخطاء شائعة وحالات حدّية
- السماح للابن الأول بتحديد نتيجة النقطة. في
ma..oيفشل فرع p وينجح فرع n، لذا فإن الشيفرة التي تُرجع نتيجة الابن الأول تُجيب بـ false عندما تجرّب p أولًا. أجب بـ false فقط بعد فشل جميع الأبناء. - الإجابة بـ true عندما ينفد النمط عند أي عقدة. بعد إضافة
rose، يصلrosإلى عقدة، لكن لا تنتهي أي كلمة عندها. تحقّق من علامة النهاية. - السماح للنقطة بمطابقة حرف مفقود. يطابق
card.كلمةcards، لكنه لا يطابقcard. - اعتبار النقطة حرفًا. تؤدي الفهرسة في مصفوفة ذات 26 خانة باستخدام
'.' - 'a'إلى قراءة خارج المصفوفة، لذا تحقّق من النقطة أولًا.
أسئلة شائعة3
ما التعقيد الزمني لإضافة الكلمات والبحث عنها؟
تستغرق إضافة كلمة طولها L وقتًا قدره O(L)، وكذلك البحث الذي لا يحتوي على نقاط، مهما كان عدد الكلمات المخزنة. يستغرق البحث الذي يحتوي على d نقاط O(26^d · L) في أسوأ الحالات. يتطلب ذلك ذاكرة قدرها O(T) من العُقد مقابل T حرفًا مضافًا.
هل يمكنك حلّها باستخدام مجموعة تجزئة بدلًا من شجرة بادئة؟
نعم، مع بعض المفاضلات. تجيب المجموعة عن نمط لا يحتوي على نقاط بعملية بحث واحدة. أما مع النقاط، فإما أن تفحص كل كلمة مخزنة بهذا الطول، O(n·L)، أو تستبدل كل نقطة بجميع الأحرف الـ26 وتبحث عن كل نتيجة، أي 17,576 عملية بحث لثلاث نقاط.
هل ينبغي أن يكون البحث في الشجرة البادئة تكراريًا أم حلقيًا؟
كلاهما صالح. ينتقل الاستدعاء التكراري مستوىً واحدًا لكل حرف في النمط، وبحد أقصى 20 هنا، لذا يظل مكدس الاستدعاءات صغيرًا. يحتفظ الإصدار التكراري بمكدس من أزواج (node, position) ويدفع كل ابن عند النقطة.
مسائل مشابهة
مسائل تعتمد على الأفكار نفسها. حلّ اثنتين أو ثلاث منها يثبّت النمط.
def wordDictionaryOps(ops, words):
# اكتب الشيفرة هنااختبار #1
اختبار #2
اختبار #3
الوسائط
ops = ["addWord", "addWord", "search", "search", "search", "search"] words = ["rose", "rise", "r.se", "ros", "rose.", "..s."]
الإخراج المتوقع
["null", "null", "true", "false", "false", "true"]