مقدمة في علوم الحاسوب والبرمجة باستخدام Python › المحاضرة 22: الصنف O الكبير (Big Oh) والصنف ثيتا (Theta) 24 / 30 السابق التالي

المحاضرة 22: الصنف O الكبير (Big Oh) والصنف ثيتا (Theta)

المحاضرة 22: الصنف O الكبير (Big Oh) والصنف ثيتا (Theta)

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

هذه ترجمة عربية لمادة مقرّرة من MIT OpenCourseWare:

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.

منهج الترجمة: عنوان واحد وترجمة عربية لكل شريحة من شرائح الملف الأصلي (60 شريحة). الرياضيات نُقلت بصيغة LaTeX داخل $$…$$ و$…$ كما هي. جداول النتائج والرسوم البيانية نُقلت إلى جداول Markdown. الشيفرة وعناوين الوثائق (docstrings) تُركت بالإنجليزية كما هي، والتعليقات التي كانت في الأصل قد تُرجمت. الرسوم البيانية (الرسوم المنحننية لمنحنى النمو، ومخططات التسلسل الهرمي لأصناف التعقيد) لم تُضمَّن، ونُقلت ما يمكن نقله منها نصًّا.

الشريحة 1: عنوان المحاضرة

  • BIG OH and THETA
  • (download slides and .py files to follow along)
  • 6.100L Lecture 22 — Ana Bell

الشريحة 2: قياس الزمن (Timing)

الشريحة 3: قياس زمن برنامج

  • استخدم وحدة time
  • الاستيراد (importing) يعني إدخال مجموعة دوال إلى ملفك
  • شغّل الساعة
  • انادي الدالة
  • أوقف الساعة
import time

def convert_to_km(m):
    return m * 1.609

t0 = time.perf_counter()
convert_to_km(100000)
dt = time.perf_counter() - t0
print("t =", dt, "s,")

ملاحظة المترجم: تذييل هذه الشريحةوأخرى شرائح هذه المحاضرة يحمل في الملف الأصلي سطرين متداخلين «6.0001 LECTURE 8» و«6.100L LECTURE 22»، أي أن الملف مُعدّ من شريحة قديمة لمقرر 6.0001 وأُضيفت إليه شريحة العنوان؛ نُقل التذييل الحرفي كما هو لأنه جزء من الملف الأصلي.

الشريحة 4: مثال: convert_to_km مركّبة (compound)

def convert_to_km(m):
    return m * 1.609

def compound(invest, interest, n_months):
    total=0
    for i in range(n_months):
        total = total * interest + invest
    return total
  • كم يستغرق حساب هذه الدوال من وقت؟
  • هل يعتمد الزمن على معاملات الإدخال؟
  • هل تختلف الأزمنة بشكل ملحوظ بين هاتين الدالتين؟

الشريحة 5: إنشاء قائمة إدخال

L_N = [1]
for i in range(7):
    L_N.append(L_N[-1]*10)

for N in L_N:
    t = time.perf_counter()
    km = convert_to_km(N)
    dt = time.perf_counter()-t
    print(f"convert_to_km({N}) took {dt} seconds ({1/dt}/sec)")

الشريحة 6: شغّلها! — ملاحظات على convert_to_km

  • الملاحظة: يبدو أن الزمن المتوسط مستقل عن حجم المُعامل

الشريحة 7: قياس الزمن: compound بعدد متغيّر من الأشهر

def compound(invest, interest, n_months):
    total=0
    for i in range(n_months):
        total = total * interest + invest
    return total

النتائج المقيسة:

نداء الدالة الزمن (ثانية) عدد العمليات في الثانية
compound(1) 2.26e-06 441,696.12
compound(10) 2.31e-06 433,839.48
compound(100) 6.59e-06 151,676.02
compound(1000) 5.02e-05 19,938.59
compound(10000) 5.10e-04 1,961.80
compound(100000) 5.14e-03 194.46
compound(1000000) 4.79e-02 20.86
compound(10000000) 4.46e-01 2.24
  • الملاحظة 1: ينمو الزمن مع المدخل فقط عندما تتغيّر n_months
  • الملاحظة 2: يبدو أن الزمن المتوسط يزداد بمقدار 10 عندما يزداد حجم المُعامل بمقدار 10
  • الملاحظة 3: العلاقة بين الحجم والزمن متوقَّعة فقط عند الأحجام الكبيرة

الشريحة 8: قياس الزمن: المجموع على قائمة L

def sum_of(L):
    total = 0.0
    for elt in L:
        total = total + elt
    return total
  • الملاحظة 1: صار حجم المدخل هو طول القائمة، لا حجم أرقام عناصرها.
  • الملاحظة 2: يبدو أن الزمن المتوسط يزداد بمقدار 10 عندما يزداد حجم المُعامل بمقدار 10
  • الملاحظة 3: العلاقة بين الحجم والزمن متوقَّعة فقط عند الأحجام الكبيرة
L_N = [1]
for i in range(7):
    L_N.append(L_N[-1]*10)
  • الملاحظة 4: يبدو أن الزمن يكافئ زمن حساب compound
for N in L_N:
    L = list(range(N))
    t = time.perf_counter()
    s = sum_of(L)
    dt = time.perf_counter()-t
    print(f"sum_of({N}) took {dt} seconds ({1/dt}/sec)")

الشريحة 9: قياس الزمن: البحث عن عنصر في قائمة

# search each element one-by-one
def is_in(L, x):
    for elt in L:
        if elt==x:
            return True
    return False

# search by bisecting the list (list should be sorted!)
def binary_search(L, x):
    lo = 0
    hi = len(L)
    while hi-lo > 1:
        mid = (hi+lo) // 2
        if L[mid] <= x:
            lo = mid
        else:
            hi = mid
    return L[lo] == x

# search using built-in operator
x in L

الشريحة 10: قياس الزمن: البحث عن عنصر في قائمة — النتائج

الطريقة الحجم الزمن (ثانية) (1/ث) النسبة مقارنةً بعُشر الحجم
is_in 10000000 1.62e-01 6.16/sec 9.57 أضعاف عند عدد عناصر أقل بعشرة أضعاف
binary 10000000 9.37e-06 106,761.64/sec 1.40 أضعاف عند عدد عناصر أقل بعشرة أضعاف
builtin 10000000 5.64e-02 17.72/sec 9.63 أضعاف عند عدد عناصر أقل بعشرة أضعاف
is_in 100000000 1.64e+00 0.61/sec 10.12 أضعاف عند عدد عناصر أقل بعشرة أضعاف
binary 100000000 1.18e-05 84,507.09/sec 1.26 أضعاف عند عدد عناصر أقل بعشرة أضعاف
builtin 100000000 5.70e-01 1.75/sec 10.11 أضعاف عند عدد عناصر أقل بعشرة أضعاف
  • الملاحظة 1: البحث عنصرًا عنصرًا ينمو بمعامل 10 عندما تزيد L بمعامل 10

الشريحة 11: قياس الزمن: البحث عن عنصر في قائمة (تتمة)

  • نفس جدول الشريحة 10.
  • الملاحظة 1: البحث عنصرًا عنصرًا ينمو بمعامل 10 عندما تزيد L بمعامل 10
  • الملاحظة 2: الدالة المدمجة (built-in) تنمو بمعامل 10 عندما تزيد L بمعامل 10

الشريحة 12: قياس الزمن: البحث عن عنصر في قائمة (تتمة)

  • نفس جدول الشريحة 10.
  • الملاحظة 1: البحث عنصرًا عنصرًا ينمو بمعامل 10 عندما تزيد L بمعامل 10
  • الملاحظة 2: الدالة المدمجة (built-in) تنمو بمعامل 10 عندما تزيد L بمعامل 10
  • الملاحظة 3: يبدو أن زمن البحث بالتنصيف شبه مستقل عن الحجم

الشريحة 13: قياس الزمن: البحث عن عنصر في قائمة (تتمة)

  • نفس جدول الشريحة 10.
  • الملاحظة 1: البحث عنصرًا عنصرًا ينمو بمعامل 10 عندما تزيد L بمعامل 10
  • الملاحظة 2: الدالة المدمجة (built-in) تنمو بمعامل 10 عندما تزيد L بمعامل 10
  • الملاحظة 3: يبدو أن زمن البحث بالتنصيف شبه مستقل عن الحجم
  • الملاحظة 4: البحث بالتنصيف أسرع بكثير من is_in، خاصة في المسائل الأكبر

الشريحة 14: قياس الزمن: البحث عن عنصر في قائمة (تتمة)

  • نفس جدول الشريحة 10.
  • الملاحظة 1: البحث عنصرًا عنصرًا ينمو بمعامل 10 عندما تزيد L بمعامل 10
  • الملاحظة 2: الدالة المدمجة (built-in) تنمو بمعامل 10 عندما تزيد L بمعامل 10
  • الملاحظة 3: يبدو أن زمن البحث بالتنصيف شبه مستقل عن الحجم
  • الملاحظة 4: البحث بالتنصيف أسرع بكثير من is_in، خاصة في المسائل الأكبر
  • الملاحظة 5: is_in أبطئ قليلًا من استخدامقدرة بايثون «in»

الشريحة 15: قياس الزمن: ماذا رأينا حتى الآن؟

def is_in(L, x):
    for elt in L:
        if elt==x:
            return True
    return False

فحنا رأينا حسابات يتغيّر فيها الزمن اختلافًا كبيرًا جدًا:

  • زمن ثابت (Constant time)
  • خطي في حجم المُعامل (Linear in size of argument)
  • شيء أقل من خطي؟ (Something less than linear?)
def binary_search(L, x):
    lo = 0
    hi = len(L)
    while hi-lo > 1:
        mid = (hi+lo) // 2
        if L[mid] <= x:
            lo = mid
        else:
            hi = mid
    return L[lo] == x

الشريحة 16: قياس الزمن: دالة القطر (diameter)

L=[(cos(0), sin(0)),
   (cos(1), sin(1)),
   (cos(2), sin(2)), ... ] #example numbers

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

الشريحة 17: قياس الزمن: دالة القطر (تتمة)

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

ملاحظة المترجم: الشرائح 17–22 تعرض الدالة نفسها بلا إضافات نصّية؛ في الأصل مؤشَّر على سطر الحلقة الداخلي for j in range(i+1, len(L)) وعلى if dist > farthest_dist: (باللون المميّز في العرض) لإبراز الحلقة المتداخلة والشرط الذي يحدّث أبعد مسافة. الروابط والأسهم اللونية لا تظهر في طبقة النص، لذلك نُقلت الدالة كاملة كما هي.

الشريحة 18: قياس الزمن: دالة القطر (تتمة)

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

الشريحة 19: قياس الزمن: دالة القطر (تتمة)

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

الشريحة 20: قياس الزمن: دالة القطر (تتمة)

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

الشريحة 21: قياس الزمن: دالة القطر (تتمة)

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

الشريحة 22: قياس الزمن: دالة القطر (تتمة)

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

الشريحة 23: قياس الزمن: دالة القطر (تتمة)

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
  • يصير أبطأ بكثير مع نمو حجم المدخل
  • تربيعي (Quadratic): لقائمة طولها len(L)، تُنفّذ len(L)/2 عملية في المتوسط لكل عنصر
  • len(L) × len(L)/2 عملية — أسوأ من النمو الخطي

الشريحة 24: رسم حجم المدخل مقابل زمن التشغيل

الدوال ونموّها:

الدالة النمو
is_in خطي (linear)
binary_search لوغاريتمي (logarithmic)
diameter تربيعي (quadratic)

ملاحظة المترجم: الشريحة في الأصل رسم بياني (محاور_logarithmic) يقارن المنحنيات الثلاثة. الرسم غير مُضمَّن، ونُقلت أسماء الدوال وتصنيفات النمو المكتوبة على الرسم.

الشريحة 25: آلتان مختلفتان

  • حاسوبي المحمول القديم (My old laptop)

  • حاسوبي المكتبي القديم (My old desktop)

  • أبطأ بنحو الضعفين للمسائل الكبيرة

  • الملاحظة 1: حتى مع الشيفرة نفسها، قد تؤثّر الآلة الفعلية في السرعة.

  • الملاحظة 2: إذا نظرت فقط إلى الزيادة النسبية في زمن التشغيل مقارنةً بتشغيل سابق، فإن مدة الإدخال n أضعاف الحجم تعني أن زمن التشغيل يقارب n أضعاف المدة.

ملاحظة المترجم: الشريحة تعرض منحنيين لأداء نفس الشيفرة على آلتين. الرسم غير مُضمَّن.

الشريحة 26: لا تُخطئني!

  • قياس الزمن أداة حاسمة لتقييم أداء البرامج
  • في نهاية المطاف، هو لا يُستبدل للتقييم في العالم الحقيقي
  • لكن سنرى أداة تكميلية (تعقيد قارب (asymptotic complexity)) لها مزايا أخرى:
    • تقييم أَ priori (قبل كتابة الشيفرة أو تشغيلها)
    • تقيّم الخوارزمية بمعزل عن الآلة وعن التنفيذ (ما الكفاءة الجوهرية للخوارزمية؟)
    • يوفّر فهماً مباشرًا في تصميم الخوارزميات الكفؤة

الشريحة 27: العدّ (COUNTING)

الشريحة 28: عدّ العمليات

  • افترض أن هذه الخطوات تستغرق زمنًا ثابتًا:
    • العمليات الرياضية
    • المقارنات
    • الإسنادات
    • الوصول إلى الكائنات في الذاكرة
  • عدّ عدد هذه العمليات التي تُنفَّذ كدالة في حجم المدخل

convert_to_km ← عمليتان (2 ops)

def convert_to_km(m):
    return m * 1.609

sum_of ← 1+len(L)*3+1 = 3*len(L)+2 عملية

def sum_of(L):
    total = 0
    for i in L:
        total += i
    return total

الشريحة 29: عدّ العمليات: is_in

def is_in_counter(L, x):
    global count
    count += 1 #return of value
    for elt in L:
        count += 2 # set elt, if == test
        if elt==x:
            return True
    return False

الشريحة 30: عدّ العمليات: is_in (تتمة)

def is_in_counter(L, x):
    global count
    count += 1
    for elt in L:
        count += 2
        if elt==x:
            return True
    return False
def binary_search_counter(L, x):
    global count
    lo = 0
    hi = len(L)
    count += 3
    while hi-lo > 1:
        count += 2
        mid = (hi+lo) // 2
        count += 3
        if L[mid] <= x:
            lo = mid
        else:
            hi = mid
        count += 3
    count += 3
    return L[lo] == x

الشريحة 32: عدّ العمليات — النتائج

اختبار is_in:

عدد العناصر عدد العمليات المستخدَمة في is_in
1 9
10 37
100 307
1000 3007
10000 30007
100000 300007
1000000 3000007

اختبار binary_search:

عدد العناصر عدد العمليات المستخدَمة في البحث المتنصيف
1 15
10 85
100 148
1000 211
10000 295
100000 358
1000000 421
  • الملاحظة 1: عدد عمليات is_in يزداد بمعامل 10 مع زيادة الحجم بمعامل 10
  • الملاحظة 2: لكن عدد عمليات البحث المتنصيف ينمو ببطء أكبر بكثير. غير واضح بمعدّل أيّ.

الشريحة 33: رسم حجم المدخل مقابل عدد العمليات

ملاحظة المترجم: الشريحة رسم بياني لنمو عدد العمليات مقابل حجم المدخل لكلتا الدالتين. الرسم غير مُضمَّن، ونُقلت أرقام جدولَي الشريحة 31 التي يستند إليهما.

الشريحة 34: مشاكل القياس والعدّ

  • قياس زمن التشغيل الحقيقي للبرنامج
    • يعتمد على الآلة
    • يعتمد على التنفيذ
    • المدخلات الصغيرة لا تُظهر النمو
  • عدّ العدد الدقيق للخطوات
    • يُفضي بنا إلى صيغة (formula)!
    • مستقل عن الآلة، وهذا شيء جيّد
    • يعتمد على التنفيذ
    • الثوابت الضربية والجمعية لا تهمّ عند المدخلات الكبيرة
  • نريد أن:
    • نقيّم الخوارزمية
    • نقيّم قابلية التوسّع (scalability)
    • نقيّم من حيث حجم المدخل

ملاحظة المترجم: المُقصد «قياس زمن التشغيل الحقيقي للبرنامج» (timing the exact running time of the program).

الشريحة 35: الكفاءة من حيث المدخل: الصورة الكبيرة

  • تذكّر mysum (حلقة واحدة) وsquare (حلقتان متداخلتان)

mysum(x)

  • ماذا حدث لكفاءة البرنامج مع زيادة x؟
  • x أكبر بعشرة أضعاف يعني أن البرنامج
    • استغرق نحو عشرة أضعاف المدة للتشغيل
    • نفّذ نحو عشرة أضعاف عدد العمليات
  • عبّر عنها بصورة «رتبة» (order of) مقارنةً بمتغيّر المدخل: الكفاءة = رتبة x

square(x)

  • ماذا حدث لكفاءة البرنامج مع زيادة x؟
  • x أكبر بضعفين يعني أن البرنامج
    • استغرق نحو أربعة أضعاف المدة للتشغيل
    • نفّذ نحو أربعة أضعاف عدد العمليات
  • x أكبر بعشرة أضعاف يعني أن البرنامج
    • استغرق نحو مئة ضعف المدة للتشغيل
    • نفّذ نحو مئة ضعف عدد العمليات
  • عبّر عنها بصورة «رتبة» (order of) مقارنةً بمتغيّر المدخل: الكفاءة = رتبة x2x^2

الشريحة 36: رتبة النمو (ORDER of GROWTH)

الشريحة 37: رتب النمو (ORDERS OF GROWTH)

  • إنها تدوين (notation)
  • يُقيّم البرامج عندما يكون المدخل كبيرًا جدًا
  • يعبّر عن نمو زمن تشغيل البرنامج
  • يضع حدًّا أعلى (upper bound) على النمو
  • لا يلزم الدقة: «رتبة» وليس نموًا «دقيقًا»
  • ركّز على أكبر العوامل في زمن التشغيل (أي قسم من البرنامج سيستغرق أطول وقت في التشغيل؟)

الشريحة 38: طريقة أفضل — طريقة عامة مع التقريب (Approximation)

  • استخدم فكرة عدّ العمليات في خوارزمية، لكن لا تقلق من التغيّرات الصغيرة في التنفيذ
  • عندما يكون x كبيرًا، فإن 3x+4 و3x وx متشابهة إلى حدّ كبير!
  • لا يهمّنا القيمة الدقيقة: ops = 1+x(2+1)
  • عبّر عنها بصورة «رتبة» (order of) مقارنةً بالمدخل: ops = Order of x
  • ركّز على أداء الخوارزمية عندما يصير حجم المسألة كبيرًا إلى حدّ غير محدود
  • صِل الزمن اللازم لإكمال الحساب بحجم مدخل المسألة
  • يلزمك أن تقرّر ما الذي ستقيسه. ما هو المدخل؟

الشريحة 39: أيّ مدخل نستخدم لقياس الكفاءة؟

  • نريد التعبير عن الكفاءة من حيث المدخل، لذلك يلزم أن نقرّر ما هو مدخلنا
  • قد يكون عددًا صحيحًا — مثل convert_to_km(x)
  • قد يكون طول قائمة — مثل list_sum(L)
  • أنت تقرّر عندما يكون للدالة أكثر من مُعامل — مثل is_in(L, e)
  • قد يختلف الأمر حسب المدخل الذي تنظر إليه

الشريحة 40: المدخلات المختلفة تغيّر طريقة تشغيل البرنامج

  • دالة تبحث عن عنصر في قائمة:
def is_in(L, e):
    for i in L:
        if i == e:
            return True
    return False
  • هل يستغرق البرنامج وقتًا أطول مع زيادة e؟
  • لا

الشريحة 41: المدخلات المختلفة تغيّر طريقة تشغيل البرنامج (تتمة)

def is_in(L, e):
    for i in L:
        if i == e:
            return True
    return False
  • هل يستغرق البرنامج وقتًا أطول مع زيادة L؟
  • ماذا لو كان لـ L طول ثابت وعناصره أرقام كبيرة؟
    • لا
  • ماذا لو كانت لـ L أطوال مختلفة؟
    • نعم!

الشريحة 42: المدخلات المختلفة تغيّر طريقة تشغيل البرنامج (تتمة)

def is_in(L, e):
    for i in L:
        if i == e:
            return True
    return False
  • عندما يكون e هو العنصر الأول في القائمة ← الحالة الأفضل (BEST CASE)
  • عندما ننظر في نحو نصف عناصر القائمة ← الحالة المتوسطة (AVERAGE CASE)
  • عندما لا يكون e في القائمة ← الحالة الأسوأ (WORST CASE)
  • نريد قياس هذا السلوك بطريقة عامة

الشريحة 43: النمو القارب (Asymptotic Growth)

  • الهدف: وصف كيف ينمو الزمن مع نمو حجم المدخل
  • صيغة تربط المدخل بعدد العمليات
  • لدينا صيغة لعدد العمليات اللازمة لحساب خوارزمية، ونريد معرفة السلوك القارب عندما يصير حجم المسألة كبيرًا
  • نريد وضع حدّ على النمو
  • لا يلزم الدقة: «رتبة» وليس نموًا «دقيقًا»
  • سنركّز على الحدّ (term) الذي ينمو أسرع
  • نتجاهل الثوابت الجمعية والضربية، لأننا نريد أن نعرف كيف يزداد الزمن اللازم بسرعة مع زيادة حجم المدخل
  • هذا ما يُسمّى رتبة النمو (order of growth)
  • نستخدم المفاهيم الرياضية «O الكبير» (big O) و«ثيتا الكبير» (big Θ)

الشريحة 44: تعريف O الكبير (Big O)

3x2+20x+1=O(x2)3x^2 + 20x + 1 = O(x^2)
  • لنفترض أن شيفرة ما تعمل في f(x)=3x2+20x+1f(x) = 3x^2 + 20x + 1 خطوة
  • فكّر في هذا في الصيغة الناتجة عن عدّ عدد العمليات.
  • ليس حدًّا أعلى؛ مع x→∞x \to \infty سيتجاوزه f(x) دائمًا
  • لن يتقاطعا أبدًا مرة أخرى!
g(x)=xg(x) = x
  • O الكبير طريقة لوضع حدّ أعلى على نمو أي دالة
  • التقاطع (Crossover)
4x2>3x2+20x+1∀x>20.044x^2 > 3x^2 + 20x + 1 \quad \forall x > 20.04
  • f(x)=O(g(x))f(x) = O(g(x)) يعني أن g(x) مضروبة في ثابت ما ستتجاوز f(x) دائمًا فوق عتبة ما
  • «في النهاية» (eventually) أي فوق قيمة عتبة معيّنة من x

ملاحظة المترجم: الشريحة في الأصل رسم بياني لمنحنيين: المنحنى البرتقالي 4x24x^2 والمنحنى الأزرق 3x2+20x+13x^2+20x+1، مع تحديد نقطة التقاطع عند x=20.04x = 20.04 ووسم «Never cross again!». الرسم غير مُضمَّن، ونُقلت المعادلات والوسوم.

الشريحة 45: O الكبير رسميًا

  • حدّ O الكبير هو حدّ أعلى على نمو دالة ما
  • f(x)=O(g(x))f(x) = O(g(x)) تعني أن هناك ثابتين c0c_0 وx0x_0 بحيث c0 g(x)≥f(x)لكل x>x0c_0 \, g(x) \ge f(x) \quad \text{لكل } x > x_0

مثال: f(x)=3x2+20x+1f(x) = 3x^2 + 20x + 1

f(x)=O(x2)f(x) = O(x^2) لأن 4x2>3x2+20x+1∀x≥204x^2 > 3x^2 + 20x + 1 \quad \forall x \ge 20

(c0=4, x0=20.000)(c_0 = 4,\ x_0 = 20.000)
  • التقاطع عند x=20.04x = 20.04
  • البرتقالي > الأزرق لكل x>20.04x > 20.04
  • هذان الخطان لن يتقاطعا أبدًا مرة أخرى
  • 0≤x≤300 \le x \le 30
  • 0≤x≤1000 \le x \le 100

ملاحظة المترجم: الأرقام 0 <= x <= 30 و0 <= x <= 100 هما حدّا محورَي الرسم الأفقي في النسخة الموسَّعة من الشريحة. المنحنيات نفسها غير مُضمَّنة.

الشريحة 46: تعريف ثيتا الكبير (Big Θ)

3x2−20x−1=θ(x2)3x^2 - 20x - 1 = \theta(x^2)
  • حدّ ثيتا الكبير هو حدّ أعلى وحدّ أدنى على نمو دالة ما
  • لنفترض f(x)=3x2−20x−1f(x) = 3x^2 - 20x - 1

f(x)=Θ(g(x))f(x) = \Theta(g(x)) تعني:

هناك ثابتين c0c_0 وx0x_0 بحيث c0 g(x)≥f(x)لكل x>x0c_0 \, g(x) \ge f(x) \quad \text{لكل } x > x_0

و

هناك ثابتين c1c_1 وx1x_1 بحيث c1 g(x)≤f(x)لكل x>x1c_1 \, g(x) \le f(x) \quad \text{لكل } x > x_1

  • مثال: f(x)=Θ(x2)f(x) = \Theta(x^2) لأن 4x2>3x2−20x−1∀x≥0(c0=4, x0=0)4x^2 > 3x^2 - 20x - 1 \quad \forall x \ge 0 \quad (c_0 = 4,\ x_0 = 0)
  • و 2x2<3x2−20x−1∀x≥20(c1=2, x1=20.000)2x^2 < 3x^2 - 20x - 1 \quad \forall x \ge 20 \quad (c_1 = 2,\ x_1 = 20.000)
  • البرتقالي > الأزرق لكل x>0x > 0
  • الأزرق > الأخضر لكل x>20.04x > 20.04
  • هذان الخطان لن يتقاطعا أبدًا مرة أخرى
  • 0≤x≤1000 \le x \le 100

ملاحظة المترجم: الشريحة رسم بياني بثلاثة منحنيات (برتقالي 4x24x^2، أزرق f(x)f(x)، وأخضر 2x22x^2) مع تحديد التقاطعات. الرسم غير مُضمَّن. وفي النص المستخرَج يظهر الحدّ الثاني مطبوعًا هكذا «∀x ≥ 20»، بينما وسوم الرسم تقول x > 20.04؛ ونُقلت المعادلة كما في الأصل والوسم كما هو.

الشريحة 47: ثيتا مقابل O

  • في الممارسة، يُفضَّل حدود ثيتا لأنها «ضيّقة» (tight)

  • على سبيل المثال: f(x)=3x2−20x−1f(x) = 3x^2 - 20x - 1

  • f x=O x2=O x3=O(2x)f \, x = O \, x^2 = O \, x^3 = O(2x) وكل رتبة أعلى — لأنها كلّها تحدّ من أعلى

  • f x=θ x2f \, x = \theta \, x^2
  • ≠Θ x3≠Θ 2x\ne \Theta \, x^3 \ne \Theta \, 2x وكل رتبة أعلى — لأنها تحدّ من أعلى دون أن تحدّ من أدنى

الشريحة 48: أمثلة على التبسيط

  • تُحذف الثوابت والعوامل الضربية
  • يُركَّز على الحدّ الغالب (dominant term)
Θ(n2):n2+2n+2\Theta(n^2) : n^2 + 2n + 2 Θ(x2):3x2+100000x+31000\Theta(x^2) : 3x^2 + 100000x + 31000 Θ(a):log⁡(a)+a+4\Theta(a) : \log(a) + a + 4

الشريحة 49: الفكرة الكبرى (BIG IDEA)

  • عبّر عن ثيتا من حيث المدخل.
  • لا تستخدم n في كل مرة!

الشريحة 50: جرّب بنفسك!

التعبير رتبته
1000∗log⁡(x)+x1000*\log(x) + x Θ(x)\Theta(x)
n2log⁡(n)+n3n^2\log(n) + n^3 Θ(n3)\Theta(n^3)
log⁡(y)+0.000001y\log(y) + 0.000001y Θ(y)\Theta(y)
2b+1000a2+100∗b2+0.0001a32b + 1000a^2 + 100*b^2 + 0.0001a^3 Θ(2b)\Theta(2b) أو Θ(a3)\Theta(a^3) أو Θ(2b+a3)\Theta(2b+a^3)
  • كلّها قد تكون صحيحة، ويعتمد الأمر على المدخل الذي نهتمّ به

الشريحة 51: استخدام ثيتا لتقييم خوارزميتك

def fact_iter(n):
    """assumes n an int >= 0"""
    answer = 1
    while n > 1:
        answer *= n
        n -= 1
    return answer
  • عدد الخطوات: 5n+25n + 2
  • تعقيد قارب في أسوأ حالة (worst case asymptotic complexity):
  • تجاهل الثوابت الجمعية
    • 2 لا يهمّ عندما يكون n كبيرًا
  • تجاهل الثوابت الضربية
    • 5 لا يهمّ إذا أردنا فقط أن نعرف كيف يغيّر زيادة n الزمن اللازم
Θ(n)\Theta(n)

الشريحة 52: دمج أصناف التعقيد — الحلقات على التوالي (LOOPS IN SERIES)

  • حلّل العبارات داخل الدوال للحصول على رتبة النمو
  • طبّق بعض القواعد، وركّز على الحدّ الغالب
  • قانون الجمع لِـ Θ()\Theta():
    • يُستخدم مع العبارات المتتابعة
  • Θ(f(n))+Θ(g(n))=Θ(f(n)+g(n))\Theta(f(n)) + \Theta(g(n)) = \Theta(f(n) + g(n))
  • على سبيل المثال:
for i in range(n):
    print('a')       # Θ(n)

for j in range(n*n):
    print('b')       # Θ(n^2)
  • Θ(n)+Θ(n∗n)=Θ(n+n2)=Θ(n2)\Theta(n) + \Theta(n * n) = \Theta(n + n^2) = \Theta(n^2) بسبب حدّ n2n^2 الغالب

الشريحة 53: دمج أصناف التعقيد — الحلقات المتداخلة (NESTED LOOPS)

  • حلّل العبارات داخل الدوال للحصول على رتبة النمو
  • طبّق بعض القواعد، وركّز على الحدّ الغالب
  • قانون الضرب لِـ Θ\Theta:
    • يُستخدم مع العبارات/الحلقات المتداخلة
  • Θ(f(n))∗Θ(g(n))=Θ(f(n)∗g(n))\Theta(f(n)) * \Theta(g(n)) = \Theta(f(n) * g(n))
  • على سبيل المثال:
for i in range(n):              # Θ(n)
    for j in range(n//2):       # Θ(n) لكل تكرار للحلقة الخارجية
        print('a')
  • Θ(n)×Θ(n)=Θ(n×n)=Θ(n2)\Theta(n) \times \Theta(n) = \Theta(n \times n) = \Theta(n^2)
  • الحلقة الخارجية تعمل n مرات والحلقة الداخلية تعمل n مرات لكل تكرار من الحلقة الخارجية.

الشريحة 54: تحليل التعقيد

  • ما هو تعقيد ثيتا لهذا البرنامج؟
def f(x):
    answer = 1
    for i in range(x):
        for j in range(i,x):
            answer += 2
    return answer
  • الحلقة الخارجية Θ(x)\Theta(x)

  • الحلقة الداخلية Θ(x)\Theta(x)

  • كل ما تبقّى Θ(1)\Theta(1)

  • Θ(1)+Θ(x)∗Θ(x)∗Θ(1)+Θ(1)\Theta(1) + \Theta(x) * \Theta(x) * \Theta(1) + \Theta(1)
  • التعقيد الكلي هو Θ(x2)\Theta(x^2) بقواعد الجمع والضرب

الشريحة 55: جرّب بنفسك!

  • ما هو تعقيد ثيتا لهذا البرنامج؟ انتبه لوصفه من حيث المدخل
  • (تلميح: ما الذي يهمّ مع القوائم، حجم عناصرها أم طولها؟)
def f(L):
    Lnew = []
    for i in L:
        Lnew.append(i**2)
    return Lnew

الإجابة:

  • الحلقة: Θ(len(L))\Theta(len(L))
  • f هي Θ(len(L))\Theta(len(L))

الشريحة 56: جرّب بنفسك!

  • ما هو تعقيد ثيتا لهذا البرنامج؟
def f(L, L1, L2):
    """ L, L1, L2 are the same length """
    inL1 = False
    for i in range(len(L)):
        if L[i] == L1[i]:
            inL1 = True
    inL2 = False
    for i in range(len(L)):
        if L[i] == L2[i]:
            inL2 = True
    return inL1 and inL2

الإجابة:

  • الحلقة: Θ(len(L))+Θ(len(L))\Theta(len(L)) + \Theta(len(L))
  • f هي Θ(len(L))\Theta(len(L)) أو Θ(len(L1))\Theta(len(L1)) أو Θ(len(L2))\Theta(len(L2))

الشريحة 57: أصناف التعقيد (COMPLEXITY CLASSES)

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

  • Θ(1)\Theta(1) تدل على زمن تشغيل ثابت
  • Θ(log⁡n)\Theta(\log n) تدل على زمن تشغيل لوغاريتمي
  • Θ(n)\Theta(n) تدل على زمن تشغيل خطي
  • Θ(nlog⁡n)\Theta(n \log n) تدل على زمن تشغيل لوغاريتمي-خطي
  • Θ(nc)\Theta(n^c) تدل على زمن تشغيل متعدّد الحدود (polynomial) — حيث c ثابت
  • Θ(cn)\Theta(c^n) تدل على زمن تشغيل أُسّي (exponential) — حيث c ثابت مرفوع إلى أُسّ يعتمد على حجم المدخل

الشريحة 58: نمو أصناف التعقيد (COMPLEXITY GROWTH)

الصنف N = 10 N = 100 N = 1000 N = 1000000
Constant (ثابت) 1 1 1 1
Logarithmic (لوغاريتمي) 1 2 3 6
Linear (خطي) 10 100 1000 1000000
Log-linear (لوغاريتمي-خطي) 10 200 3000 6000000
Polynomial (متعدّد الحدود) 100 10000 1000000 1000000000000
Exponential (أُسّي) 1024 1267650600228229 4014967030705376 1071508607186267320948425049060001810561404811705533607495500997724955095019507183129258198524957007686984145252000000000000000000000000000
  • حظًا موفّقًا!!

ملاحظة المترجم: عمود N = 10 لأُسّي هو 210=10242^{10} = 1024، وعمود N = 100 هو 21002^{100}، وعمود N = 1000 هو 210002^{1000}، وعمود N = 1000000 هو 210000002^{1000000}. أرقام هذه القيم الضخمة في الملف الأصلي موزّعة على أسطر متتابعة بلا فاصل، وقد نُقلت كما هي بلا حذف أو تقريب. ولاحظ أن عدد خاناتها يزداد بنحو رتبة مقدار لكل مضاعف إضافي من 10 في N.

الشريحة 59: الخلاصة

  • قياس الزمن يعتمد على الآلة/التنفيذ/الخوارزمية
  • عدّ العمليات يعتمد على التنفيذ/الخوارزمية
  • رتبة النمو تعتمد على الخوارزمية
  • قارن كفاءة الخوارزميات
    • تدوين يصف النمو
    • رتبة النمو الأدنى أفضل
    • مستقل عن الآلة أو عن تنفيذ بعينه
  • باستخدام ثيتا
    • صِف رتبة النمو القارب (asymptotic order of growth)
    • التدوين القارب (asymptotic notation)
    • حدّ أعلى وحدّ أدنى

الشريحة 60: MIT OpenCourseWare

  • https://ocw.mit.edu
  • 6.100L Introduction to Computer Science and Programming Using Python — Fall 2022
  • ولمعلومات كيفية الاستشهاد بهذه المواد أو شروط استخدامها، راجع: https://ocw.mit.edu/terms