التعقيد المساحي
الزمن ليس كل شيء: كم ذاكرة تحتاج خوارزميتك؟ ⏱ نحو 30 دقيقة.
- تعرّف التعقيد المساحي وتحسبه لخوارزميات بسيطة.
- تفرّق بين الحل في المكان والحل بنسخة.
- تحسب مساحة مكدس الاستدعاءات في العودية.
- توفّر الذاكرة بالمولّدات، وتطبّق مقايضة الذاكرة بالزمن.
١ما التعقيد المساحي؟
التعقيد المساحي Space Complexity: كم ذاكرة إضافية تحتاجها الخوارزمية أثناء العمل، كدالة في n. لا نحسب المدخل نفسه عادة، بل ما تحجزه الخوارزمية فوقه: متغيرات، وقوائم جديدة، ومكدس الاستدعاءات.
| الحالة | التعقيد المساحي | مثال |
|---|---|---|
| عدد ثابت من المتغيرات | O(1) | مجموع القائمة بمتغير total |
| قائمة جديدة بحجم المدخل | O(n) | نسخة مقلوبة من القائمة |
مصفوفة n × n | O(n²) | جدول المسافات بين كل مدينتين |
| عودية بعمق d | O(d) | المضروب العودي: عمق n ← O(n) |
٢في المكان أم بنسخة؟
قلب القائمة بطريقتين: الأولى تنشئ قائمة جديدة، والثانية تبدّل العناصر داخل القائمة نفسها (في المكان In-place):
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)
نقيس الفرق فعليًّا على مئة ألف عنصر:
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")
٣العودية تستهلك ذاكرة
كل استدعاء ينتظر في مكدس الاستدعاءات يحجز إطارًا في الذاكرة. لذلك المجموع العودي O(n) مساحة، والمجموع بحلقة O(1):
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))))
مع 5000 عنصر تتجاوز النسخة العودية حد العمق (نحو 1000)، بينما الحلقة لا تبالي. وهذا من أسباب تفضيل الحلقة للتكرار البسيط.
٤وفّر الذاكرة بالمولّد
إن كنت تحتاج القيم واحدة واحدة فقط، فلا تحفظها كلها في قائمة. المولّد Generator يحسب القيمة حين تُطلب:
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))
٥المقايضة: ذاكرة أكثر ← زمن أقل
كثيرًا ما نشتري السرعة بالذاكرة. سؤال يتكرر آلاف المرات: «هل هذا الرقم أولي؟». نحسب الإجابات مرة واحدة ونحفظها في جدول، فتصبح كل إجابة بعدها O(1):
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])
توقّع وتحقّق
مساحة دالة تحسب المجموع بمتغير واحد:
قلب القائمة بالتبديل داخلها:
مساحة المضروب العودي factorial(n):
n إطار في المكدس.حفظ النتائج مسبقًا لتسريع الأسئلة مثال على:
Space-time tradeoff.الخلاصة
- المساحة: ما تحجزه الخوارزمية فوق المدخل، كدالة في
n. - في المكان
O(1)، والنسخةO(n). - العودية بعمق
dتحتاجO(d)في مكدس الاستدعاءات. - المولّد يحسب عند الطلب بدل حفظ كل القيم.
- نشتري الزمن بالذاكرة أحيانًا: جدول محسوب مسبقًا.
تحدٍّ برمجي: تدوير القائمة في المكان
متقدمدوّر القائمة خطوة واحدة إلى اليمين (الأخير يصبح الأول) داخل القائمة نفسها، دون إنشاء قائمة جديدة ودون insert.
- احفظ آخر عنصر في متغير.
- أزح العناصر من الآخر إلى الأول خطوة واحدة بحلقة.
- ضع المحفوظ في الموقع 0، واطبع القائمة.
- اطبع أيضًا: نفس القائمة: True (بمقارنة id قبل وبعد)
ابدأ من هذا الكود، واضغط ▶ شغّل هنا لتكتب حلّك وتجرّبه مباشرة:
nums = [10, 20, 30, 40, 50]before = id(nums)
💡 تلميح 1
for i in range(len(nums) - 1, 0, -1): nums[i] = nums[i - 1]💡 تلميح 2
O(1)حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
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)