هياكل البيانات والخوارزميات / براهين التحسين والتجميعات المحفوظة / الدرس 17
دوال السابقة ومطابقات النص المتداخلة
أعد استخدام حدود النمط المثبتة بدلا من إعادة مسح النص، واحفظ المطابقات المتداخلة بدلالة Unicode صريحة.
في هذه الصفحة
أهداف الدرس
- عرّف معنى المحرف ومعنى المطابقة
- تسجل قيمة السابقة أطول حد صحيح غير شامل للكل
- تتبّع سابقة النمط المطابقة أثناء مسح النص
عرّف معنى المحرف ومعنى المطابقة
يبحث هذا الدرس عن تطابق دقيق في نصوص Python وفقا لنقاط ترميز Unicode، وليس وفقا للعناقيد الكتابية التي يدركها المستخدم أو لتكافؤ نصي مستقل عن الإعداد المحلي. وقد تتشابه نصوص بصريا مع اختلاف تسلسل نقاط ترميزها. إذا احتجت إلى التطبيع أو طي حالة الأحرف، فعرّفه قبل المطابقة، واشرح كيفية إعادة مواضع النص المحوّل إلى النص الأصلي. تحوي نتيجة المطابقة فهارس البدايات، بما فيها المطابقات المتداخلة. ويُعرّف النمط الفارغ هنا بأنه يطابق كل حد من 0 إلى len(text). هذه اختيارات مقصودة للواجهة، وليست اصطلاحات عامة. ويفترض التحليل المبسط مساواة ثابتة الزمن لعناصر التسلسل المنفردة.
تسجل قيمة السابقة أطول حد صحيح غير شامل للكل
لسابقة نمط تنتهي عند الفهرس i، تكون pi[i] طول أطول سابقة صحيحة تكون لاحقة أيضا. وتعني «صحيحة» هنا استبعاد السابقة الحالية كاملة. يمد البرنامج الأول الحد السابق عندما تتفق المحارف التالية. وعند الاختلاف يرجع إلى حد ذلك الحد، أي pi[j-1]، لأن المرشحين الأقصر غير الواقعين على هذه السلسلة لا يستطيعون حفظ اللاحقة التي طوبقت بالفعل. تفقد إعادة j إلى j-1 عميانا هذه المعلومات البنيوية، وقد تضيف عملا غير لازم. يلخص الجدول الناتج حقائق مساواة قابلة لإعادة الاستخدام داخل النمط، قبل فحص النص.
def prefix_function(pattern):
pi = [0] * len(pattern)
for i in range(1, len(pattern)):
j = pi[i - 1]
while j and pattern[i] != pattern[j]:
j = pi[j - 1]
if pattern[i] == pattern[j]:
j += 1
pi[i] = j
return pi
print(prefix_function("ababaca"))
print(prefix_function(""))
[0, 0, 1, 2, 3, 0, 1]
[]
عند a الأخيرة يصح حد طوله 1. وعند c تفشل جميع مرشحات الحدود السابقة المكونة من a وb، فيصبح طول الحد 0.
تتبّع سابقة النمط المطابقة أثناء مسح النص
يحافظ البرنامج الثاني على أن j هو طول سابقة النمط المساوية للاحقة النص الذي عولج حتى الآن. تتبع الاختلافات جدول السابقة دون تحريك فهرس النص إلى الخلف. وعند مطابقة كاملة يسجل البداية، ثم يرجع إلى pi[j-1]، فيحفظ حدا يمكن أن يشارك في مطابقة متداخلة تالية. وتفوت إعادة j إلى الصفر بعد كل مطابقة حالات تداخل مثل aa في aaaa. لا يضم التنفيذ النمط والنص بواسطة فاصل، فيتجنب شرطا إضافيا هو اختيار رمز لا يظهر في أي من المدخلين.
def prefix_function(pattern):
pi = [0] * len(pattern)
for i in range(1, len(pattern)):
j = pi[i - 1]
while j and pattern[i] != pattern[j]:
j = pi[j - 1]
if pattern[i] == pattern[j]:
j += 1
pi[i] = j
return pi
def find_all(text, pattern):
if not pattern:
return list(range(len(text) + 1))
pi = prefix_function(pattern)
matches, j = [], 0
for i, char in enumerate(text):
while j and char != pattern[j]:
j = pi[j - 1]
if char == pattern[j]:
j += 1
if j == len(pattern):
matches.append(i - len(pattern) + 1)
j = pi[j - 1]
return matches
print(find_all("aaaa", "aa"))
print(find_all("باباب", "باب"))
print(find_all("ab", ""))
[0, 1, 2]
[0, 2]
[0, 1, 2]
تستخدم العينة العربية نقاط ترميز دقيقة بلا تشكيل، وتشمل مطابقات متداخلة. ويسرد ناتج النمط الفارغ الحدود، وليس المحارف.
يبقى مجموع عمل حلقات الرجوع المتداخلة خطيا
أثناء بناء جدول السابقة، يزيد j بمقدار واحد على الأكثر في كل خطوة خارجية، ويخفضه كل رجوع حصرا. وعلى امتداد التنفيذ يحد مجموع الزيادات السابقة مجموع الانخفاضات، فيكون بناء الجدول خطيا. وينطبق الاستدلال نفسه، المشابه لتحليل الجهد، على مسح النص: تزيد الامتدادات الناجحة j، وتستهلك الإخفاقات التقدم المتراكم دون إعادة زيارة مواضع النص. إذا كان طول النمط m، وطول النص n، وعدد المطابقات الناتجة z، فالمجموع O(m+n+z)، والمساحة المخزنة O(m+z) في هذه الواجهة التي تجسد الناتج. لا يكفي مجرد رؤية while داخل for لاستنتاج تعقيد تربيعي.
تُعاد التهيئة فقط عند ثبات عقد النمط
إذا تشاركت نصوص كثيرة نمطا ثابتا واحدا، فيمكن الاحتفاظ بجدول سابقته وإعادة استخدامه. ويُبطل الجدول تغير النمط أو قاعدة التطبيع أو سياسة المقارنة. ويمكن للمطابق التدفقي الاحتفاظ بـ j بين القطع أيضا، لكن فهارس المطابقة تحتاج إلى إزاحة عامة متراكمة وإلى حد فك محارف متسق عبر القطع. تقسيم بايتات خام في وسط ترميز Unicode مشكلة مستقلة لفك الترميز، ولا يحلها مطابق النص. لا تحوّل فهارس نقاط الترميز مباشرة إلى إزاحات بايت أو مواضع عناقيد كتابية دون الربط اللازم. اجعل عقود التمثيل هذه صريحة.
قارن كل نتيجة صغيرة بالشرائح المباشرة
عد النصوص والأنماط الصغيرة على أبجدية محدودة، وقارن find_all بفحص كل موضع بداية صالح باستخدام الشرائح. أدرج الرموز المتكررة، وغياب المطابقة، ونمطا أطول من النص، ونصا فارغا، ونمطا فارغا، ومطابقات متداخلة. اختبر بضع عينات Unicode صريحة بصورة منفصلة عن عد ASCII. وتحقق من جدول السابقة مباشرة بفحص كل حد صحيح للأنماط الصغيرة. قد تكون هذه المراجع المستقلة أبطأ، لكنها واضحة. تدعم مجموعة الفحوص المحدودة برهان الحدود، ولا تثبت مطابقة تراعي العناقيد الكتابية أو ترتيبا لغويا محليا أو ادعاءات أداء النصوص الكبيرة.
الأخطاء الشائعة وإصلاحها
اختفاء المطابقات المتداخلة
- لماذا يحدث؟
- أُعيد طول السابقة المطابقة إلى الصفر بعد النجاح.
- كيف تصلحه؟
- ارجع إلى أطول حد صحيح للمطابقة الكاملة.
- كيف تتجنب تكراره؟
- اختبر aa في aaaa، وأنماطا عربية مكررة.
إعادة نمط متغير استخدام جدول سابقة قديم
- لماذا يحدث؟
- أغفلت هوية المخزن المؤقت النمط أو سياسة المقارنة.
- كيف تصلحه؟
- أعد البناء عند تغير أي اعتماد للتهيئة المسبقة.
- كيف تتجنب تكراره؟
- غيّر النمط مع الحفاظ على طوله.
الإبلاغ عن فهارس نقاط الترميز كأنها إزاحات بايت
- لماذا يحدث؟
- اختلط تمثيل النص بتمثيل البايتات المرمزة.
- كيف تصلحه؟
- احفظ الإزاحات في التمثيل المعلن، أو ابن ربطا صريحا بين التمثيلات.
- كيف تتجنب تكراره؟
- أدرج عينات متعددة البايت وعينات ذات محارف تجميعية.
اختبر فهمك
1. لماذا تنتج مطابقة aa داخل aaaa ثلاث نتائج؟
اعرض الحل المشروح
تختار كل بداية من 0 و1 و2 حرفي a متتاليين. يسمح العقد بالتداخل، لذلك لا يجوز أن يؤدي العثور على أول ظهور إلى إسقاط اللاحقة المشتركة. يحفظ الرجوع إلى حد طوله 1 بعد كل مطابقة كاملة احتمال التداخل التالي.
2. لماذا يمكن أن يبقى عمل حلقات الرجوع المتداخلة خطيا؟
اعرض الحل المشروح
يستهلك كل رجوع تقدما في السابقة المطابقة جمعته امتدادات سابقة. ولا تزيد الامتدادات أكثر من واحد لكل موضع ممسوح؛ لذلك يحد مجموع الزيادات السابقة مجموع عدد الانخفاضات. يتجاهل اعتبار كل while مستقلة ذات كلفة n هذا الحد المشترك للتقدم.
أهم ما تعلمته
- عرّف معنى المحرف ومعنى المطابقة
- قارن كل نتيجة صغيرة بالشرائح المباشرة