من O(n²) إلى O(n)
أربعة أنماط تحوّل الحل البطيء إلى سريع، دون تغيير النتيجة. ⏱ نحو 35 دقيقة.
- تكتشف العمل المكرر في الحل البسيط.
- تحوّل البحث داخل الحلقة إلى قاموس أو مجموعة.
- تطبق النافذة المنزلقة والمجاميع التراكمية.
- تطبق أسلوب المؤشرين على القوائم المرتبة، وتتحقق أن التحسين لم يغيّر النتيجة.
١طريقة التحسين
أغلب التحسينات الكبيرة لا تأتي من تسريع سطر، بل من تغيير الفكرة: نتخلص من عمل مكرر. والسؤال الذي يقودك: ما الذي أحسبه أكثر من مرة؟ وأين أبحث بحثًا خطيًّا؟
| النمط البطيء | التحسين | من ← إلى |
|---|---|---|
| بحث داخل حلقة | قاموس أو مجموعة | O(n²) ← O(n) |
| مجموع كل نافذة من جديد | النافذة المنزلقة | O(n·k) ← O(n) |
| مجموع كل مجال من جديد | المجاميع التراكمية | O(n) لكل سؤال ← O(1) |
| مقارنة كل زوج في قائمة مرتبة | مؤشّران من الطرفين | O(n²) ← O(n) |
٢1) القاموس بدل الحلقة الداخلية
المسألة: في قائمة أسعار، هل يوجد منتجان مجموع سعريهما يساوي قيمة البطاقة بالضبط؟
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))
قد يجد كل حل زوجًا مختلفًا؛ وكلاهما صحيح لأن السؤال «هل يوجد زوج؟».
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")
٣2) النافذة المنزلقة
المسألة: مبيعات يومية لسنة، وأعلى مجموع لـ 7 أيام متتالية. الطريقة الساذجة تجمع كل أسبوع من جديد. الذكية: حين تنزلق النافذة يومًا، أضف اليوم الجديد واطرح اليوم الذي خرج:
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))
٤3) المجاميع التراكمية
المسألة: آلاف الأسئلة من نوع «كم المبيعات من اليوم 30 إلى اليوم 120؟». نحسب مرة واحدة قائمة prefix حيث prefix[i] مجموع أول i يوم، ثم كل سؤال طرح واحد:
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]))
٥4) مؤشّران من الطرفين
إن كانت القائمة مرتبة، فلإيجاد زوج مجموعه قيمة معينة نضع مؤشرًا في البداية وآخر في النهاية: إن كان المجموع أصغر حرّكنا الأيسر، وإن كان أكبر حرّكنا الأيمن:
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))
٦خطوات التحسين
- اكتب الحل البسيط الصحيح أولًا، واختبره.
- احسب تعقيده، وحدد السطر الذي يتكرر أكثر.
- اسأل: هل أبحث بحثًا خطيًّا؟ هل أحسب الشيء نفسه مرتين؟
- طبّق النمط المناسب: قاموس، أو نافذة، أو مجاميع تراكمية، أو مؤشران.
- قارن نتيجة الحلين على حالات كثيرة، ثم قِس الزمن.
توقّع وتحقّق
بحث بـ in على قائمة داخل حلقة على n:
n × n.النافذة المنزلقة: عند الانزلاق يومًا:
O(1) لكل انزلاق.بعد حساب المجاميع التراكمية، مجموع مجال:
أسلوب المؤشرين من الطرفين يحتاج:
الخلاصة
- التحسين الكبير يأتي من حذف العمل المكرر لا من تسريع سطر.
- بحث داخل حلقة ← قاموس أو
set: منO(n²) إلىO(n). - النافذة المنزلقة: أضف الداخل واطرح الخارج.
- المجاميع التراكمية: مجموع أي مجال بطرح واحد.
- المؤشران من الطرفين للقوائم المرتبة.
تحدٍّ برمجي: أطول سلسلة بلا تكرار
متقدمأطول جزء متصل من سلسلة طلبات العملاء لا يتكرر فيه عميل. اكتب حلًّا O(n) بنافذة منزلقة وقاموس آخر موقع.
- قاموس last: العميل ← آخر موقع رآه.
- start بداية النافذة: إن ظهر عميل داخل النافذة، انقل start بعد ظهوره السابق.
- اطبع الطول الأطول: 4
ابدأ من هذا الكود، واضغط ▶ شغّل هنا لتكتب حلّك وتجرّبه مباشرة:
orders = ["سعود", "نورة", "خالد", "سعود", "ريم", "فهد", "خالد", "نورة"]💡 تلميح 1
i - start + 1💡 تلميح 2
last[name] >= start يعني أن التكرار داخل النافذة.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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)