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

التعقيد المساحي

الزمن ليس كل شيء: كم ذاكرة تحتاج خوارزميتك؟ ⏱ نحو 30 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تعرّف التعقيد المساحي وتحسبه لخوارزميات بسيطة.
  • تفرّق بين الحل في المكان والحل بنسخة.
  • تحسب مساحة مكدس الاستدعاءات في العودية.
  • توفّر الذاكرة بالمولّدات، وتطبّق مقايضة الذاكرة بالزمن.

١ما التعقيد المساحي؟

التعقيد المساحي Space Complexity: كم ذاكرة إضافية تحتاجها الخوارزمية أثناء العمل، كدالة في n. لا نحسب المدخل نفسه عادة، بل ما تحجزه الخوارزمية فوقه: متغيرات، وقوائم جديدة، ومكدس الاستدعاءات.

الحالةالتعقيد المساحيمثال
عدد ثابت من المتغيراتO(1)مجموع القائمة بمتغير total
قائمة جديدة بحجم المدخلO(n)نسخة مقلوبة من القائمة
مصفوفة n × nO(n²)جدول المسافات بين كل مدينتين
عودية بعمق dO(d)المضروب العودي: عمق n ← O(n)

٢في المكان أم بنسخة؟

قلب القائمة بطريقتين: الأولى تنشئ قائمة جديدة، والثانية تبدّل العناصر داخل القائمة نفسها (في المكان In-place):

main.py
def reversed_copy(a):          # ذاكرة إضافية O(n)    result = []    for i in range(len(a) - 1, -1, -1):        result.append(a[i])    return result def reverse_in_place(a):       # ذاكرة إضافية O(1)    i, j = 0, len(a) - 1    while i < j:        a[i], a[j] = a[j], a[i]        i += 1        j -= 1 nums = [1, 2, 3, 4, 5]print(reversed_copy(nums), nums)reverse_in_place(nums)print(nums)
Shell×
>>> %Run main.py
[5, 4, 3, 2, 1] [1, 2, 3, 4, 5]
[5, 4, 3, 2, 1]
>>>

نقيس الفرق فعليًّا على مئة ألف عنصر:

main.py
import tracemalloc def reversed_copy(a):    return [a[i] for i in range(len(a) - 1, -1, -1)] def reverse_in_place(a):    i, j = 0, len(a) - 1    while i < j:        a[i], a[j] = a[j], a[i]        i += 1; j -= 1 data = list(range(100_000))for f in (reversed_copy, reverse_in_place):    tracemalloc.start()    f(data)    peak = tracemalloc.get_traced_memory()[1]    tracemalloc.stop()    print(f.__name__, "←", peak // 1024, "KB")
Shell×
>>> %Run main.py
reversed_copy ← 782 KB
reverse_in_place ← 0 KB
>>>

٣العودية تستهلك ذاكرة

كل استدعاء ينتظر في مكدس الاستدعاءات يحجز إطارًا في الذاكرة. لذلك المجموع العودي O(n) مساحة، والمجموع بحلقة O(1):

main.py
import sys def total_rec(a, i=0):    if i == len(a):        return 0    return a[i] + total_rec(a, i + 1) def total_loop(a):    s = 0    for x in a:        s += x    return s print(total_loop(list(range(5000))))print("حد العمق في بايثون:", sys.getrecursionlimit())print(total_rec(list(range(500))))
Shell×
>>> %Run main.py
12497500
حد العمق في بايثون: 1000
124750
>>>

مع 5000 عنصر تتجاوز النسخة العودية حد العمق (نحو 1000)، بينما الحلقة لا تبالي. وهذا من أسباب تفضيل الحلقة للتكرار البسيط.

٤وفّر الذاكرة بالمولّد

إن كنت تحتاج القيم واحدة واحدة فقط، فلا تحفظها كلها في قائمة. المولّد Generator يحسب القيمة حين تُطلب:

main.py
import sys squares_list = [x * x for x in range(1_000_000)]   # مليون قيمة محفوظةsquares_gen = (x * x for x in range(1_000_000))    # تُحسب عند الطلب print(sys.getsizeof(squares_list) // 1024, "KB")print(sys.getsizeof(squares_gen), "بايت")print(sum(squares_gen) == sum(squares_list))
Shell×
>>> %Run main.py
8250 KB
200 بايت
True
>>>

٥المقايضة: ذاكرة أكثر ← زمن أقل

كثيرًا ما نشتري السرعة بالذاكرة. سؤال يتكرر آلاف المرات: «هل هذا الرقم أولي؟». نحسب الإجابات مرة واحدة ونحفظها في جدول، فتصبح كل إجابة بعدها O(1):

main.py
def is_prime(n):                 # بلا ذاكرة: O(√n) لكل سؤال    if n < 2:        return False    k = 2    while k * k <= n:        if n % k == 0:            return False        k += 1    return True # جدول محسوب مسبقًا (غربال إراتوستينس): ذاكرة O(N)، وكل سؤال بعده O(1)N = 100table = [True] * (N + 1)table[0] = table[1] = Falsefor i in range(2, int(N ** 0.5) + 1):    if table[i]:        for j in range(i * i, N + 1, i):            table[j] = False print(is_prime(97), table[97])print([n for n in range(N + 1) if table[n]][:10])
Shell×
>>> %Run main.py
True True
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
>>>

توقّع وتحقّق

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

متغير ثابت.

قلب القائمة بالتبديل داخلها:

في المكان.

مساحة المضروب العودي factorial(n):

n إطار في المكدس.

حفظ النتائج مسبقًا لتسريع الأسئلة مثال على:

Space-time tradeoff.

الخلاصة

تحدٍّ برمجي: تدوير القائمة في المكان

متقدم

دوّر القائمة خطوة واحدة إلى اليمين (الأخير يصبح الأول) داخل القائمة نفسها، دون إنشاء قائمة جديدة ودون insert.

المطلوب:
  • احفظ آخر عنصر في متغير.
  • أزح العناصر من الآخر إلى الأول خطوة واحدة بحلقة.
  • ضع المحفوظ في الموقع 0، واطبع القائمة.
  • اطبع أيضًا: نفس القائمة: True (بمقارنة id قبل وبعد)

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

main.py
nums = [10, 20, 30, 40, 50]before = id(nums)
المطلوب×
[50, 10, 20, 30, 40]
نفس القائمة: True
💡 تلميح 1
for i in range(len(nums) - 1, 0, -1): nums[i] = nums[i - 1]
💡 تلميح 2
الذاكرة الإضافية: متغير واحد last ← O(1)

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

main.py
nums = [10, 20, 30, 40, 50]before = id(nums) last = nums[-1]for i in range(len(nums) - 1, 0, -1):    nums[i] = nums[i - 1]nums[0] = last print(nums)print("نفس القائمة:", id(nums) == before)
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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