مقدمة في علوم الحاسوب والبرمجة باستخدام Python › المحاضرة 23: أمثلة على أصناف التعقيد (Complexity Classes) 25 / 30 السابق التالي

المحاضرة 23: أمثلة على أصناف التعقيد (Complexity Classes)

المحاضرة 23: أمثلة على أصناف التعقيد (Complexity Classes)

المصادر والنسبة والترخيص

Ana Bell. 6.100L Introduction to CS and Programming using Python, Fall 2022. Massachusetts Institute of Technology: MIT OpenCourseWare, https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/. License: CC BY-NC-SA 4.0.

  • صفحة المحاضرة على MIT OpenCourseWare: https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/pages/lecture-23-complexity-classes-examples/
  • ملف الشرائح (صفحة الوصف): https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/resources/mit6_100l_f22_lec23_pdf/
  • ملف الشرائح (PDF مباشر): https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/mit6_100l_f22_lec23.pdf
  • ملف الشيفرة: https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/resources/mit6_100l_f22_lec23_code_py/
  • تفريغ المحاضرة على MIT OpenCourseWare (بالإنجليزية): https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/resources/mit6_100l_f22_lec23/
  • الترخيص: CC BY-NC-SA 4.0 — https://creativecommons.org/licenses/by-nc-sa/4.0/
  • شروط الاستخدام في MIT OpenCourseWare: https://ocw.mit.edu/terms/

منهج الترجمة: نُقلت هذه المحاضرة ترجمةً عربية كاملة أمينة لمحتوى ملف الشرائح الأصلي في MIT OpenCourseWare، تحت رخصة CC BY-NC-SA 4.0 التي تسمح بإعادة التوزيع مع الإبقاء على الإسناد والتأهيل والإشارة إلى التغيير، وبشرط أن يكون الاستخدام غير تجاري وأن تُوزَّع كل ترجمة مشتقّة تحت الرخصة نفسها. أُبقيت الشيفرة الخوارزمية والشيفرة الكاذبة (pseudocode) كما هي بالإنجليزية، ولم تُترجَم الأسماء والمكتبة ولا الكلمات المفتاحية في Python. حُوِّلت الأعمدة المتجاورة في الشرائح إلى جداول لأن نصّ الملف المستخرَج لا يحفظ مواضعها، ولكل شريحة من الشرائح التسع والستين عنوانها الخاص. لم تُضمَّن أيّ صور أو رسوم من الملف الأصلي لأن ترخيص بعض صورها مستقلّ عن رخصة MIT ولم يُتحقَّق منه.

ملاحظة المترجم: تحمل تذييل شرائح هذه المحاضرة سطرًا متبقّيًا من عرض أقدم (6.0001 LECTURE 8 / 6.0001 LECTURE 9) استُخدمت الشرائح فيه كقالب. هذا أثر موجود في ملف MIT الأصلي، وليس إشارة إلى مقرر آخر، وقد أُبقي على ذكره هنا للتوضيح.

الشريحة 1: أصناف التعقيد — أمثلة

هذه المحاضرة أمثلة على أصناف التعقيد (Complexity Classes). يُرفق ملف الشرائح وملفات .py للمتابعة الجانبيًا.

الشريحة 2: ثيتا (Θ)

  • نرمز إلى التعقيد الحتمي (asymptotic complexity) بالحرف ثيتا Θ.
  • ننظر إلى حدّ المدخل الذي يهيمن على الدالة.
  • نُسقط الأجزاء الأخرى التي لا تنمو بسرعة، ونُسقط الثوابت الجمعية، ونُسقط الثوابت الضربية.
  • ينتهي الأمر إلى عدد قليل من أصناف الخوارزميات.
  • سننظر اليوم إلى شيفرة تقع في كل واحد من هذه الأصناف.

الشريحة 3: من أين تأتي الدالة؟

بالنظر إلى الشيفرة، ابدأ بمعاملات الإدخال: ما هي؟ ثم صُغ المعادلة التي تربط الإدخال بعدد العمليات.

def f(L, L1, L2):
    inL1 = False
    for i in range(len(L1)):
        if L[i] == L1[i]:
            inL1 = True
    inL2 = False
    for i in range(len(L2)):
        if L[i] == L2[i]:
            inL2 = True
    return inL1 and inL2
f=1+len(L1)∗5+1+len(L2)∗5+2=5∗len(L1)+5∗len(L2)+3f = 1 + len(L1)*5 + 1 + len(L2)*5 + 2 = 5*len(L1) + 5*len(L2) + 3

إذا كان الطولان متساويين، فإن:

f=10∗len(L)+3f = 10*len(L) + 3 Θ(f)=Θ(10∗len(L)+3)=Θ(len(L))\Theta(f) = \Theta(10*len(L) + 3) = \Theta(len(L))

الشريحة 4: من أين تأتي الدالة؟ (طريقة أسرع)

لا حاجة لصياغة المعادلة الدقيقة. ابحث عن الحلقات وعن كل ما يتكرّر بدلالة معاملات الإدخال. وكل ما عداه ثابت.

نفس الدالة f(L, L1, L2) السابقة، والحلقتان for i in range(len(L1)) و for i in range(len(L2)) هما كل ما يتكرّر بدلالة الإدخال.

الشريحة 5: أصناف التعقيد

n هو المدخل. نريد تصميم خوارزميات تكون أقرب ما يمكن إلى قمة هذا التسلسل قدر الإمكان.

الرمز الدلالة
Θ(1)\Theta(1) زمن تشغيل ثابت (constant running time)
Θ(log⁡n)\Theta(\log n) زمن تشغيل لوغاريتمي (logarithmic)
Θ(n)\Theta(n) زمن تشغيل خطّي (linear)
Θ(nlog⁡n)\Theta(n \log n) زمن تشغيل خطّي لوغاريتمي (log-linear)
Θ(nc)\Theta(n^c) زمن تشغيل متعدّد الحدود (polynomial)، حيث cc ثابت
Θ(cn)\Theta(c^n) زمن تشغيل أُسّي (exponential)، حيث cc ثابت مرفوع إلى قوّة تعتمد على حجم الإدخال

ملاحظة المترجم: الشرائح الأصلية تكتب الحدّين الأخيرين بالإحراف مطبوعًا على هيئة Θ(nc)\Theta(nc) و Θ(cn)\Theta(cn)، لكن المقصود هو ncn^c و cnc^n، وقد ضبطتُهما هنا على الصورة الصحيحة لأن بقية الشرائح تستخدمهما بذلك.

الشريحة 6: التعقيد الثابت

شريحة فاصلة.

الشريحة 7: التعقيد الثابت

  • تعقيد مستقل عن المدخلات.
  • الخوارزميات المثيرة للاهتمام في هذا الصنف قليلة، لكن كثيرًا من الخوارزميات يمكن أن تحتوي أجزاءً تقع في هذا الصنف.
  • يمكن أن توجد حلقات أو استدعاءات ذاتية، لكن عدد التكرارات أو الاستدعاءات لا يعتمد على حجم المدخل.
  • بعض العمليات المدمجة في اللغة ثابتة الزمن:
    • الفهرسة في قائمة في Python: L[i]
    • الإلحاق في قائمة: L.append()
    • البحث في قاموس: d[key]

الشريحة 8: التعقيد الثابت: مثال ١

def add(x, y):
    return x+y

التعقيد بدلالة x أو بدلالة y: Θ(1)\Theta(1).

الشريحة 9: التعقيد الثابت: مثال ٢

def convert_to_km(m):
    return m*1.609

التعقيد بدلالة m: Θ(1)\Theta(1).

الشريحة 10: التعقيد الثابت: مثال ٣

def loop(x):
    y = 100
    total = 0
    for i in range(y):
        total += x
    return total

التعقيد بدلالة x (معامل الإدخال): Θ(1)\Theta(1)، لأن الحدّ الأقصى للتكرارات هو 100 أي عدد ثابت لا علاقة له بـ x.

الشريحة 11: التعقيد الخطّي

شريحة فاصلة.

الشريحة 12: التعقيد الخطّي

  • خوارزميات حلقية تكرارية بسيطة (simple iterative loop algorithms).
  • يجب أن تكون الحلقات دالةً في المدخل.
  • البحث الخطّي عن عنصر في قائمة (linear search): هل العنصر موجود أم لا.
  • دوال ذاتية (recursive functions) فيها استدعاء ذاتي واحد فقط وتكلفة ثابتة للاستدعاء نفسه.
  • بعض العمليات المدمجة خطّية:
    • e in L
    • شريحة من قائمة، مثل L[:len(L)//2]
    • L1 == L2
    • del(L[5])

الشريحة 13: مثال تعقيد صفر (بمفاجأة)

اضرب x في y.

def mul(x, y):
    tot = 0
    for i in range(y):
        tot += x
    return tot
  • التعقيد بدلالة y: Θ(y)\Theta(y).
  • التعقيد بدلالة x: Θ(1)\Theta(1).

الشريحة 14: الفكرة الكبرى

انتبه جيدًا إلى ما هي المدخلات.

الشريحة 15: التعقيد الخطّي: مثال ١

اجمع خانات نصّ، على افتراض أنه مكوَّن من خانات عشرية.

def add_digits(s):
    val = 0
    for c in s:
        val += int(c)
    return val
  • Θ(len(s))\Theta(len(s))
  • Θ(n)\Theta(n) حيث n=len(s)n = len(s)

الشريحة 16: التعقيد الخطّي: مثال ٢

حلقة لإيجاد مضروب عدد ≥2\ge 2 (factorial).

def fact_iter(n):
    prod = 1
    for i in range(2, n+1):
        prod *= i
    return prod
  • عدد مرات الدوران حول الحلقة هو n−1n-1.
  • عدد العمليات داخل الحلقة ثابت، ومستقل عن n.
  • الإجمالي Θ(n)\Theta(n) فقط.

الشريحة 17: مسألة طريفة حول المضروب وPython

  • المضروب ينمو في النهاية أسرع من النمو الخطّي.
  • السبب أن Python يزيد حجم الأعداد الصحيحة، مما يجعل العمليات أكثر كلفة.
  • في هذا الصف نُهمل هذه التأثيرات.

الشريحة 18: التعقيد الخطّي: مثال ٣

def fact_recur(n):
    """ assume n >= 0 """
    if n <= 1:
        return 1
    else:
        return n*fact_recur(n – 1)
  • يحسب المضروب بطريقة ذاتية.
  • إن قِست الزمن، ستلاحظ أنه أبطأ قليلًا من النسخة التكرارية بسبب استدعاءات الدوال.
  • Θ(n)\Theta(n) لأن عدد استدعاءات الدوال خطّي في n.
  • تنفيذا المضروب، التكراري والذاتي، لهما ترتيب النمو نفسه.

الشريحة 19: التعقيد الخطّي: مثال ٤

def compound(invest, interest, n_months):
    total=0
    for i in range(n_months):
        total = total * interest + invest Θ(1)
    return total
  • Θ(1)∗Θ(n_months)=Θ(n_months)\Theta(1) * \Theta(n\_months) = \Theta(n\_months)
  • Θ(n)\Theta(n) حيث n=n_monthsn = n\_months
  • Θ(n_months)\Theta(n\_months)
  • إن كنت دقيقًا إلى حدّ كبير، فلا بد من احتساب جملتي الإسناد والإرجاع: Θ(1)+4∗Θ(n)+Θ(1)=Θ(1+4∗n+1)=Θ(n)\Theta(1) + 4*\Theta(n) + \Theta(1) = \Theta(1 + 4*n + 1) = \Theta(n) حيث n=n_monthsn = n\_months.

ملاحظة المترجم: وسم Θ(1)\Theta(1) الظاهر في سطر جسم الحلقة هو تعليق تربوي كتبه MIT الأصلي على حاشية السطر نفسه؛ أُبقي كما هو لأنه جزء من الشريحة.

الشريحة 20: تعقيد فيبوناتشي التكراري

def fib_iter(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        fib_i = 0
        fib_ii = 1
        for i in range(n-1):
            tmp = fib_i
            fib_i = fib_ii
            fib_ii = tmp + fib_ii
        return fib_ii

الحساب: Θ(1)+Θ(1)+Θ(n)∗Θ(1)+Θ(1)=Θ(n)\Theta(1) + \Theta(1) + \Theta(n) * \Theta(1) + \Theta(1) = \Theta(n)

الشريحة 21: التعقيد متعدّد الحدود (Polynomial)

شريحة فاصلة.

الشريحة 22: التعقيد متعدّد الحدود (غالبًا تربيعي)

  • أكثر خوارزميات متعدّدة الحدود شيوعًا هي التربيعية، أي أن التعقيد ينمو مع مربّع حجم المدخل.
  • يظهر عادةً عندما تكون لدينا حلقات متداخلة (nested loops) أو استدعاءات ذاتية لدوال.

الشريحة 23: التعقيد التربيعي: مثال ١

def g(n):
    """ assume n >= 0 """
    x = 0
    for i in range(n):
        for j in range(n):
            x += 1
    return x
  • يحسب n2n^2 بطريقة غير فعّالة جدًّا.
  • انظر إلى الحلقات: هل هي بدلالة المُدخل؟
  • حلقات متداخلة.
  • انظر إلى المدى: range(n) لكل منهما.
  • كل حلقة تتكرّر nn مرّات.
  • Θ(n)∗Θ(n)∗Θ(1)=Θ(n2)\Theta(n) * \Theta(n) * \Theta(1) = \Theta(n^2)

الشريحة 24: التعقيد التربيعي: مثال ٢

هل L1 مجموعة جزئية (subset) من L2؟ أي: هل كل عناصر L1 موجودة في L2؟

def is_subset(L1, L2):
    for e1 in L1:
        matched = False
        for e2 in L2:
            if e1 == e2:
                matched = True
                break
        if not matched:
            return False
    return True
الحالة L1 L2 النتيجة
نعم [3, 5, 2] [2, 3, 5, 9] كل عناصر L1 في L2
لا [3, 5, 2] [2, 5, 9] العنصر 3 غير موجود في L2

الشريحة 25: التعقيد التربيعي: مثال ٢ (التحليل)

نفس الدالة is_subset، مع الشرح:

  • الحلقة الخارجية تُنفَّذ len(L1) مرّة.
  • كل تكرار لها يُنفّذ الحلقة الداخلية حتى len(L2) مرّة.
  • Θ(len(L1)∗len(L2))\Theta(len(L1) * len(L2))
  • إذا كان L1 و L2 بالطول نفسه، ولم يكن أيّ عنصر من L1 في L2: Θ(len(L1)2)\Theta(len(L1)^2)

الشريحة 26: التعقيد التربيعي: مثال ٣

ابحث عن تقاطع قائمتين، وأعِد قائمة فيها كل عنصر يظهر مرّة واحدة فقط.

def intersect(L1, L2):
    tmp = []
    for e1 in L1:
        for e2 in L2:
            if e1 == e2:
                tmp.append(e1)
    unique = []
    for e in tmp:
        if not(e in unique):
            unique.append(e)
    return unique
L1 L2 النتيجة
[3, 5, 2] [2, 3, 5, 9] [2, 3, 5]
[7, 7, 7] [7, 7, 7] [7]

الشريحة 27: التعقيد التربيعي: مثال ٣ (التحليل)

  • الحلقة المتداخلة الأولى تأخذ Θ(len(L1)∗len(L2))\Theta(len(L1) * len(L2)) خطوة.
  • الحلقة الثانية تأخذ على الأكثر Θ(len(L1)∗len(L2))\Theta(len(L1) * len(L2)) خطوة. وهي عادةً ليست بالسوءة هذه.
    • مثال: [7,7,7] و [7,7,7] ينتج tmp=[7,7,7,7,7,7,7,7,7].
  • الإجمالي Θ(len(L1)∗len(L2))\Theta(len(L1) * len(L2))

الشريحة 28: تعقيد القطر (diameter)

def diameter(L):
    farthest_dist = 0
    for i in range(len(L)):
        for j in range(i+1, len(L)):
            p1 = L[i]
            p2 = L[j]
            dist = math.sqrt( (p1[0]-p2[0])**2 + (p1[1]-p2[1])**2 )
            if dist > farthest_dist:
                farthest_dist = dist
    return farthest_dist

عدد التكرارات: len(L)∗len(L)/2=len(L)2/2len(L) * len(L)/2 = len(L)^2 / 2، أي Θ(len(L)2)\Theta(len(L)^2).

الشريحة 29: جرّب بنفسك (YOU TRY IT!)

def all_digits(nums):
    """ nums is a list of numbers """
    digits = [0,1,2,3,4,5,6,7,8,9]
    for i in nums:
        isin = False
        for j in digits:
            if i == j:
                isin = True
                break
        if not isin:
            return False
    return True

الإجابة: ما المدخل؟ الحلقة الخارجية Θ(nums)\Theta(nums)، والحلقة الداخلية Θ(1)\Theta(1) لأن digits دائمًا عشرة خانات. الإجمالي: Θ(len(nums))\Theta(len(nums))

الشريحة 30: جرّب بنفسك (YOU TRY IT!)

ما التعقيد الحتمي (asymptotic complexity) للدالة f التالية؟ وماذا لو كانت L1 و L2 و L3 بالطول نفسه؟

def f(L1, L2, L3):
    for e1 in L1:
        for e2 in L2:
            if e1 in L3 and e2 in L3 :
                return True
    return False

الإجابة: Θ(len(L1))∗Θ(len(L2))∗Θ(len(L3)+len(L3))\Theta(len(L1)) * \Theta(len(L2)) * \Theta(len(L3) + len(L3)) الإجمالي: Θ(len(L1)∗len(L2)∗len(L3))\Theta(len(L1) * len(L2) * len(L3)) والإجمالي إذا كانت القوائم متساوية الطول: Θ(len(L1)3)\Theta(len(L1)^3)

الشريحة 31: التعقيد الأُسّي (Exponential)

شريحة فاصلة.

الشريحة 32: التعقيد الأُسّي

  • دوال ذاتية فيها أكثر من استدعاء ذاتي واحد لكل حجم من أحجام المسألة.
    • فيبوناتشي.
  • كثير من المسائل المهمة هي في جوهرها أُسّية.
  • هذا أمر محزن، لأن الكلفة قد تكون عالية.
  • سيقودنا هذا إلى التفكير في الحلول التقريبية (approximate solutions) بسرعة أكبر.
التعبير القراءة
2302^{30} ≈\approx مليون
21002^{100} أكثر عدد دورات في حاسب than كل الحواسيب في العالم، تعمل طوال التاريخ المسجَّل، يمكن أن تُنجزه

الشريحة 33: تعقيد فيبوناتشي الذاتي

def fib_recur(n):
    """ assumes n an int >= 0 """
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib_recur(n-1) + fib_recur(n-2)

الحالة الأسوأ: Θ(2n)\Theta(2^n)

الشريحة 34: تعقيد فيبوناتشي الذاتي (شجرة الاستدعاء)

استدعاء Fib(6) يولّد الشجرة التالية (تُقرأ من الجذر إلى الأور):

Fib(6)
├── Fib(5)
│   ├── Fib(4)
│   │   ├── Fib(3)
│   │   │   ├── Fib(2)
│   │   │   └── Fib(1)
│   │   └── Fib(2)
│   └── Fib(3)
│       ├── Fib(2)
│       └── Fib(1)
└── Fib(4)
    ├── Fib(3)
    │   ├── Fib(2)
    │   └── Fib(1)
    └── Fib(2)
  • يمكننا أن نفعل أفضل قليلًا من 2n2^n لأن الشجرة تخفّ باتجاه اليمين.
  • لكن التعقيد يبقى من الرتبة الأُسّية.

ملاحظة المترجم: رُسمت الشجرة هنا في نصّي بصيغة شجرة نصّية؛ بينما الأصل يرسمها بخطوط متّصلة لا يمكن لطبقة النصّ المستخرَجة أن تحفظ مواضعها.

الشريحة 35: التعقيد الأُسّي: توليد المجموعات الفرعية

المُدخل هو [1, 2, 3]، والمُخرج هو كل توليفات العناصر بكل الأطوال:

[[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]]
def gen_subsets(L):
    if len(L) == 0:
        return [[]]
    extra = L[-1:]
    smaller = gen_subsets(L[:-1])
    new = []
    for small in smaller:
        new.append(small+extra)
    return smaller+new

الشريحة 36: تمثيل الخوارزمية — الخطوة الأولى

نفس الدالة gen_subsets، ومعها بداية تمثيل الشجرة:

المستوى القيمة
الجذر [1,2,3]
ما بعده [1,2]، ثم [1]، ثم []

الحالة الأساسية: عندما تكون L فارغة، الدالة تُعيد [[]] — أي القائمة الفارغة هي المجموعة الفرعية الوحيدة.

الشريحة 37: تمثيل الخوارزمية — ما بعد الحالة الأساسية

المستوى القيمة
الجذر [1,2,3]
[1,2]، [1]، []
ابن [] [[]]

الشريحة 38: تمثيل الخوارزمية — بعد أول استدعاء ذاتي مكتمل

المستوى القيمة
الجذر [1,2,3]
ابن [1,2] [[],[1]]
ابن [1] [1]
ابن [] [[]]
الحفيد []

ملاحظة المترجم: التقسيم إلى "أبناء" و"أحقاد" استعملتُ فيه التسلسل الرقمي 2 و3 تمييزًا بين المستويات في الرسم الأصلي؛ الأرقام نفسها ليست في نصّ MIT المكتوب، والمقصود هو عمق الشجرة.

الشريحة 39: تمثيل الخوارزمية — الجانب الأيسر مكتمل

المستوى القيمة
الجذر [1,2,3]
ابن [1,2] [[],[1],[2],[1,2]]
ابن [1] [[],[1]]
ابن [] [[]]
الحفيد []

الشريحة 40: تمثيل الخوارزمية — اكتمال الشجرة

المستوى القيمة
الجذر [1,2,3]
ابن [1,2] [[],[1],[2],[1,2]، ثم [3]، ثم [1,3]، [2,3]، [1,2,3]]
ابن [1] [[],[1]]
ابن [] [[]]

المصفوفة الكاملة الناتجة في الأعلى هي:

[[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]]

الشريحة 41: تمثيل الخوارزمية — الشكل النهائي للشجرة

نفس الشجرة كما في الشريحة السابقة، بعد إعادة ترتيب فروعها في الرسم:

المستوى القيمة
الجذر [1,2,3]
[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
ابن [1,2] [[],[1]]
ابن [1] [[]]
ابن [] []

ملاحظة المترجم: الأرقام التسلسلية الظاهرة في الرسم الأصلي بين الشريحتين لا يغطّيها النصّ المستخرَج، لذلك لم أنسبها إلى أيّ فرع بعينه.

الشريحة 42: التعقيد الأُسّي — توليد المجموعات الفرعية

نفس الدالة gen_subsets، مع الملاحظتين:

  • نفترض أن append ثابتة الزمن.
  • الزمن اللازم لصنع القوائم الفرعية يشمل الزمن اللازم لحل المسألة الأصغر، والزمن اللازم لصنع نسخة من كل عناصر المسألة الأصغر.

الشريحة 43: التعقيد الأُسّي — توليد المجموعات الفرعية (الحساب)

  • فكّر في حجم smaller.
  • لمجموعة حجمها kk هناك 2k2^k حالة، أي أن الحجم يتضاعف عند كل استدعاء.
  • إذن للحل نحتاج 2n−1+2n−2+⋯+202^{n-1} + 2^{n-2} + \dots + 2^0 خطوة، أي Θ(2n)\Theta(2^n).
  • الزمن اللازم لصنع نسخة من smaller: الدمج (concatenation) ليس ثابتًا، بل Θ(n)\Theta(n).
  • التعقيد الإجمالي هو Θ(n∗2n)\Theta(n * 2^n) حيث n=len(L)n = len(L).

الشريحة 44: التعقيد اللوغاريتمي

شريحة فاصلة.

الشريحة 45: تعقيد صعب

def digit_add(n):
    """ assume n an int >= 0 """
    answer = 0
    s = str(n)
    for c in s[::-1]:
        answer += int(c)
    return answer
  • يجمع خانات العدد معًا.
  • n = 83 لكن الحلقة تتكرّر مرّتين فقط. ما العلاقة؟
  • n = 4271 لكن الحلقة تتكرّر أربع مرّات فقط! ما العلاقة؟؟

في الشريحة الأولى ظهرت خانة 1 (آخر خانة في 4271) بعد أن حُلَّت 427:

4271

الشريحة 46: تعقيد صعب

نفس الدالة ونفس السؤالين. ظهرت الخانة 7 بعد أن حُلَّ 427 بـ 21 المتبقّي:

42 7 1

الشريحة 47: تعقيد صعب

ظهرت الخانة 2 بعد أن حُلَّ 42 بـ 1 المتبقّي:

4 2 7 1

الشريحة 48: تعقيد صعب

ظهرت الخانة 4 أخيرًا، فاكتمل العدد:

4 2 7 1

ملاحظة المترجم: الشرائح 45–48 ترسم إزالة خانة من يسار 4271 خطوةً خطوة مع الحلقة (1، ثم 7، ثم 2، ثم 4). أرقام الصفوف في النصّ المستخرَج هي بقايا الرسم لا عناصر مستقلّة، ولهذا رُبطت هنا بترتيب إزالة الخانات الذي يوضّحه الرسم.

الشريحة 49: تعقيد صعب (الحل)

نفس الدالة digit_add.

  • الجزء الصعب: التكرار يتم على طول النصّ، لا على مقدار n نفسه.
    • فكّر فيه كأنك تقسم n على 10 في كل تكرار.
    • n/10len(s)=1n/10^{len(s)} = 1، أي: نقسم على 10 حتى يبقى عنصر واحد يُضاف.
    • len(s)=log⁡(n)len(s) = \log(n)
  • Θ(log⁡n)\Theta(\log n) — الأساس لا يهمّ.

الشريحة 50: التعقيد اللوغاريتمي

  • التعقيد ينمو بمقدار لوغاريتم حجم أحد مُدخلاته.
  • مثال خوارزمية: البحث الثنائي (binary search) في قائمة.
  • مثال سنراه بعد عدّة شرائح: إحدى تنفيذات البحث بالتنصيف (bisection search).

الشريحة 51: القوائم والقواميس

يجب أن نكون حذرين عند استخدام الدوال المدمجة!

القوائم — حيث n هو len(L):

العملية التعقيد
الفهرسة L[i] Θ(1)\Theta(1)
التخزين Θ(1)\Theta(1)
الطول len(L) Θ(1)\Theta(1)
append Θ(1)\Theta(1)
== Θ(n)\Theta(n)
remove Θ(n)\Theta(n)
copy Θ(n)\Theta(n)
reverse Θ(n)\Theta(n)
التكرار Θ(n)\Theta(n)
e in L Θ(n)\Theta(n)

القواميس — حيث n هو len(d):

العملية التعقيد
الفهرسة Θ(1)\Theta(1)
التخزين Θ(1)\Theta(1)
الطول len(d) Θ(1)\Theta(1)
الحذف Θ(1)\Theta(1)
.keys Θ(n)\Theta(n)
.values Θ(n)\Theta(n)
التكرار Θ(n)\Theta(n)

الشريحة 52: خوارزميات البحث

شريحة فاصلة.

الشريحة 53: خوارزميات البحث

البحث الخطّي (linear search):

  • بحث بالقوة الغاشمة (brute force).
  • لا يلزم أن تكون القائمة مرتّبة.

البحث بالتنصيف (bisection search):

  • يجب أن تكون القائمة مرتّبة (MUST) حتى تعطي إجابة صحيحة.
  • سنرى تنفيذين مختلفين للخوارزمية.

الشريحة 54: البحث الخطّي على قائمة غير مرتّبة

def linear_search(L, e):
    found = False
    for i in range(len(L)):
        if e == L[i]:
            found = True
    return found
  • يجب أن ننظر في كل العناصر لنقرّر أنه غير موجود.
  • Θ(len(L))\Theta(len(L)) للحلقة، مضروبة في Θ(1)\Theta(1) لاختبار e == L[i].
  • التعقيد الإجمالي Θ(n)\Theta(n) حيث n=len(L)n = len(L)، أي Θ(len(L))\Theta(len(L)).

الشريحة 55: البحث الخطّي على قائمة غير مرتّبة (نسخة ثانية)

def linear_search(L, e):
    for i in L:
        if e == L[i]:
            return True
    return False
  • نفس التحليل السابق.
  • Θ(len(L))\Theta(len(L)) للحلقة، مضروبة في Θ(1)\Theta(1) لاختبار e == L[i].
  • التعقيد الإجمالي Θ(n)\Theta(n) حيث n=len(L)n = len(L)، أي Θ(len(L))\Theta(len(L)).

ملاحظة المترجم: الشريحة تحمل النصّ الدقيق للدالة linear_search وعلى يسارها نسختان متتاليتان منها (تُقرأان على أنها found = False ثم for i in L: مع return True/return False). النسخة الأولى تُعيد found بعد الحلقة، والثانية تُعيد True/False مباشرة. تُركت الاثنتان كما تظهران في الشريحة.

الشريحة 56: البحث الخطّي على قائمة مرتّبة

def search(L, e):
    for i in L:
        if i == e:
            return True
        if i > e:
            return False
    return False
  • يجب أن ننظر فقط حتى نصل إلى عدد أكبر من e.
  • Θ(len(L))\Theta(len(L)) للحلقة، مضروبة في Θ(1)\Theta(1) لاختبار i == e أو i > e.
  • التعقيد الإجمالي Θ(len(L))\Theta(len(L))، أي Θ(n)\Theta(n) حيث n=len(L)n = len(L).

الشريحة 57: البحث بالتنصيف عن عنصر في قائمة مرتّبة

  1. اختر فهرسًا i يقسم القائمة إلى نصفين.
  2. اسأل: هل L[i] == e؟
  3. إن لم يكن كذلك، اسأل: هل L[i] أكبر أم أصغر من e؟
  4. حسب الإجابة، ابحث في النصف الأيسر أو الأيمن من L عن e.
  • هذه نسخة جديدة من «اقسم وانتصر» (divide-and-conquer): إذًا الاستدعاء الذاتي (recursion)!
  • نقسم المشكلة إلى نسخ أصغر منها (قائمة أصغر)،إضافةً إلى عمليات بسيطة.
  • الإجابة عن النسخة الأصغر هي الإجابة عن النسخة الأصلية.

الشريحة 58: تحليل تعقيد البحث بالتنصيف

  • ننتهي من النظر في القائمة عندما:
1=n/2i1 = n/2^i
  • إذن ما العلاقة بين الطول الأصلي للقائمة وعدد مرّات تقسيمها؟
i=log⁡ni = \log n
  • التعقيد هو Θ(log⁡n)\Theta(\log n) حيث nn هو len(L).

ملاحظة المترجم: الشريحة الأصلية تمثّل المساواة 1 = n/2^i بخطّ مستقيم على محور لوغاريتمي بحوارَين «…»، وهذه الأشكال لا تنجو من طبقة النصّ؛ أُعيدت المعادلة بصيغتها الرياضية الصحيحة.

الشريحة 59: الفكرة الكبرى

تنفيذان مختلفان لهما قيمتا Θ\Theta مختلفتان.

الشريحة 60: البحث بالتنصيف — التنفيذ ١

def bisect_search1(L, e):
    if L == []:
        return False
    elif len(L) == 1:
        return L[0] == e
    else:
        half = len(L)//2
        if L[half] > e:
            return bisect_search1( L[:half], e)
        else:
            return bisect_search1( L[half:], e)

الشريحة 61: تعقيد bisect_search1 (حيث n هو len(L))

  • Θ(log⁡n)\Theta(\log n) استدعاءات بحث بالتنصيف.
  • كل استدعاء ذاتي يقتطع المدى المطلوب البحث فيه إلى النصف. أسوأ حالة للوصول إلى مدى حجمه 1 من n هي عندما n/2k=1n/2^k = 1، أي عندما k=log⁡nk = \log n، وبهذا نحصل على تعبير يربط k بـ n.
  • Θ(n)\Theta(n) لكل استدعاء بحث للتنصيف بسبب نسخ القائمة.
  • Θ(log⁡n)∗Θ(n)=Θ(nlog⁡n)\Theta(\log n) * \Theta(n) = \Theta(n \log n) حيث n=len(L)n = len(L).
    • هذه هي الإجابة في هذا الصف.
  • إن كنت دقيقًا، ستلاحظ أن القائمة أيضًا تُنصَّف عند كل استدعاء ذاتي، فتصير هنالك سلسلة لا نهائية (لا داعي للقلق بشأنها في هذا الصف).
  • Θ(n)\Theta(n) هي حدّ أدقّ (tighter bound) لأن نسخ القائمة يهيمن على log⁡n\log n.

الشريحة 62: تنفيذ بديل للبحث بالتنصيف

  • صغّر حجم المسألة بمعامل 2 في كل خطوة.
  • احتفظ بفهرسي low و high للبحث في القائمة.
  • تجنّب نسخ القائمة.
  • تعقيد الاستدعاء الذاتي هو Θ(log⁡n)\Theta(\log n) حيث nn هو len(L).

الشريحة 63: البحث بالتنصيف — التنفيذ ٢

def bisect_search2(L, e):
    def bisect_search_helper(L, e, low, high):
        if high == low:
            return L[low] == e
        mid = (low + high)//2
        if L[mid] == e:
            return True
        elif L[mid] > e:
            if low == mid: #nothing left to search
                return False
            else:
                return bisect_search_helper(L, e, low, mid - 1)
        else:
            return bisect_search_helper(L, e, mid + 1, high)
    if len(L) == 0:
        return False
    else:
        return bisect_search_helper(L, e, 0, len(L) - 1)

الشريحة 64: تعقيد bisect_search2 ودالة المساعِد (حيث n هو len(L))

  • Θ(log⁡n)\Theta(\log n) استدعاءات بحث بالتنصيف.
  • كل استدعاء ذاتي يقتطع المدى المطلوب البحث فيه إلى النصف. أسوأ حالة للوصول إلى مدى حجمه 1 من n هي عندما n/2k=1n/2^k = 1، أي عندما k=log⁡nk = \log n.
  • نمرّر القائمة والفهارس كمعاملات. القائمة لا تُنسخ أبدًا، بل تُمرَّر فقط: Θ(1)\Theta(1) عند كل استدعاء ذاتي.
  • Θ(log⁡n)∗Θ(1)=Θ(log⁡n)\Theta(\log n) * \Theta(1) = \Theta(\log n) حيث n=len(L)n = len(L).

الشريحة 65: متى نرتب أولًا ثم نبحث؟

شريحة فاصلة.

الشريحة 66: البحث في قائمة مرتّبة — حيث n هو len(L)

  • باستخدام البحث الخطّي، البحث عن عنصر هو Θ(n)\Theta(n).
  • باستخدام البحث الثنائي، يمكننا البحث عن عنصر في Θ(log⁡n)\Theta(\log n) — بافتراض أن القائمة مرتّبة!
  • متى يكون من المعقول أن نرتب أولًا ثم نبحث؟
SORT+Θ(log⁡n)<Θ(n)⇒SORT<Θ(n)−Θ(log⁡n)SORT + \Theta(\log n) < \Theta(n) \Rightarrow SORT < \Theta(n) - \Theta(\log n)
  • متى يكون الترتيب أصغر من Θ(n)\Theta(n)؟!
    • أبدًا ما دام أنك على الأقل ستحتاج إلى النظر في كل عنصر!

الشريحة 67: التكلفة المُطفَّرة (Amortized Cost) — حيث n هو len(L)

  • لماذا نرتّب أولًا إذن؟
  • رتّب القائمة مرّة واحدة ثم أجرِ عمليات بحث كثيرة.
  • نُطفئ (amortize) كلفة الترتيب على عمليات البحث الكثيرة.
  • SORT+K∗Θ(log⁡n)<K∗Θ(n)SORT + K * \Theta(\log n) < K * \Theta(n)، أي إن كان K كبيرًا فإن زمن الترتيب يصبح غير ذي أثر.

ملاحظة المترجم: ظهرت حروف دخيلة عند استخراج جملة «رتّب القائمة مرّة واحدة ثم …» من ملف PDF الأصلي، وأصلحتها هنا إلى «كثير من المرّات».

الشريحة 68: خلاصة أصناف التعقيد

  • قارن كفاءة الخوارزميات.
  • رتّب حسب رتبة النمو.
  • استخدم Θ\Theta لحدّ أعلى وحدّ أدنى، أي حدّ «مضبوط» (tight).
  • الخيارات المعطاة لدالة f:

انظر إلى ما يتكرّر:

  • انظر إلى الحلقات:
    • هل هي بدلالة مُدخل f؟
    • هل هناك حلقات متداخلة؟
  • انظر إلى الاستدعاءات الذاتية:
    • إلى أي عمق يصل مكدّس استدعاءات الدوال؟
  • انظر إلى الدوال المدمجة:
    • هل أيّ منها يعتمد على المدخل؟

الشريحة 69: MIT OpenCourseWare

MITOpenCourseWare
https://ocw.mit.edu

6.100L Introduction to Computer Science and Programming Using Python
Fall 2022

For information about citing these materials or our Terms of Use, visit: https://ocw.mit.edu/terms.