طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 101 · تركيب البيانات والخوارزميات
الدرس 20 · الوحدة 9: تطبيق تحسينات على الحلول

من O(n²) إلى O(n)

أربعة أنماط تحوّل الحل البطيء إلى سريع، دون تغيير النتيجة. ⏱ نحو 35 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تكتشف العمل المكرر في الحل البسيط.
  • تحوّل البحث داخل الحلقة إلى قاموس أو مجموعة.
  • تطبق النافذة المنزلقة والمجاميع التراكمية.
  • تطبق أسلوب المؤشرين على القوائم المرتبة، وتتحقق أن التحسين لم يغيّر النتيجة.

١طريقة التحسين

أغلب التحسينات الكبيرة لا تأتي من تسريع سطر، بل من تغيير الفكرة: نتخلص من عمل مكرر. والسؤال الذي يقودك: ما الذي أحسبه أكثر من مرة؟ وأين أبحث بحثًا خطيًّا؟

النمط البطيءالتحسينمن ← إلى
بحث داخل حلقةقاموس أو مجموعةO(n²) ← O(n)
مجموع كل نافذة من جديدالنافذة المنزلقةO(n·k) ← O(n)
مجموع كل مجال من جديدالمجاميع التراكميةO(n) لكل سؤال ← O(1)
مقارنة كل زوج في قائمة مرتبةمؤشّران من الطرفينO(n²) ← O(n)

٢1) القاموس بدل الحلقة الداخلية

المسألة: في قائمة أسعار، هل يوجد منتجان مجموع سعريهما يساوي قيمة البطاقة بالضبط؟

main.py
def pair_slow(prices, target):          # O(n²): كل زوج    for i in range(len(prices)):        for j in range(i + 1, len(prices)):            if prices[i] + prices[j] == target:                return prices[i], prices[j]    return None def pair_fast(prices, target):          # O(n): مرور واحد وقاموس    seen = {}    for p in prices:        need = target - p        if need in seen:                # O(1)            return need, p        seen[p] = True    return None prices = [35, 18, 90, 45, 150, 60, 75, 40, 28, 55]print(pair_slow(prices, 100), pair_fast(prices, 100))
Shell×
>>> %Run main.py
(45, 55) (60, 40)
>>>

قد يجد كل حل زوجًا مختلفًا؛ وكلاهما صحيح لأن السؤال «هل يوجد زوج؟».

main.py
import time def pair_slow(prices, target):    for i in range(len(prices)):        for j in range(i + 1, len(prices)):            if prices[i] + prices[j] == target:                return True    return False def pair_fast(prices, target):    seen = set()    for p in prices:        if target - p in seen:            return True        seen.add(p)    return False prices = list(range(1, 2001, 2))     # ألف عدد فرديfor f in (pair_slow, pair_fast):    t = time.perf_counter()    f(prices, 1)                     # لا يوجد زوج مجموعه 1: الحالة الأسوأ    print(f.__name__, round((time.perf_counter() - t) * 1000, 1), "ms")
Shell×
>>> %Run main.py
pair_slow 37.6 ms
pair_fast 0.1 ms
>>>

٣2) النافذة المنزلقة

المسألة: مبيعات يومية لسنة، وأعلى مجموع لـ 7 أيام متتالية. الطريقة الساذجة تجمع كل أسبوع من جديد. الذكية: حين تنزلق النافذة يومًا، أضف اليوم الجديد واطرح اليوم الذي خرج:

main.py
import randomrandom.seed(2)sales = [random.randint(100, 900) for _ in range(365)]k = 7 def best_slow(a, k):                 # O(n·k)    return max(sum(a[i:i + k]) for i in range(len(a) - k + 1)) def best_fast(a, k):                 # O(n)    window = sum(a[:k])    best = window    for i in range(k, len(a)):        window += a[i] - a[i - k]    # أضف الداخل واطرح الخارج        best = max(best, window)    return best print(best_slow(sales, k), best_fast(sales, k))
Shell×
>>> %Run main.py
5054 5054
>>>

٤3) المجاميع التراكمية

المسألة: آلاف الأسئلة من نوع «كم المبيعات من اليوم 30 إلى اليوم 120؟». نحسب مرة واحدة قائمة prefix حيث prefix[i] مجموع أول i يوم، ثم كل سؤال طرح واحد:

main.py
sales = [120, 340, 200, 510, 80, 430, 260, 150] prefix = [0]for s in sales:    prefix.append(prefix[-1] + s)      # O(n) مرة واحدةprint(prefix) def range_sum(i, j):                   # مجموع الأيام من i إلى j شاملة: O(1)    return prefix[j + 1] - prefix[i] print(range_sum(2, 5), sum(sales[2:6]))
Shell×
>>> %Run main.py
[0, 120, 460, 660, 1170, 1250, 1680, 1940, 2090]
1220 1220
>>>

٥4) مؤشّران من الطرفين

إن كانت القائمة مرتبة، فلإيجاد زوج مجموعه قيمة معينة نضع مؤشرًا في البداية وآخر في النهاية: إن كان المجموع أصغر حرّكنا الأيسر، وإن كان أكبر حرّكنا الأيمن:

main.py
def pair_sorted(a, target):     # O(n) وذاكرة O(1)    i, j = 0, len(a) - 1    while i < j:        s = a[i] + a[j]        if s == target:            return a[i], a[j]        if s < target:            i += 1        else:            j -= 1    return None print(pair_sorted([18, 28, 35, 40, 45, 55, 60, 75, 90, 150], 100))
Shell×
>>> %Run main.py
(40, 60)
>>>

٦خطوات التحسين

  1. اكتب الحل البسيط الصحيح أولًا، واختبره.
  2. احسب تعقيده، وحدد السطر الذي يتكرر أكثر.
  3. اسأل: هل أبحث بحثًا خطيًّا؟ هل أحسب الشيء نفسه مرتين؟
  4. طبّق النمط المناسب: قاموس، أو نافذة، أو مجاميع تراكمية، أو مؤشران.
  5. قارن نتيجة الحلين على حالات كثيرة، ثم قِس الزمن.

توقّع وتحقّق

بحث بـ in على قائمة داخل حلقة على n:

n × n.

النافذة المنزلقة: عند الانزلاق يومًا:

O(1) لكل انزلاق.

بعد حساب المجاميع التراكمية، مجموع مجال:

طرح واحد.

أسلوب المؤشرين من الطرفين يحتاج:

ليعرف أي مؤشر يتحرك.

الخلاصة

تحدٍّ برمجي: أطول سلسلة بلا تكرار

متقدم

أطول جزء متصل من سلسلة طلبات العملاء لا يتكرر فيه عميل. اكتب حلًّا O(n) بنافذة منزلقة وقاموس آخر موقع.

المطلوب:
  • قاموس last: العميل ← آخر موقع رآه.
  • start بداية النافذة: إن ظهر عميل داخل النافذة، انقل start بعد ظهوره السابق.
  • اطبع الطول الأطول: 4

ابدأ من هذا الكود، واضغط ▶ شغّل هنا لتكتب حلّك وتجرّبه مباشرة:

main.py
orders = ["سعود", "نورة", "خالد", "سعود", "ريم", "فهد", "خالد", "نورة"]
المطلوب×
5
💡 تلميح 1
طول النافذة الحالية: i - start + 1
💡 تلميح 2
الشرط last[name] >= start يعني أن التكرار داخل النافذة.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

main.py
orders = ["سعود", "نورة", "خالد", "سعود", "ريم", "فهد", "خالد", "نورة"] last = {}start = 0best = 0for i, name in enumerate(orders):    if name in last and last[name] >= start:        start = last[name] + 1    last[name] = i    best = max(best, i - start + 1)print(best)
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس