0Pricing
Coding Interview Prep · درس

الاستدعاء الذاتي وطريقة شجرة الاستدعاء الذاتي

تتبّع الاستدعاءات الذاتية ضمن أشجار، وطبّق مبرهنة Master، واستنتج التعقيدات الزمنية لفرز الدمج والعاملي ومتغيرات Fibonacci

الاستدعاء الذاتي وطريقة شجرة الاستدعاء الذاتي درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

الاستدعاء الذاتي ومكدس الاستدعاءات

عندما تستدعي الدالة نفسها، يضيف كل استدعاء إطارًا إلى المكدس، فتتراكم الإطارات حتى الوصول إلى حالة الأساس، ثم تُفكّ بالترتيب العكسي. إن تصوّر ذلك هو الخطوة الأولى لتحليل الاستدعاء الذاتي.

def factorial(n):
    if n == 0:       # base case
        return 1
    return n * factorial(n - 1)  # recursive call

# Call chain: factorial(4)
#   4 * factorial(3)
#     3 * factorial(2)
#       2 * factorial(1)
#         1 * factorial(0) -> 1
# Unwinds: 1, 2, 6, 24
print(factorial(5))  # 120

شجرة الاستدعاء الذاتي لمتتالية فيبوناتشي

توسّع شجرة الاستدعاء الذاتي كل استدعاء إلى الاستدعاءات الفرعية له. تنقسم Fibonacci الساذجة إلى استدعاءين في كل مرة، فتشكّل شجرة تضم نحو 2^n عقدة — أي بتعقيد O(2^n). اطّلع على الشيفرة.

call_count = [0]

def fib_naive(n):
    call_count[0] += 1
    if n <= 1:
        return n
    return fib_naive(n-1) + fib_naive(n-2)

for n in [5, 10, 15, 20]:
    call_count[0] = 0
    result = fib_naive(n)
    print(f'fib({n})={result}, calls={call_count[0]}')
# Calls roughly double each time n increases by 1

التعرّف على المسائل الفرعية المتكررة

في تلك الشجرة، تتكرر الاستدعاءات نفسها مثل fib(3) عبر الفروع. وتشير هذه المسائل الفرعية المتداخلة إلى إمكانية استخدام الحفظ، الذي يخفض التعقيد من O(2^n) إلى O(n).

# Memoised: each unique sub-problem computed once
def fib_memo(n, memo={}):
    if n in memo: return memo[n]
    if n <= 1:    return n
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

call_count2 = [0]
def fib_counted(n, memo={}):
    call_count2[0] += 1
    if n in memo: return memo[n]
    if n <= 1:    return n
    memo[n] = fib_counted(n-1, memo) + fib_counted(n-2, memo)
    return memo[n]

fib_counted(20)
print(f'calls with memo: {call_count2[0]}')  # only 21

شجرة الاستدعاء الذاتي لفرز الدمج

تتكون شجرة فرز الدمج من log n مستويات، وينفَّذ في كل مستوى عمل إجمالي بتعقيد O(n) — إذ تتم معالجة كل عنصر مرة واحدة. اضربهما لتحصل على O(n log n). اطّلع على الشيفرة.

# Merge sort: at each level, n total elements are merged
# Level 0:  1 merge of n elements    -> n work
# Level 1:  2 merges of n/2 each     -> n work
# Level 2:  4 merges of n/4 each     -> n work
# ...log(n) levels...
# Total: n * log(n)

# Verify with operation counter:
def merge_sort_counted(arr):
    ops = [0]
    def _sort(a):
        if len(a) <= 1: return a
        m = len(a) // 2
        l, r = _sort(a[:m]), _sort(a[m:])
        result, i, j = [], 0, 0
        while i < len(l) and j < len(r):
            ops[0] += 1
            if l[i] <= r[j]: result.append(l[i]); i+=1
            else:             result.append(r[j]); j+=1
        return result + l[i:] + r[j:]
    return _sort(arr), ops[0]

_, c = merge_sort_counted(list(range(64, 0, -1)))
print(f'Merge ops: {c}')  # ~384 ~ 64*log2(64)=384

نظرية الماستر

تحل نظرية الماستر العلاقة T(n) = a*T(n/b) + O(n^d) عبر ثلاث حالات. وبالنسبة إلى فرز الدمج (a=2, b=2, d=1)، فإنها تعطي O(n log n). احفظ الحالات الثلاث للاختبار.

# Merge sort: T(n) = 2*T(n/2) + O(n)
# a=2, b=2, d=1, log_b(a)=log2(2)=1=d  => O(n log n)

# Binary search: T(n) = 1*T(n/2) + O(1)
# a=1, b=2, d=0, log2(1)=0=d  => O(log n)

# Strassen matrix mult: T(n) = 7*T(n/2) + O(n^2)
# a=7, b=2, d=2, log2(7)~2.81 > 2 => O(n^log2(7)) ~ O(n^2.81)

import math
print('log2(7) =', math.log2(7))  # 2.807...

رسم أشجار الاستدعاء الذاتي: خطوة بخطوة

لرسم شجرة استدعاء ذاتي: ضع T(n) في الأعلى، ووسّع كل استدعاء، واجمع العمل في كل مستوى، ثم اضربه في عدد المستويات. تدرّب حتى تصبح العملية تلقائية.

# Factorial: T(n) = T(n-1) + O(1)
# Tree is a chain: n levels, O(1) each -> O(n)

# Fibonacci: T(n) = T(n-1) + T(n-2) + O(1)
# Binary tree of depth n, ~2^n nodes -> O(2^n)

# Merge sort: T(n) = 2*T(n/2) + O(n)
# Log levels, n work each -> O(n log n)

def count_recursive_calls(n, results=[]):
    if n <= 1:
        results.append(n)
        return n
    return count_recursive_calls(n-1, results) + count_recursive_calls(n-2, results)

results = []
count_recursive_calls(8, results)
print(f'fib(8) leaf calls: {len(results)}')

الاستدعاء الذاتي الأُسّي: المجموعات الجزئية

يكون توليد جميع المجموعات الجزئية بتعقيد O(2^n) — إذ يوجد منها بالضبط 2^n مجموعة، ولذلك لا يمكنك تحقيق أفضل من ذلك. يكون كل عنصر إما ضمن المجموعة أو خارجها، فتُبنى شجرة ثنائية من الاختيارات. اطّلع على الشيفرة.

def subsets(nums):
    result = []
    def backtrack(start, current):
        result.append(list(current))  # O(n) copy
        for i in range(start, len(nums)):
            current.append(nums[i])
            backtrack(i + 1, current)
            current.pop()
    backtrack(0, [])
    return result

nums = [1, 2, 3]
ss = subsets(nums)
print(len(ss))  # 8 = 2^3
print(ss)

الاستدعاء الذاتي النهائي وتحسينه

يحدث الاستدعاء الذاتي النهائي عندما يكون الاستدعاء الذاتي هو الخطوة الأخيرة تمامًا. تعيد بعض اللغات استخدام الإطار له، لكن Python لا تفعل ذلك — لذا ستؤدي الاستدعاءات العميقة إلى تجاوز سعة المكدس. استخدم حلقة بدلًا من ذلك.

# Tail-recursive factorial (accumulator pattern)
def fact_tail(n, acc=1):
    if n == 0:
        return acc
    return fact_tail(n - 1, n * acc)  # tail call

# Python does NOT TCO, so this overflows for large n
# Instead, convert to iterative:
def fact_iter(n):
    acc = 1
    while n > 0:
        acc *= n
        n -= 1
    return acc

print(fact_tail(10))  # 3628800
print(fact_iter(10))  # 3628800

التعقيد المكاني للاستدعاء الذاتي

يحتفظ كل استدعاء ذاتي بإطار، ولذلك يستهلك الاستدعاء الذاتي مساحة بتعقيد O(العمق). يكون الاستدعاء الذاتي الخطي بتعقيد O(n)، بينما يكون بحث DFS في شجرة متوازنة بتعقيد O(log n). إذا تعمقت أكثر من اللازم، فستصل إلى RecursionError.

import sys
print(sys.getrecursionlimit())  # default 1000

# Increase limit for deep problems
sys.setrecursionlimit(10000)

# Track max depth manually
def max_depth_tracker(n, depth=0, max_seen=[0]):
    max_seen[0] = max(max_seen[0], depth)
    if n <= 0:
        return
    max_depth_tracker(n - 1, depth + 1, max_seen)
    return max_seen[0]

print(max_depth_tracker(50))  # 50  => O(n) stack frames

شجرة الاستدعاء الذاتي لفرز Quick sort

يكون Quick sort بتعقيد O(n log n) عند اختيار محور جيد، لكن اختيار محور سيئ مع إدخال مرتب قد يرفع التعقيد إلى O(n^2). لذلك يهم اختيار المحور عشوائيًا. اطّلع على الشيفرة.

import random

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = random.choice(arr)  # randomised -> O(n log n) expected
    less    = [x for x in arr if x < pivot]
    equal   = [x for x in arr if x == pivot]
    greater = [x for x in arr if x > pivot]
    return quick_sort(less) + equal + quick_sort(greater)

print(quick_sort([3, 6, 8, 10, 1, 2, 1]))  # sorted

دالة القوة: استدعاء ذاتي بتعقيد log n

يحتاج الحساب الساذج لـ x^n إلى O(n) عملية ضرب، لكن التربيع ينصّف العمل في كل خطوة: x^n = (x^(n/2))^2. وهذا يعطي O(log n) بوضوح — تطبيقًا لفكرة التنصيف. اطّلع على الشيفرة.

def fast_pow(x, n):
    if n == 0: return 1
    if n < 0:  return 1 / fast_pow(x, -n)
    if n % 2 == 0:
        half = fast_pow(x, n // 2)
        return half * half          # O(log n) calls
    return x * fast_pow(x, n - 1)

print(fast_pow(2, 10))   # 1024
print(fast_pow(3, 5))    # 243
# Only log2(10)=3-4 recursive calls for n=10

اختبار سريع

اختبار سريع — لنعرف ما علّمتك إياه طريقة شجرة الاستدعاء الذاتي. سؤال واحد فقط، خذ وقتك. 🌳

مراجعة الدرس

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

الأسئلة الشائعة

هل درس «الاستدعاء الذاتي وطريقة شجرة الاستدعاء الذاتي» مجاني؟

نعم — نص درس «الاستدعاء الذاتي وطريقة شجرة الاستدعاء الذاتي» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «الاستدعاء الذاتي وطريقة شجرة الاستدعاء الذاتي»؟

تتبّع الاستدعاءات الذاتية ضمن أشجار، وطبّق مبرهنة Master، واستنتج التعقيدات الزمنية لفرز الدمج والعاملي ومتغيرات Fibonacci تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «الاستدعاء الذاتي وطريقة شجرة الاستدعاء الذاتي»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. ترميز Big-O من الصفر
  2. تحليل الحلقات والحلقات المتداخلة
  3. الاستدعاء الذاتي وطريقة شجرة الاستدعاء الذاتي
  4. تعقيد المساحة والمفاضلات
← العودة إلى Coding Interview Prep