المحاضرة 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.
- صفحة المحاضرة الرسمية على OCW: https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/pages/lecture-22-big-oh-and-theta/
- الشرائح (ملف PDF): https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/resources/mit6_100l_f22_lec22_pdf/ — والملف المباشر: https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/mit6_100l_f22_lec22.pdf
- ملفات الشيفرة للتمرين: https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/resources/mit6_100l_f22_lec22_code_py/
- النص الكامل (Transcript) للمحاضرة على OCW: https://ocw.mit.edu/courses/6-100l-introduction-to-cs-and-programming-using-python-fall-2022/resources/mit6_100l_f22_lec22/
- رخصة CC BY-NC-SA 4.0: https://creativecommons.org/licenses/by-nc-sa/4.0/
- شروط الاستخدام في MIT OCW: https://ocw.mit.edu/terms/
منهج الترجمة: عنوان واحد وترجمة عربية لكل شريحة من شرائح الملف الأصلي (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
الشريحة 31: عدّ العمليات: البحث المتنصيف (binary search)
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) مقارنةً بمتغيّر المدخل: الكفاءة = رتبة
الشريحة 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)
- لنفترض أن شيفرة ما تعمل في خطوة
- فكّر في هذا في الصيغة الناتجة عن عدّ عدد العمليات.
- ليس حدًّا أعلى؛ مع سيتجاوزه
f(x)دائمًا - لن يتقاطعا أبدًا مرة أخرى!
- O الكبير طريقة لوضع حدّ أعلى على نمو أي دالة
- التقاطع (Crossover)
- يعني أن
g(x)مضروبة في ثابت ما ستتجاوزf(x)دائمًا فوق عتبة ما - «في النهاية» (eventually) أي فوق قيمة عتبة معيّنة من
x
ملاحظة المترجم: الشريحة في الأصل رسم بياني لمنحنيين: المنحنى البرتقالي والمنحنى الأزرق ، مع تحديد نقطة التقاطع عند ووسم «Never cross again!». الرسم غير مُضمَّن، ونُقلت المعادلات والوسوم.
الشريحة 45: O الكبير رسميًا
- حدّ O الكبير هو حدّ أعلى على نمو دالة ما
- تعني أن هناك ثابتين و بحيث
مثال:
لأن
- التقاطع عند
- البرتقالي > الأزرق لكل
- هذان الخطان لن يتقاطعا أبدًا مرة أخرى
ملاحظة المترجم: الأرقام
0 <= x <= 30و0 <= x <= 100هما حدّا محورَي الرسم الأفقي في النسخة الموسَّعة من الشريحة. المنحنيات نفسها غير مُضمَّنة.
الشريحة 46: تعريف ثيتا الكبير (Big Θ)
- حدّ ثيتا الكبير هو حدّ أعلى وحدّ أدنى على نمو دالة ما
- لنفترض
تعني:
هناك ثابتين و بحيث
و
هناك ثابتين و بحيث
- مثال: لأن
- و
- البرتقالي > الأزرق لكل
- الأزرق > الأخضر لكل
- هذان الخطان لن يتقاطعا أبدًا مرة أخرى
ملاحظة المترجم: الشريحة رسم بياني بثلاثة منحنيات (برتقالي ، أزرق ، وأخضر ) مع تحديد التقاطعات. الرسم غير مُضمَّن. وفي النص المستخرَج يظهر الحدّ الثاني مطبوعًا هكذا «∀x ≥ 20»، بينما وسوم الرسم تقول
x > 20.04؛ ونُقلت المعادلة كما في الأصل والوسم كما هو.
الشريحة 47: ثيتا مقابل O
-
في الممارسة، يُفضَّل حدود ثيتا لأنها «ضيّقة» (tight)
-
على سبيل المثال:
-
وكل رتبة أعلى — لأنها كلّها تحدّ من أعلى
-
وكل رتبة أعلى — لأنها تحدّ من أعلى دون أن تحدّ من أدنى
الشريحة 48: أمثلة على التبسيط
- تُحذف الثوابت والعوامل الضربية
- يُركَّز على الحدّ الغالب (dominant term)
الشريحة 49: الفكرة الكبرى (BIG IDEA)
- عبّر عن ثيتا من حيث المدخل.
- لا تستخدم
nفي كل مرة!
الشريحة 50: جرّب بنفسك!
| التعبير | رتبته |
|---|---|
| أو أو |
- كلّها قد تكون صحيحة، ويعتمد الأمر على المدخل الذي نهتمّ به
الشريحة 51: استخدام ثيتا لتقييم خوارزميتك
def fact_iter(n):
"""assumes n an int >= 0"""
answer = 1
while n > 1:
answer *= n
n -= 1
return answer
- عدد الخطوات:
- تعقيد قارب في أسوأ حالة (worst case asymptotic complexity):
- تجاهل الثوابت الجمعية
2لا يهمّ عندما يكونnكبيرًا
- تجاهل الثوابت الضربية
5لا يهمّ إذا أردنا فقط أن نعرف كيف يغيّر زيادةnالزمن اللازم
الشريحة 52: دمج أصناف التعقيد — الحلقات على التوالي (LOOPS IN SERIES)
- حلّل العبارات داخل الدوال للحصول على رتبة النمو
- طبّق بعض القواعد، وركّز على الحدّ الغالب
- قانون الجمع لِـ :
- يُستخدم مع العبارات المتتابعة
- على سبيل المثال:
for i in range(n):
print('a') # Θ(n)
for j in range(n*n):
print('b') # Θ(n^2)
- بسبب حدّ الغالب
الشريحة 53: دمج أصناف التعقيد — الحلقات المتداخلة (NESTED LOOPS)
- حلّل العبارات داخل الدوال للحصول على رتبة النمو
- طبّق بعض القواعد، وركّز على الحدّ الغالب
- قانون الضرب لِـ :
- يُستخدم مع العبارات/الحلقات المتداخلة
- على سبيل المثال:
for i in range(n): # Θ(n)
for j in range(n//2): # Θ(n) لكل تكرار للحلقة الخارجية
print('a')
- الحلقة الخارجية تعمل
nمرات والحلقة الداخلية تعملnمرات لكل تكرار من الحلقة الخارجية.
الشريحة 54: تحليل التعقيد
- ما هو تعقيد ثيتا لهذا البرنامج؟
def f(x):
answer = 1
for i in range(x):
for j in range(i,x):
answer += 2
return answer
-
الحلقة الخارجية
-
الحلقة الداخلية
-
كل ما تبقّى
-
التعقيد الكلي هو بقواعد الجمع والضرب
الشريحة 55: جرّب بنفسك!
- ما هو تعقيد ثيتا لهذا البرنامج؟ انتبه لوصفه من حيث المدخل
- (تلميح: ما الذي يهمّ مع القوائم، حجم عناصرها أم طولها؟)
def f(L):
Lnew = []
for i in L:
Lnew.append(i**2)
return Lnew
الإجابة:
- الحلقة:
fهي
الشريحة 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
الإجابة:
- الحلقة:
fهي أو أو
الشريحة 57: أصناف التعقيد (COMPLEXITY CLASSES)
نريد تصميم خوارزميات تكون أقرب ما يمكن إلى أعلى هذا التسلسل الهرمي قدر الإمكان
- تدل على زمن تشغيل ثابت
- تدل على زمن تشغيل لوغاريتمي
- تدل على زمن تشغيل خطي
- تدل على زمن تشغيل لوغاريتمي-خطي
- تدل على زمن تشغيل متعدّد الحدود (polynomial) — حيث
cثابت - تدل على زمن تشغيل أُسّي (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لأُسّي هو ، وعمودN = 100هو ، وعمودN = 1000هو ، وعمودN = 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