بحثي النصي كان بطيئاً: كيف أنقذتني خوارزمية KMP من جحيم البحث الخطي؟

مقدمة: حكاية من قلب المعركة البرمجية

يا جماعة الخير، اسمعوا مني هالحكاية. قبل كم سنة، كنت شغال على مشروع كبير، نظام تحليل سجلات (Logs) لشركة ضخمة. الفكرة كانت بسيطة: النظام يقرأ ملايين الأسطر من ملفات السجلات كل دقيقة، ويبحث عن أنماط معينة بتدل على أخطاء أو هجمات أمنية. في البداية، الأمور كانت “عال العال”. بنيت النموذج الأولي باستخدام أبسط طريقة ممكنة للبحث عن نص داخل نص آخر، اللي هي البحث الخطي أو زي ما بحبوا يسموها بالإنجليزي “Naive Search”.

في مرحلة الاختبار على كمية بيانات قليلة، كل شيء كان تمام والعميل مبسوط. لكن يوم الإطلاق الحقيقي، يوم ما شبكنا النظام على السيرفرات اللي بتضخ بيانات حقيقية… وقع الفأس بالرأس. النظام صار بطيء لدرجة الشلل. استهلاك المعالج (CPU) وصل 100%، والذاكرة (RAM) كانت تصرخ وتستغيث. التنبيهات كانت توصل متأخرة بساعات، والموبايل ما كان يهدا من اتصالات مدير المشروع اللي صوته كان يوصلني من رام الله لآخر الدنيا.

شعرت بإحباط ما بعده إحباط. هل يعقل إنه مشروع شهور من التعب ينهار بسبب وظيفة بحث بسيطة؟ قعدت مع حالي، فنجان القهوة السادة جنبي، وصرت أراجع كل سطر كتبته. وقتها، لمعت في بالي ذكرى من أيام الجامعة، من محاضرة الخوارزميات اللي كان يعطينا إياها دكتور الله يذكره بالخير. ذكرى خوارزمية كان اسمها غريب شوي… KMP. يومها كنت أحكي لحالي “شو هالحكي الفاضي النظري، مين بستخدم هالأشياء؟”. ما كنت أعرف إنه هذي الذكرى راح تكون طوق النجاة.

المشكلة: لماذا كان البحث الخطي (Naive Search) كابوسًا؟

قبل ما نحكي عن المنقذ KMP، خلينا نفهم أصل المشكلة. ليش الطريقة “البديهية” للبحث كانت كارثية؟

كيف يعمل البحث الخطي؟

الفكرة بسيطة جدًا، زي ما اسمها بدل. لو بدك تبحث عن كلمة “فلسطين” في نص طويل، بتعمل الآتي:

  1. بتاخذ كلمة “فلسطين” (بنسميها النمط أو Pattern).
  2. بتحطها عند بداية النص الكبير (Text).
  3. بتقارن حرف بحرف. إذا كل الحروف تطابقت، مبروك، لقيت الكلمة.
  4. إذا حصل عدم تطابق، “بتسحب” كلمة “فلسطين” خانة واحدة لليمين، وبترجع تعيد المقارنة من أول وجديد.

خلينا نشوف مثال بسيط. بدنا نبحث عن النمط "ABABC" في النص "ABABDABACDABABCABAB".


النص:  ABABDABACDABABCABAB
النمط: ABABC
       ↑
       (تطابق)

النص:  ABABDABACDABABCABAB
النمط:  ABABC
        ↑
        (تطابق)

النص:  ABABDABACDABABCABAB
النمط:   ABABC
         ↑
         (تطابق)

النص:  ABABDABACDABABCABAB
النمط:    ABABC
          ↑
          (تطابق)

النص:  ABABDABACDABABCABAB
النمط:     ABABC
           ↑
           (هنا فشل! D لا تساوي C)

لما فشلت المقارنة، شو بنعمل؟ البحث الخطي بقول لك: “انسى كل اللي فات، وحرك النمط خطوة واحدة بس لليمين وارجع قارن من الصفر”. وهذا هو مكمن الغباء (مع احترامي للاسم Naive).

أين تكمن الكارثة؟ (التعقيد الزمني)

الكارثة تكمن في هذه العودة العمياء للبداية. في المثال السابق، لما فشلت المقارنة عند حرف D، كنا بنعرف إنه أول 4 حروف من النمط (ABAB) تطابقوا مع النص. هاي معلومة ثمينة جدًا! البحث الخطي برميها في الزبالة وبرجع يبدأ من جديد، مقارنًا الحرف الثاني من النص مع أول حرف من النمط. هذا التكرار غير الضروري هو اللي بقتل الأداء.

بلغة الأرقام، لو طول النص هو n وطول النمط هو m، ففي أسوأ الأحوال (تخيل نص كله حرف ‘A’ وبتبحث عن ‘AA…AB’)، راح تحتاج لـ O(n * m) عملية. لما يكون عندك نصوص بحجم الجيجابايت، هذا الرقم بصير فلكي، والسيرفر “بجيم” وبقعد يعيط.

الحل السحري: خوارزمية KMP.. مش مجرد حروف!

هنا يأتي دور الأبطال الثلاثة: Donald Knuth، و James H. Morris، و Vaughan Pratt. هؤلاء العباقرة فكروا بطريقة مختلفة تمامًا. قالوا: “ليش نرجع لوراء بشكل أعمى؟ ليش ما نستخدم المعلومات اللي جمعناها من التطابق الجزئي عشان نقفز قفزة ذكية للأمام؟”

الفكرة العبقرية وراء KMP

الفكرة الأساسية في KMP هي: عند حدوث عدم تطابق، لا تحرك مؤشر النص للخلف أبدًا. بدلًا من ذلك، انظر إلى النمط نفسه، واستخدم هيكله الداخلي لتحديد مقدار “القفزة” التي يجب أن تقفزها للأمام.

كيف؟ عن طريق تحليل النمط مرة واحدة فقط قبل البدء بالبحث، وإنشاء جدول مساعد صغير. هذا الجدول هو العقل المدبر للعملية كلها.

مفتاح اللغز: جدول LPS (أطول بادئة تطابق لاحقة)

اسم الجدول ممكن يخوف شوي، “Longest Proper Prefix which is also a Suffix”، أو اختصارًا LPS. بس فكرته بسيطة. خلينا نفصصه:

  • البادئة (Prefix): جزء من بداية الكلمة (ليس الكلمة كلها).
  • اللاحقة (Suffix): جزء من نهاية الكلمة (ليس الكلمة كلها).

لكل جزء من النمط، بدنا نعرف ما هي أطول سلسلة حروف تكون في نفس الوقت بادئة ولاحقة لهذا الجزء. خلينا ناخذ مثال عملي، النمط "AABAACAABAA".

لما نوصل للحرف A في الموقع 4 (الكلمة الجزئية هي AABAA):

  • البادئات: A, AA, AAB, AABA
  • اللاحقات: A, AA, BAA, ABAA

أطول سلسلة مشتركة هي "AA"، وطولها 2. إذن، قيمة LPS عند هذا الموقع هي 2.

جدول LPS الكامل للنمط "AABAACAABAA" (طوله 11) سيكون هكذا:


النمط:  A A B A A C A A B A A
الفهرس: 0 1 2 3 4 5 6 7 8 9 10
LPS:    0 1 0 1 2 0 1 2 3 4 5

طيب شو فائدة هذا الجدول يا أبو عمر؟ فائدته جبارة. قيمة LPS عند أي نقطة بتخبرك، في حال فشلت المقارنة، وين ترجع في النمط (وليس في النص) عشان تكمل مقارنة. الرقم lps[i] هو فعليًا طول البادئة التالية اللي ممكن تتطابق، فبتقدر تقفز مباشرة بدون ما ترجع لوراء في النص الأصلي.

لنبني جدول LPS: الكود خطوة بخطوة

كتابة كود بناء جدول LPS هي قلب الخوارزمية. ممكن تبين معقدة لأول وهلة، بس إذا فهمت الفكرة، بتصير منطقية. هذا مثال بايثون مع شرح وافٍ:


def compute_lps_array(pattern):
    m = len(pattern)
    lps = [0] * m  # إنشاء مصفوفة LPS وملؤها بالأصفار
    length = 0  # طول أطول بادئة تطابق لاحقة سابقة
    i = 1

    # الحلقة تبدأ من ثاني حرف لأن lps[0] دائمًا صفر
    while i < m:
        if pattern[i] == pattern[length]:
            # إذا تطابق الحرف الحالي مع الحرف الموجود عند 'length'
            length += 1
            lps[i] = length
            i += 1
        else:
            # إذا لم يتطابقا
            if length != 0:
                # لا نرجع للصفر مباشرة، بل نعود للـ lps السابق
                # هذا هو الجزء السحري، القفزة الذكية داخل النمط نفسه
                length = lps[length - 1]
            else:
                # إذا وصلنا للبداية (length=0)، فلا يوجد تطابق
                lps[i] = 0
                i += 1
    return lps

نصيحة من أبو عمر: لا تحفظ هذا الكود حفظًا. حاول أن تتبعه يدويًا على ورقة وقلم باستخدام نمط مثل "AAABAAA". عندما “تراها” بعينك، لن تنساها أبدًا.

تطبيق KMP على أرض الواقع: من النظرية إلى الكود

بعد ما جهزنا جدول LPS، صار البحث نفسه عملية سهلة ومباشرة جدًا.

خوارزمية البحث الرئيسية

الآن، نمشي على النص والنمط بمؤشرين (i للنص و j للنمط).


def kmp_search(text, pattern):
    n = len(text)
    m = len(pattern)

    if m == 0:
        return [] # لا يمكن البحث عن نمط فارغ

    lps = compute_lps_array(pattern)
    found_indices = []

    i = 0  # مؤشر للنص
    j = 0  # مؤشر للنمط

    while i < n:
        if pattern[j] == text[i]:
            # تطابق! تقدم في كلا المؤشرين
            i += 1
            j += 1
        
        if j == m:
            # مبروك! وجدنا النمط كاملًا
            # نضيف فهرس بداية التطابق
            found_indices.append(i - j)
            # نستمر بالبحث عن تطابقات أخرى باستخدام جدول LPS
            j = lps[j - 1]
        
        elif i < n and pattern[j] != text[i]:
            # عدم تطابق بعد تطابق جزئي
            if j != 0:
                # هنا القفزة الذكية! لا نحرك i
                # فقط نرجع j إلى الخلف حسب جدول LPS
                j = lps[j - 1]
            else:
                # عدم تطابق عند أول حرف في النمط
                # فقط تقدم في النص
                i += 1
    
    return found_indices

# مثال الاستخدام
text = "ABABDABACDABABCABAB"
pattern = "ABABC"
result = kmp_search(text, pattern)
print(f"تم العثور على النمط في الفهرس: {result}") # سيطبع: [10]

مقارنة الأداء: الأرقام لا تكذب

الآن، نأتي للحظة الحقيقة. ما هو التعقيد الزمني لخوارزمية KMP؟

  • بناء جدول LPS يأخذ وقتًا O(m) حيث m هو طول النمط.
  • عملية البحث نفسها تأخذ وقتًا O(n) حيث n هو طول النص (لأن المؤشر i لا يرجع للخلف أبدًا).

إذًا، التعقيد الإجمالي هو O(n + m). مقارنة بـ O(n * m) للبحث الخطي، هذا فرق السماء عن الأرض! في مشروعي، البحث الذي كان يستغرق دقائق وساعات، أصبح يتم في أجزاء من الثانية. كأني انتقلت من عربة تجرها الخيول إلى سيارة فيراري.

نصائح من أبو عمر (خلاصة التجربة)

  • لا تستخف بالأساسيات: الخوارزميات وهياكل البيانات التي تعلمتها في الجامعة ليست مجرد “حكي نظري”. هي الأدوات الحقيقية التي تميز المبرمج المحترف عن الهاوي. استثمر وقتًا في فهمها بعمق.
  • اعرف متى تستخدم KMP: هذه الخوارزمية تتألق عندما تبحث عن نمط ثابت بشكل متكرر في نصوص طويلة أو متغيرة. إذا كان النمط يتغير في كل مرة، فإن تكلفة حساب جدول LPS قد لا تكون مجدية.
  • قِس قبل أن تحسّن (Profile before you optimize): قبل أن تقفز لتغيير خوارزمية البحث، تأكد أولًا أن البحث هو فعلًا “عنق الزجاجة” في تطبيقك. استخدم أدوات تحليل الأداء (Profilers) لتحديد الأجزاء البطيئة بدقة.
  • انظر إلى البدائل: KMP رائعة، لكنها ليست الوحيدة. خوارزمية Boyer-Moore غالبًا ما تكون أسرع في الممارسة العملية لأنها تبدأ المقارنة من نهاية النمط، مما يسمح بقفزات أكبر. خوارزمية Rabin-Karp تستخدم التجزئة (Hashing) وهي ممتازة للبحث عن عدة أنماط مختلفة في نفس الوقت.

الزبدة والخلاصة النهائية 💡

رحلتي مع البحث الخطي و KMP علمتني درسًا لن أنساه: البرمجة ليست مجرد كتابة كود “يعمل”، بل هي فن وعلم حل المشاكل بأكثر الطرق كفاءة وذكاء. في عالم البيانات الضخمة اليوم، الكود البطيء ليس مجرد إزعاج، بل هو فشل في التصميم.

عندما تواجهك مشكلة أداء، لا تستسلم للحلول السطحية. ارجع للأساسيات، فكّر في بنية المشكلة، وابحث عن الأداة (الخوارزمية) الصحيحة للمهمة. استثمار ساعة في فهم خوارزمية مثل KMP يمكن أن يوفر عليك أيامًا من الصداع ومكالمات العملاء الغاضبين. تذكر دائمًا، الكود الأنيق ليس فقط الكود النظيف، بل هو أيضًا الكود السريع والذكي. والله ولي التوفيق.

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

أتمتة العمليات

إشعاراتنا كانت ضجيجاً والمهام تتطلب التنقل بين ألف شاشة: كيف أنقذنا ChatOps من جحيم الفوضى التشغيلية؟

أشارككم تجربتي كـ"أبو عمر"، مبرمج فلسطيني، في تحويل فوضى الإشعارات والتنقل بين الأنظمة إلى بيئة عمل منظمة وفعالة باستخدام ChatOps. اكتشفوا كيف أتمتنا عملياتنا، ووفرنا...

12 أبريل، 2026 قراءة المزيد
نصائح برمجية

شروطنا المتشعبة كانت متاهة: كيف أنقذتنا ‘شروط الحماية’ (Guard Clauses) من جحيم الـ if-else المتداخل؟

هل تعاني من تداخل الشروط في الكود؟ أشاركك قصة حقيقية وكيف غيّرت 'شروط الحماية' (Guard Clauses) طريقة كتابتي للكود، محولةً المتاهات المعقدة إلى مسارات واضحة...

12 أبريل، 2026 قراءة المزيد
​معمارية البرمجيات

خدماتنا كانت متشابكة كخيوط العنكبوت: كيف أنقذتنا ‘المعمارية الموجهة بالأحداث’ (EDA) من جحيم الاعتمادية المباشرة؟

أشارككم قصة حقيقية من تجربتي كـ "أبو عمر" المبرمج، وكيف كانت خدماتنا على وشك الانهيار بسبب التشابك والاعتمادية. اكتشفوا معنا كيف كانت "المعمارية الموجهة بالأحداث"...

12 أبريل، 2026 قراءة المزيد
ذكاء اصطناعي

قرارات نموذجنا كانت صندوقاً أسود: كيف أنقذتنا تقنيات التفسير (XAI) من جحيم التنبؤات الغامضة؟

أشارككم قصة من الميدان، يوم كاد نموذج ذكاء اصطناعي "صندوق أسود" أن يورطنا في قرارات كارثية. هذه المقالة هي دليلك لفهم تقنيات الذكاء الاصطناعي القابل...

12 أبريل، 2026 قراءة المزيد
خوارزميات

قاعدة بياناتنا كانت تستغيث: كيف أنقذنا ‘فلتر بلوم’ من جحيم التحقق المكلف من وجود العناصر؟

أشارككم قصة حقيقية من ميدان المعركة البرمجية، يوم كادت قاعدة بياناتنا أن تنهار تحت ضغط عمليات التحقق المتكررة. سأكشف لكم عن السلاح السري الذي أنقذنا،...

12 أبريل، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

تطبيقنا كان حصناً منيعاً: كيف أنقذتنا ‘معايير الوصول الرقمي (WCAG)’ من جحيم الإقصاء الرقمي؟

أشارككم قصة حقيقية حول كيف أدركنا أن تطبيقنا "المثالي" كان يقصي شريحة كبيرة من المستخدمين. هذه المقالة هي رحلتنا في فهم وتطبيق معايير الوصول الرقمي...

11 أبريل، 2026 قراءة المزيد
البودكاست