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

النص الكامل للمحاضرة 22: رمزا O الكبيرة وثيتا

النص الكامل للمحاضرة 22: رمزا O الكبيرة وثيتا

المصدر الأصلي: النص الرسمي الإنجليزي، PDF، فيديو المحاضرة الرسمي، صفحة المقرر.

النسبة والترخيص: آنا بيل (Ana Bell)، مقرر 6.100L، خريف 2022، معهد ماساتشوستس للتكنولوجيا، MIT OpenCourseWare. هذه ترجمة عربية غير رسمية وليست معتمدة من MIT ولا تعني تأييده لها. المواد الأصلية المشمولة والترجمة خاضعة لرخصة CC BY-NC-SA 4.0: الاستخدام غير التجاري، مع النسبة، ومشاركة التكييفات بالرخصة نفسها. لا تشمل الرخصة الافتراضية مواد الأطراف الثالثة المستثناة.

ملاحظة المترجم عن التغطية: تُرجمت صفحات النص الرسمي العشرون، من البداية حتى نهاية المحاضرة، فقرةً فقرةً دون تلخيص. تتبع الأقسام فواصل الصفحات المادية في PDF الرسمي، وتحافظ على ترتيب الفقرات والتكرار والتصحيحات الشفهية ومداخلات الحضور والإشارات الصوتية. تُحفظ أسماء Python والتعبيرات الرياضية، وتُفصل ملاحظات المترجم عن كلام المصدر. مصدر المقابلة المحلي هو content-src/mit-6100l/extracted/1A8qBYXfGlxrh1AzmvG-kNRw4M5bppdQ-_transcript.txt، مع النسخة .layout.txt للتحقق من حدود الفقرات.

صفحة المصدر 1

MITOCW | المحاضرة 22 من 6.100L، الإصدار 2.mp4

[صرير] [حفيف] [نقرات]

آنا بيل (ANA BELL):

حسنًا، لنبدأ. في المحاضرة الماضية بدأنا الحديث عن موضوع جديد تمامًا في علوم الحاسوب. وبدأنا نتعلم كيفية معرفة زمن تشغيل (Runtime) برامجنا. إذًا فعلنا—نظرنا إلى كيفية توقيت البرنامج فعلًا بتحديد المدة التي يستغرقها بالضبط. ثم كيفية عدّ عدد العمليات في البرنامج.

اليوم سنفعل الشيء نفسه تمامًا في البداية. في النصف الأول من المحاضرة سنوقّت مجموعة من البرامج ثم سنعدّ عدد العمليات، كما من قبل تمامًا. لكننا سنفعل ذلك في سياق برامج أو دوال مختلفة قليلًا وأكثر إثارة للاهتمام قليلًا، تتضمن أعدادًا فقط بوصفها معاملات (Parameters)، ثم دوال تتضمن قوائم (Lists) بوصفها معاملات.

سيكون ذلك النصف الأول من المحاضرة. ثم بعد ذلك سننظر إلى فكرة رتبة النمو (Order of Growth)، وهي نوعًا ما ما نمهّد له بهذه المجموعة من المحاضرات. وفي رتبة النمو سيكون هناك قليل من الرياضيات، وقليل من الرسم البياني، لكن ليس كثيرًا. ثم سنرى فقط كيفية تقييم رتبة نمو الدوال فعلًا من تلك النقطة فصاعدًا.

حسنًا، دعونا نبدأ بمعرفة زمن تشغيل برامجنا فقط، صحيح؟ كانت هذه طريقة سريعة وسهلة جدًّا لنا لمعرفة المدة التي تستغرقها برامجنا بالضبط. في المحاضرة الماضية استوردنا وحدة time هذه (Module)، ونفعل ذلك مرة أخرى هذه المرة.

لكن بدلًا من تشغيل دالة الوقت التي رأيناها في المحاضرة الماضية هنا، بدلًا من تشغيل دالة الوقت التي أعطتنا نوعًا من الوقت المطلق العام منذ تاريخ ما في الماضي، سنشغّل هذه الدالة المختلفة قليلًا المسماة عدّاد الأداء (Performance Counter). وهذا ما يُستخدم عادة في العالم الحقيقي لمعرفة المدة التي يستغرقها برنامج فعلي أو دالة للتشغيل. والسبب في استخدامنا لها هو أنها أدق.

إذًا دالة time.time التي استخدمناها في المحاضرة الماضية أعطتنا ربما دقة مقدارها 1 × 10^-3 أو شيء كبير جدًّا جدًّا كهذا. يستطيع عدّاد الأداء فعلًا أن يعطينا دقة لشيء أصغر بكثير، كثيرًا. ربما 1 × 10^-8 أو شيء صغير جدًّا. لذلك سنتمكن من رؤية أزمنة بعض الدوال التي كانت في الأساس صفرًا في المحاضرة الماضية.

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

وعدّاد الأداء مفيد جدًّا عندما نحصل على فروق الوقت هذه، DT، صحيح؟ الفرق بين بعض الأوقات. إذًا نشغّل عدّاد الأداء للحصول على «وقت البداية»، بين علامتي اقتباس. ونشغّل الدالة. ونشغّل عدّاد الأداء مرة أخرى للحصول على «وقت التوقف»، بين علامتي اقتباس. ونطرح وقت البداية للحصول على DT.

ثم سنطبع ذلك الوقت لنرى المدة التي تستغرقها الدالة فعلًا للتشغيل. نعم، هذا ما قلته. حسنًا، سننظر إلى دالتين مختلفتين عن المرة الماضية، لكن سيكون لهما الطابعان العامان نفسيهما اللذان رأيناهما في المحاضرة الماضية.

صفحة المصدر 2

إذًا الدالة الأولى التي سننظر إليها اسمها التحويل إلى كيلومترات، تأخذ عددًا من الأميال وتُرجع القيمة بالكيلومترات. والدالة الثانية دالة اسمها compound. ينبغي أن تبدو هذه مألوفة جدًّا. ستعيد إليكم ذكريات مجموعة المسائل الأولى.

إنها دالة تأخذ استثمارًا شهريًّا، ومعدل فائدة للشهر، وعددًا من الأشهر لاستثمار ذلك المبلغ. وتُرجع مقدار المال الذي حصلتم عليه خلال ذلك العدد من الأشهر. يمكنكم أن تروا هنا أن لديكم المجموع وقد هُيّئ، وحلقة تمر على ذلك العدد من الأشهر، وتحدّث المجموع وفق معدل الفائدة ومقدار المال الذي لديكم الآن، زائد ما استثمرتموه لذلك الشهر.

حسنًا، الأسئلة الثلاثة التي سنجيب عنها، تمامًا كما أجبنا في المحاضرة الماضية، هي: كم يستغرق تشغيل هذه الدوال بالثواني الفعلية؟ على أي معاملات مدخلات تعتمد الدالة فعلًا؟ وهل تعمل هاتان الدالتان فعلًا لمدد زمنية مختلفة؟ وما ذلك الفرق، صحيح؟ هل تعمل إحداهما في 12 ثانية والأخرى في 0.5؟ ما الوقت الفعلي الذي يستغرقه تشغيلهما؟

إذًا هذا الكود الخاص بنا. هاتان هما الدالتان. قبل أن نتابع دعوني أريكم كيف ننشئ المدخلات. كما من قبل تمامًا، ننشئ قائمة بكل المدخلات المختلفة التي سنشغّل الدالة بها. هنا لدي هذه ln. ستحتوي الأعداد 1 و10 و100 و1,000 وهكذا. وستكون هذه معاملات دالتي. واحدًا في كل مرة بالطبع.

ثم لدي حلقتي هنا لكل واحد من تلك المدخلات، 1 و10 و100 و1,000. سأشغّل دالتي فقط، صحيح؟ هنا أقيس الوقت الذي تستغرقه. ثم سأعرض الوقت الذي استغرقه تشغيل البرنامج. وللتسلية فقط سأعرض أيضًا عدد المرات التي كان يمكن أن يعمل فيها هذا البرنامج خلال ثانية واحدة. لأنه بالنسبة إليَّ كان من الصعب قليلًا قراءة 1 × 10^-8 أو شيء كهذا. لكن كان أسهل بكثير لي رؤية هذا العدد الكبير لعدد مرات تشغيل تلك الدالة الممكنة في ثانية واحدة.

هنا لدي التحويل إلى كيلومترات. سأشغّله. وسنرى أنه هذا هنا: المدة التي استغرقتها الدالة فعلًا. في آخر مرة شغّلنا فيها برنامجًا بسيطًا جدًّا كهذا، قالت النتائج كلها تقريبًا إنه استغرق صفر ثانية، صحيح؟ كان سريعًا جدًّا فحسب، بحيث لم تتمكن دالة time.time من التقاط فرق الوقت الدقيق ذاك.

لكن عدّاد الأداء هذا يستطيع، وهذا أجمل بكثير. الآن نرى أنه مهما كان المدخل، يبدو أن الوقت هو نفسه تقريبًا، صحيح؟ 3 × 10^-7 ثانية مهما كان المدخل. كان ذلك متوقعًا.

والآن ماذا عن دالة compound؟ ستكون أكثر إثارة للاهتمام قليلًا. لأن هناك فعلًا ثلاثة معاملات لهذه الدالة، صحيح؟ ما سنفعله هو تغيير كل واحد منها، ورؤية أي من تلك المعاملات يؤثر فعلًا في زمن التشغيل.

هنا هذا الجزء سيثبّت معدل الفائدة لدي، ويثبّت عدد الأشهر. وسأغيّر المبلغ الذي أستثمره كل شهر. حسنًا، إذا شغّلت ذلك، كان سريعًا جدًّا. مرة أخرى ننظر إلى النتائج هنا. ومهما كان المبلغ الذي أستثمره كل شهر، يبدو أن البرنامج لا يغيّر فعلًا المدة التي يستغرقها للتشغيل، صحيح؟ دائمًا نحو 1 × 10^-6 ثانية للتشغيل.

صفحة المصدر 3

حسنًا، ماذا لو غيّرت معدل الفائدة؟ كان تغيير هذا أصعب قليلًا. لكنني استقررت على هذا بوصفه الشيء الذي أغيّره. آسفة، أغيّره بهذه الطريقة. إذًا سيكون 1.1 أو 1.0 أو 1.001. هذا ما سأستثمره—معدل الفائدة لكل ما سأستثمر فيه. وسأثبّت 10 دولارات بصفته استثماري في الشهر، وأثبّت الأشهر الاثني عشر مرة أخرى.

إذا شغّلت ذلك، فالأمر نفسه. يبدو أن تغيير هذا الاستثمار لا يحدث فرقًا كبيرًا فعلًا في المدة التي يستغرقها البرنامج للتشغيل. معامل أخير نجربه. الآن سأثبّت الاستثمار الأولي عند 10 دولارات شهريًّا، وسأثبّت معدل الفائدة عند هذا شهريًّا. وسأغيّر عدد الأشهر التي سأستثمر خلالها هذا.

إذًا مرة أخرى ستكون n هي 1 و10 و100 و1,000 و10,000 وهكذا. دعونا نرى ما سيفعله هذا. إنه يفعل بالفعل شيئًا مختلفًا عن الاثنين الآخرين، لأنه لم ينتهِ من التشغيل بعد، صحيح؟ ما زال يعمل على هذا الأخير هنا في الأسفل. لكن يمكننا أن نرى أن أشياء أكثر إثارة للاهتمام تحدث الآن، صحيح؟

إذًا هنا لدي—في البداية من الصعب قليلًا التمييز لتلك الأعداد الصغيرة، وهذا مقبول. لكن لحسن الحظ يمكننا تشغيله لمجموعة من المدخلات. ابتداء من هنا تقريبًا عندما أبدأ الاستثمار—آسفة، عندما أبدأ استثمار مالي على مدى 1,000 شهر و10,000 شهر و100,000 شهر وهكذا، يبدو أننا نرى نمطًا.

مرة أخرى، لـ1,000 شهر يستغرق—يستغرق البرنامج نحو 5 × 10^-5 ثانية للتشغيل. إذا زدت عدد الأشهر بعامل 10، يستغرق ضرب 10^-4 ثانية للتشغيل. ثم مع زيادة مدخلي بعامل 10، عدد الأشهر، يبدو أن وقت التشغيل لدي يزيد بعامل 10 أيضًا، صحيح؟ إذًا 0.005 و0.05 و0.7 و0.8، شيء من هذا القبيل.

حسنًا، هذا من تشغيل سابق. بالطبع سيكون كل تشغيل مختلفًا، لأننا فقط نحصل على الوقت الذي استغرقه البرنامج للتشغيل، لذلك سيكون الوقت الفعلي مختلفًا. لكن هناك بضعة أشياء نلاحظها. Python عرضت فعلًا الوقت الذي استغرقه البرنامج للتشغيل بالصيغة العلمية (Scientific Notation)، وهذا لطيف نوعًا ما.

إذًا هذا 4.3 × 10^-6. عرفت كيف تعرضه لي هكذا. لذلك هو ليس—ليس فيه مجموعة من الأصفار. ثم الملاحظة كما ربما كنا—كما ربما توقعتم هي—لهذا التحويل إلى كيلومترات كان مستقلًّا، صحيح؟ إذًا هذا هو الكيلومترات، لا دالة compound.

لكن دالة compound هنا، هذا مرة أخرى من تشغيل سابق، يمكننا تدوين بعض الملاحظات. الأولى أن الوقت لم يتغير فعلًا مع المدخل إلا عندما غيّرنا عدد الأشهر، n_months. عندما غيّرنا الاستثمار الأولي أو معدل الفائدة، استغرق البرنامج في الأساس الوقت نفسه للتشغيل. إذًا عدد الأشهر فقط هو الذي أحدث فرقًا فعليًّا لنا.

الملاحظة الثانية، مرة أخرى شيء لاحظناه، هي أننا مع زيادة عدد الأشهر بعامل 10، يزيد الوقت الذي يستغرقه البرنامج للتشغيل بعامل 10 أيضًا. مرة أخرى، شيء تحدثنا عنه. والملاحظة الأخيرة هي أن هذه العلاقة تظهر بوضوح شديد عندما يكون المدخل كبيرًا جدًّا، صحيح؟

عندما يكون المدخل صغيرًا، أعتقد أنني ذكرت هذا في المرة الماضية، إذا حدّث حاسوبي نفسه لسبب ما أو قرر تخصيص بعض الموارد لتشغيل تطبيق في الخلفية لأي سبب، أثناء محاولته حساب دالة compound بمدخل قيمته 1، فقد يتغير هذا العدد تغيرًا هائلًا، صحيح؟ لأن 2 × 10^-6 يمكن أن يتأثر كثيرًا بقليل من الوقت المخصص لشيء آخر فقط.

صفحة المصدر 4

بينما 4 ثوانٍ أو 14 ثانية، إذا خصص حاسوبي قليلًا من الوقت لشيء آخر، فلن تتأثر تلك الـ4 أو الـ14 كثيرًا، صحيح؟ لذلك عندما تكون الأعداد كبيرة، فهذا هو الوقت الذي يمكننا فيه رؤية سلوك دالتنا بوضوح أكبر بكثير. لا عندما تكون الأعداد صغيرة.

حسنًا، الآن أود النظر إلى بعض الدوال الأخرى. ستكون مدخلات هذه الدوال قائمة بدلًا من أعداد فقط. رأينا مجموعة من الأمثلة مع الأعداد. لكن دعونا نرى ما يحدث عندما يكون مدخلي قائمة.

هذه دالة بسيطة جدًّا. تأخذ قائمة L. وتجمع جميع العناصر في القائمة L. رأينا هذا مرات كثيرة بالفعل. نهيّئ total ليكون صفرًا، ونمر على كل عنصر في L، ونحتفظ بمجموعنا التراكمي بإضافة العنصر إلى ذلك المجموع فقط—بسيط جدًّا—ونُرجعه.

والآن كيف نشغّل هذه الدالة فعلًا بمجموعة كبيرة من المدخلات المختلفة؟ هذا ما سنراه بعد ذلك. هذا الجزء هنا هو نفسه تمامًا كما من قبل. إنه ينشئ لنا فعلًا قائمة 1 و10 و100 و1,000 و10,000 وهكذا.

لكن بوضوح لا يمكن أن يكون العدد 10 مدخلًا لهذه الدالة لأن هذه الدالة تتوقع قائمة. لذلك لا يمكن أن تكون L هي 10. يجب أن تكون قائمة فيها بعض الأشياء. بدلًا من ذلك، ما سنفعله انطلاقًا من ذلك العدد، 1 و10 و100 و1,000 وهكذا، هو أن ننشئ فعلًا قائمة فيها بعض الأعداد الوهمية العشوائية فقط. لا يهمني فعلًا ما هذه الأعداد.

إذًا دعونا نجعلها الأعداد من 0 إلى 9، ومن 0 إلى 99، ومن 0 إلى 999 فقط. وفعليًّا سيكون في كل واحدة من هذه القوائم بعد ذلك عنصر واحد، أو 10 عناصر، أو 100 عنصر، أو 1,000 عنصر، وهكذا. حسنًا، هل الجميع بخير مع ذلك؟

حسنًا، المدخل مختلف الآن. يجب أن يكون قائمة. ننشئ فقط مجموعة من القوائم بأطوال مختلفة. العلاقة بين هذه الأطوال هي أن القوائم أكبر بعشر مرات من القائمة السابقة، صحيح؟

ثم الآن لدي قائمة المدخل هنا. أفعل الشيء نفسه تمامًا كما من قبل. ليس بعد. أشغّل عدّاد الأداء لإنشاء وقت البداية. أشغّل دالتي وأحصل على DT وأعرض الشيء نفسه تمامًا كما من قبل. حسنًا، دعونا نشغّل ذلك هنا في الأسفل.

يعمل، يعمل، يعمل. مرة أخرى علينا الانتظار قليلًا. يبدو أنه يعمل، لكنه يصبح أبطأ فأبطأ فقط، وهذا مقبول. ماذا نلاحظ؟ عندما كان لدي عنصر واحد في قائمتي، استغرق 1 × 10^-5 ثانية للتشغيل. وعندما كان لدي 10، استغرق 1 × 10^-6 ثانية للتشغيل.

كان الوقت أقصر فعلًا مع وجود عناصر أكثر فيها. ترون، هذا ما أقصده بأن الأمر لا يمكن التنبؤ به كثيرًا للأعداد الصغيرة. لكن عندما نصل إلى قوائم بطول 1,000 و10,000 و100,000 ومليون وهكذا، يمكننا البدء برؤية النمط. مع 10,000 استغرق 4 × 10^-4 ثانية للتشغيل. ومع 100,000 استغرق ضرب 10^-3 ثانية للتشغيل.

ثم مع زيادة المدخل بعامل 10، أي إن طول قائمتي يزيد بعامل 10، يبدو أن البرنامج يستغرق عشرة أمثال الوقت للتشغيل. حسنًا، شيء مشابه جدًّا لما سبق. إذًا الملاحظة الأولى التي يمكننا استخلاصها من هذه الدالة هي أن حجم المدخل (Input Size)، بالطبع، أصبح طول قائمتنا، صحيح؟ ليس العدد 10 أو العدد 1,000 فقط. إنه طول—قائمة بطول 10 أو قائمة بطول 1,000.

صفحة المصدر 5

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

ثم الملاحظة الأخيرة، هذه بالمقارنة مع دالة compound حيث نغيّر عدد الأشهر. يبدو أن الوقت الذي يستغرقه هذا البرنامج فعلًا للتشغيل قابل للمقارنة—من حيث الثواني البحتة فقط—بالوقت الذي استغرقته compound للتشغيل.

هنا عندما كان مدخلي 100 مليون، أعتقد؟ نعم، 100 مليون. استغرق نحو 8 ثوانٍ للتشغيل. وعندما كان في قائمتي 100 مليون عنصر، استغرق نحو 7 ثوانٍ للتشغيل، صحيح؟ ثم أسرع بعشر مرات أثناء—عندما خفّضنا المدخل بعامل 10، حسنًا؟

إذًا بدأنا بالفعل نرى شيئًا سنصل إلى—نعم، تفضل.

أحد الحضور (AUDIENCE):

ما دالة sum_of؟

آنا بيل (ANA BELL):

آه، sum_of هي هذه الدالة التي كتبتها هنا فقط. نعم، إنها فقط—هل سمّيتها شيئًا مختلفًا هنا؟ لا.

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

هل هناك أسئلة أخرى قبل الانتقال إلى دالة أخرى للقوائم؟ حسنًا، دعونا ننظر إلى مسألة مختلفة قليلًا تتعامل مع القوائم. هذه الدالة، أو هذه الدوال الثلاث، تتعامل مع إيجاد عنصر في قائمة. وسنقارن أزمنة تشغيل هذه الدوال الثلاث.

إذًا الدالة الأولى ستكون طريقة قوة غاشمة (Brute Force) جدًّا لإيجاد العنصر في قائمة. هل كان هناك سؤال؟ نعم.

أحد الحضور (AUDIENCE):

[غير مسموع]. آه، انتظري. لا عليك، أراه. لا عليك، فهمته.

آنا بيل (ANA BELL):

حسنًا، لا مشكلة. حسنًا، الدالة الأولى ستجري بحثًا بالقوة الغاشمة لإيجاد عنصر x هنا، أحد معاملاتي، ضمن قائمة هي معاملاتي الآخر. في الأساس، عند إعطاء قائمة فيها مجموعة من العناصر، تنظر هذه الدالة بشق الأنفس إلى كل عنصر واحدًا في كل مرة وتسأل هل ذلك العنصر هو الذي أبحث عنه. حسنًا، تبدأ هنا، عند بداية قائمتي، وتتجه إلى نهاية قائمتي. هذا ما تفعله is_in.

التالية، البحث الثنائي (Binary Search)، تبحث أيضًا عن عنصر في قائمة، مع التأكد من أن القائمة مرتبة. لكن الطريقة التي ستفعل بها ذلك أذكى قليلًا. سأرسم قائمتي فعلًا بهذه الطريقة.

كل واحد من هذه سيكون عنصرًا في قائمتي. سيجري هذا بحثًا بالتنصيف (Bisection Search) لإيجاد العنصر في القائمة. إذًا مرة أخرى نبحث عن العنصر x داخل هذه القائمة L. وتذكروا أننا في البحث بالتنصيف نبدأ بنقطة طرفية للبداية ونقطة طرفية للنهاية.

صفحة المصدر 6

وتخميننا الأول للمكان—للعنصر أو لأي شيء نبحث عنه هو أن نقول: ما الـ—هل هو العنصر الأوسط؟ إذًا ننظر إلى العنصر في المنتصف ونسأل: هل أنت الذي أبحث عنه؟ في هذه الحالة بالتحديد تنظرون إلى العنصر في المنتصف وتقولون: هل أنت x التي أبحث عنها، صحيح؟ جيد. إذًا هذا هو هذا هنا.

حساب المنتصف موجود هنا، صحيح؟ والسبب في استخدامنا // للقسمة الصحيحة (Integer Division) هو الحالة التي تكون فيها لدي قائمة أنظر فعلًا إلى المنتصف هنا، صحيح؟ من الواضح أنني لا أستطيع أن أطلب الـ—أطلب من Python العنصر عند الفهرس 3.5، صحيح؟ لا تعمل هكذا.

لذلك سأقرّب إلى الأسفل فقط. يمكنكم أيضًا التقريب إلى الأعلى إذا أردتم. اخترت التقريب إلى الأسفل فقط حتى أحصل فعلًا على العنصر عند فهرس صحيح.

حصلت إذًا على العنصر الأوسط، ثم أسأل: هل أنت الذي أبحث عنه؟ وإذا لم يكن كذلك، أسأل هل هذا أصغر مما ينبغي أم أكبر مما ينبغي. ثم إذا كان أصغر مما ينبغي، أعرف أن عليَّ البحث في هذا الجزء من قائمتي. وإذا كان أكبر مما ينبغي، أحتاج إلى البحث في هذا الجزء من قائمتي. هذا ما تفعله if وelse الصغيرتان، صحيح؟

وعندما أتخذ قراري بشأن الجانب الذي أنظر إليه، أعيد ضبط نقطتي الطرف. وأنفّذ العملية من جديد بسؤال عنصر المنتصف هناك: هل أنت الذي أبحث عنه؟ وهكذا وهكذا. إذًا هذا يجري بحثًا بالتنصيف، يعرف أيضًا بالبحث الثنائي، عن العنصر في قائمة.

والطريقة الأخيرة للبحث عما إذا كان عنصر موجودًا في القائمة هي طريقة نفعلها بالفعل. إنها هذا السطر الصغير هنا، x in L، أي باستخدام الكلمة المفتاحية in. أسمي ذلك الدالة المدمجة (Built-In Function)، المعامل المدمج in (Built-In Operator).

حسنًا، لن يكون عادلًا لو طلبنا من Python فقط أن تحدد—أو أن تختار فقط عددًا عشوائيًّا وتسأل هل ذلك العنصر هو الذي نبحث عنه، حسنًا؟ بدلًا من ذلك ما سنفعله هو أخذ متوسط لكل واحدة من هذه الدوال الثلاث لجعل الأمر عادلًا فقط.

سنقول إذًا عندما نبحث عن عنصر في القائمة، سأقول إنني سأأخذ متوسط الحالة التي يكون فيها العنصر أول عنصر في القائمة. وقد أجده فورًا في هذه الحالة، أو ربما ليس فورًا في هذه الحالة.

متوسط تلك الحالة، زائد الحالة التي يكون فيها العنصر آخر عنصر في القائمة، وزائد الحالة التي يكون فيها العنصر الأوسط في قائمتي، صحيح؟ هكذا نغطي نوعًا ما جميع الاحتمالات فعلًا، نوعًا ما أفضل حالة (Best Case)، وأسوأ حالة (Worst Case)، وحالة وسطى، صحيح؟ ستُشغّل كل واحدة من هذه الدوال الثلاث مع وضع ذلك في الحسبان.

هذه إذًا الدوال الثلاث. هذه is_in ذات القوة الغاشمة لدي. وهذه البحث الثنائي. ومن الواضح أنني سأكتب فقط in عند تشغيلها. لذلك سأريكم واحدة منها فقط، سنزيل علامات التعليق عن هذا الجزء كله هنا ونشغّله.

لكن يمكنكم أن تروا هنا، بدلًا من تشغيل عدّاد الأداء واستدعاء الدالة مرة واحدة فقط، سأجري فعلًا ثلاثة استدعاءات للدالة، أكررها عبر هذه الحلقة الصغيرة، حيث أبحث عن العنصر في الموضع الصفري، والعنصر في موضع المنتصف، والعنصر في نهاية قائمتي. وآخذ متوسطها هنا في الأسفل فقط، الوقت الذي يستغرقه إيجاد تلك العناصر الثلاثة. هل هذا مفهوم؟

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

صفحة المصدر 7

إذًا أول دالة شغّلناها هي is_in. تذكروا أنها كانت ذات القوة الغاشمة. نمر بشق الأنفس على كل عنصر ونسأل هل هو الذي نبحث عنه. لا توجد طريقة ذكية في ذلك، القوة الغاشمة فقط حتى نصل.

نلاحظ أنه مع نمو قائمة المدخل بعامل 10، ينمو الوقت الذي يستغرقه إيجاد العنصر في القائمة، سواء كان الأول أو الأخير أو في المنتصف، في المتوسط بعامل 10 أيضًا.

حسنًا، تاليًا دعونا ننظر إلى الدالة المدمجة. سنهتم بالثنائية لاحقًا. الدالة المدمجة، أي استخدام معامل in فقط—وترون أن هذا كان في الأسفل هنا. هذه in. في الأساس، الدالة التي أشغّلها تسأل فقط هل x موجودة في L، وهذا يُرجع True أو False.

لذلك لم أحتج إلى إنشاء دالة لذلك. لكن معامل in المدمج ذاك له أيضًا اتجاه مشابه جدًّا، صحيح؟ مع زيادة طول قائمتي بعامل 10، يستغرق تشغيل برنامجي عشرة أمثال الوقت أيضًا. انتقلت من 0.05 إلى 0.5، وستكون التالية 5 وهكذا.

حسنًا، يبدو أن هاتين تفعلان تقريبًا النوع نفسه من—لهما الأداء نفسه. والآن ماذا عن البحث بالتنصيف أو البحث الثنائي؟ هذا ليس واضحًا جدًّا، صحيح؟

إذا نظرنا إلى المدخل، فالمدخل يزيد بوضوح بعامل 10 من هنا إلى هنا. والوقت يزيد من 9 × 10^-6 إلى 1.1 × 10^-5. لذلك العامل، كم مرة إضافية استغرق، غير واضح إطلاقًا، صحيح؟ ليس 1 تمامًا. لو كان 1، فسيعني ذلك أنه مستقل، صحيح؟ إنه ثابت (Constant). لا يهم حجم المدخل، سيعطينا دائمًا هذا المقدار—سيعمل دائمًا خلال هذا المقدار من الوقت. إذًا ليس 1 تمامًا.

لذلك هو شبه مستقل عن الحجم، لكنه ليس خطيًّا (Linear) كما كانت الدالتان الأخريان، صحيح؟ ليس 10 عندما ينمو المدخل بعامل 10. لذلك لسنا متأكدين تمامًا ما هذه الدالة. لكن بوضوح ليست سيئة مثل الاثنتين الأخريين، لكنها ليست بجودة عدم وجود علاقة، حسنًا؟

الملاحظة الرابعة ستقارن الآن الدالة التي كتبناها، is_in، هذه هنا، بالبحث الثنائي، هذا في الأسفل هنا. البحث الثنائي كان أسرع من القوة الغاشمة برتب مقدار كاملة (Orders of Magnitude)، صحيح؟ القوة الغاشمة، عندما كان المدخل—ما هذا، 10 ملايين أو 100 مليون؟ لست متأكدة. عندما كان المدخل 100 مليون، استغرقت القوة الغاشمة 1.6 ثانية.

لكن البحث بالتنصيف، البحث الثنائي، استغرق 0.00001 ثانية. ليس الأمر أننا انتقلنا من 1.5 ثانية إلى 1.2 ثانية أو 0.5 ثانية. كنا أسرع برتب مقدار كاملة، صحيح؟ 10^-5، صحيح؟

لذلك هناك فرق كبير جدًّا بين هذه الخوارزمية (Algorithm)، التي تشق طريقها بالقوة الغاشمة، وبين هذه الخوارزمية التي تفعل شيئًا ذكيًّا بإزالة نصف مساحة البحث (Search Space) في كل دورة، صحيح؟ حسنًا، من المهم معرفة ذلك.

وأخيرًا، بمقارنة الوقت البحت الذي تستغرقه هذه البرامج للتشغيل فقط، دعونا نقارن الدالة التي كتبناها، التي تمر على هذه القائمة عنصرًا في كل مرة، والدالة المدمجة in.

الدالة المدمجة in، مع أنها ما زالت من النوع نفسه—لها العلاقة الخطية نفسها بحجم المدخل، يبدو أن أداءها أفضل بكثير باستمرار، بسرعة نحو عشرة أمثال، صحيح؟ عندما استغرقت دالتنا 0.1 ثانية، استغرقت المدمجة 0.05 ثانية. وعندما استغرقت دالتنا ثانية واحدة، استغرقت المدمجة 0.5 ثانية. لذلك باستمرار من الأسرع استخدام دالة in المدمجة بدلًا من إنشاء دالتنا.

ملاحظة المترجم: يقول النص الرسمي هنا «نحو عشرة أمثال»، لكن المثالين المنطوقين 0.1 / 0.05 و1 / 0.5 يعطِيان عامل سرعة 2، لا 10. أُبقي كلام المصدر وأرقامه دون تصحيح صامت.

حسنًا، هل هناك أسئلة عن أي من هذه الملاحظات؟ هل هي مفهومة؟ هل هي مثيرة للاهتمام؟

صفحة المصدر 8

[ضحك]

حسنًا، ماذا نرى؟ مراجعة سريعة فقط لتلك الدوال الثلاث، صحيح؟ الأولى التي رأيناها خطية في حجم الوسيط (Argument). لذلك عندما يزيد حجم قائمة المدخل بعامل 10، يستغرق البرنامج عشرة أمثال الوقت للتشغيل. لكن هذه الأخرى شيء أقل من خطي، لكنه ليس ثابتًا. لذلك لسنا متأكدين تمامًا ما هو.

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

اسمها القطر (Diameter). وسأشرح ما تفعله لأنها تبدو مخيفة قليلًا. لكن افترضوا أن لدينا فعلًا بعض النقاط في مستوى ثنائي الأبعاد (2D Plane)، صحيح؟ يبدو إذًا هكذا.

في الأساس ما ستفعله هذه الدالة هو أنها ستحدد أكبر مسافة بين جميع هذه النقاط. المسافة بين هاتين النقطتين مقدار ما. والمسافة بين هاتين النقطتين مقدار آخر. أي نقطتين تعطيان أكبر مسافة؟ هذا ما تهدف هذه الدالة إلى إخبارنا به، وما مقدار تلك المسافة.

إذًا طريقة عملها هي أن لديها حلقات for متداخلة (Nested Loops). هذا مختلف عما رأيناه حتى الآن، صحيح؟ رأينا مثالًا على ذلك في المحاضرة الماضية. لكننا الآن نراه في سياق شيء مفيد فعلًا. في هذه الحالة بالتحديد سننشئ قائمة مدخلات، حسنًا؟

مرة أخرى سيكون في قائمة المدخلات لدينا بعض القيم الوهمية فقط. لا يهمني فعلًا ما هذه الأعداد. أريد فقط إنشاء مجموعة كبيرة من النقاط في مستوى ثنائي الأبعاد. لذلك ما سنفعله تقريبًا هو المرور على الأعداد من 0 إلى 10. آسفة، من 0 إلى 9، ومن 0 إلى 99، ومن 0 إلى 999 وهكذا، تمامًا كما فعلنا من قبل.

وللحصول على إحداثي صغير في المستوى ثنائي الأبعاد اعتمادًا على تلك الأعداد، سأأخذ جيب التمام (Cosine) لذلك العدد، فاصلة، الجيب (Sine) لذلك العدد. بحيث يكونان معًا، مثل جيب تمام 1 أو أيًّا كان، وجيب 1، هذه النقطة هنا، صحيح؟ تُنظّم في صف (Tuple).

ثم قد يكون هذا جيب تمام 5، وجيب 5. شيء من هذا القبيل. إذًا أنشئ فقط مجموعة كبيرة من الإحداثيات في مستوى ثنائي الأبعاد مع التأكد من أن لدي n إحداثيات. حسنًا، الآن الحلقة—آسفة، سيكون هناك حلقتان.

الحلقة الخارجية ستأخذنا في الأساس عبر كل واحد من هذه العناصر. لدي خمسة في هذه الحالة بالتحديد. والحلقة الداخلية ستمر على كل عنصر آخر. لكن لاحظوا أنها تبدأ من i + 1. وسأخبركم لماذا يكون ذلك بدلًا من البدء من 0.

دعونا نتتبع الأمر فقط. لنقل إننا نبدأ بهذا العنصر. هذا الصغير هو عنصرنا الأول في حلقة for الخارجية. الآن حصلنا على العنصر الأول في حلقة for الخارجية. وما سنفعله هو معرفة المسافة بينه وبين الجميع غيره. الآن نمر عبر حلقة for الداخلية، نمر على كل عنصر باستثناء نفسي.

سأحصل إذًا على المسافة بين هذا وهذا. بما أنه الأول، فهو بالطبع الأكبر. لكن بعد ذلك سأحصل على المسافة بين هذا وهذا، صحيح؟ وسأقول: هل أنت أكبر من هذه؟ يبدو أن الإجابة لا، لذلك ما زلنا نحتفظ بهذه بصفتها الأطول.

صفحة المصدر 9

ثم سأحصل على المسافة بين هذا وهذا، وبين هذا وهذا. وأثناء مروري، تتتبّع عبارة if الصغيرة هذه الأبعد. أي ما له أكبر مقدار (Magnitude)، صحيح؟ في هذه الحالة ربما تكون الأولى التي نظرنا إليها. وبعد أن أمر على كل عنصر، كل عنصر آخر، أكون قد أنهيت دورتي الأولى في حلقة for الخارجية.

الآن تنتقل حلقة for الخارجية إلى العنصر التالي في القائمة. لنقل إنه هذا. لا يهم فعلًا. حسنًا، هذا سينظر إلى المسافة بينه وبين الجميع غيره باستثناء الذي نظرنا إليه بالفعل، لأننا نعرف هذه المسافة بالفعل. تتبّعناها عندما مررنا على هذا.

إذًا أثناء مروري على حلقة for الخارجية وتتبع هذه النقطة هنا، تحدد المسافة بين هذا وهذا، التي أصبحت فجأة أكبر من تلك التي كنا نحتفظ بها، ثم هذا هنا. حسنًا، جيد. الآن ما زلنا نحتفظ بأكبر مسافة رأيناها. ربما تكون هذه هنا. وقد أنهيت الدورة الثانية في حلقة for الخارجية.

والآن أنتقل إلى العنصر التالي. لنقل إنه هذا. لا يهم أيضًا. الآن سيحصل هذا على المسافة بينه وبين الجميع غيره باستثناء الاثنين اللذين رأيناهما بالفعل، هذا وهذا، صحيح؟ لهذا تبدأ الحلقة الداخلية لدينا عند i + 1.

إذًا هذا سيحصل على المسافة بينه وبين هذا هناك في الخلف، وهذا هنا بعيدًا. ثم الدورة التالية في حلقة for الخارجية تنظر إلى هذا، لنقل. وتجد المسافة بينه وبين الجميع غيره. لكن أتعلمون؟ لم يبقَ إلا واحد، ذاك هناك.

ثم في المرة الأخيرة لا يحصل هذا حتى على فرصة لإيجاد المسافة بينه وبين أي شخص آخر، لأن الجميع غيره وجدوا بالفعل المسافة إليه.

وبهذه الطريقة نجد في الأساس جميع الأزواج الممكنة لجميع هذه النقاط في هذا المستوى ثنائي الأبعاد، ونتتبع أطول—أكبر مسافة، حسنًا؟ من حيث القائمة—قائمة المدخلات، الشكل الذي يبدو عليه ذلك، موضوع i + 1 هذا هنا، تقول الحلقة الخارجية في الأساس: سأبدأ بك، وسأحصل على الفرق بينك وبين العنصر عند الفهرس 1، والعنصر عند الفهرس 2، والعنصر عند الفهرس 3.

انتهت حلقة for الخارجية هذه. بعد ذلك سنحصل على الفرق—المسافة بين هذا والجميع غيره، صحيح؟ من الواضح ليس العنصر عند الفهرس 0 لأننا نعرف تلك المسافة بالفعل. لذلك سنحصل على المسافة بين العنصر عند الفهرس 1، والفهرس 2، والفهرس 3، ثم ننتهي.

ثم الحلقة الأخيرة—الحلقة الخارجية تحصل على المسافة بين العنصر عند الفهرس 2 والفهرس 3، ثم تنتهي. إذًا هاتان الحلقتان المتداخلتان فقط تفعلان كل هذا إلى أن تجد جميع هذه الأزواج. إنها في الأساس تُقرن الجميع بعضهم ببعض.

حسنًا، إذا شغّلناها، ماذا سنرى؟ ستلاحظون أولًا أن مدخلي لهذه الدالة بالتحديد سيكون أصغر بكثير، كثيرًا، من المدخلات لكل ما فعلناه حتى الآن، صحيح؟ بعض المدخلات التي رأيناها سابقًا كانت مليونًا و10 ملايين و100 مليون. في هذه الحالة بالتحديد لن أصل إلا إلى 6,000، لأن تشغيلها سيستغرق وقتًا طويلًا جدًّا إن جعلتها تصل إلى أكثر—أطول من ذلك.

ماذا نرى بالفعل؟ لدينا 100 نقطة. إذًا مئات من هذه، صحيح؟ إيجاد المسافة—إيجاد أكبر مسافة بين مجموعة من هذه الأزواج استغرق نحو 0.03 ثانية. إذا ضاعفنا ذلك إلى 200 نقطة، استغرق 0.01 ثانية. وإذا ضاعفنا ذلك إلى 400 نقطة، استغرق 0.05 ثانية، وهكذا وهكذا.

صفحة المصدر 10

كما من قبل تمامًا، دعونا ننظر إلى الأعداد الكبيرة لنرى الاتجاه. مع زيادة الأعداد بعامل 2، إذا زاد مدخلي بعامل 2، يبدو أن الوقت الذي يستغرقه تحديد أكبر مسافة يزيد بعامل 4. مدخلي يزيد بعامل 2، والوقت يزيد بعامل 4.

ولن أشغّل هذا، لكن يمكنكم إنشاء قائمة جديدة بأنفسكم وتغيير هذه لتكون المدخلات مضاعفات 10، صحيح؟ تزيد بعامل 10 في كل مرة. وسترون نمطًا مشابهًا جدًّا، حيث الوقت الذي يستغرقه تشغيل ذلك البرنامج سيكون أبطأ بنحو 100 مرة. إذًا العلاقة هناك علاقة من نوع n^2.

حسنًا، هناك بعض الملاحظات هنا أيضًا. الأولى ذكرتها بالفعل، وهي أن هذا البرنامج يستغرق وقتًا أطول بكثير للتشغيل عمومًا، صحيح؟ هنا كنا قادرين مع compound، ومع معرفة هل عنصر موجود في القائمة، والحصول على مجموع جميع العناصر في القائمة، كنا قادرين على تشغيل 100 مليون—قائمة فيها 100 مليون عنصر، ومع ذلك استغرق نحو ثانية وشيء.

بينما مع دالة القطر هذه بالكاد نستطيع الوصول إلى 6,000، وهي تستغرق بالفعل 14 ثانية. إذًا برنامج أبطأ بكثير، كثيرًا، كثيرًا، عمومًا، صحيح؟ ثم يبدو أن العلاقة من نوع n^2، تربط المدخل بالمدة التي يستغرقها البرنامج.

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

إذا رسمنا المدة التي يستغرقها البرنامج للتشغيل عندما يزيد المدخل—آسفة، عندما يكون المدخل بهذا الحجم، يمكننا رؤية أن هناك علاقة خطية. إذًا الوقت الذي يستغرقه تشغيل البرنامج خطي في المدخل.

القطر، سنتحدث عن البحث الثنائي بعد قليل. لكن القطر، مرة أخرى لاحظنا هذا بالنظر إلى الأعداد البحتة فقط، لكن من الأسهل كثيرًا رؤيته بصريًّا عندما يكون هذا على المحور x، حجم المسألة. أي بين كم نقطة نجد القطر فعلًا، والمدة التي يستغرقها البرنامج فعلًا للتشغيل. مرة أخرى العلاقة تربيعية (Quadratic). الآن بعدما رسمناها نرى العلاقة التربيعية بوضوح.

ثم هذا البحث الثنائي، لم نكن متأكدين إطلاقًا ما هو، صحيح؟ لم يكن ثابتًا تمامًا. وبالتأكيد لم يكن خطيًّا. لكن الآن بعدما رسمناه، هذا حجم المدخل وهذه المدة التي يستغرقها البرنامج فعلًا للتشغيل، يمكنكم أن تروا أنه يزيد بشدة عندما يكون حجم المدخل صغيرًا جدًّا.

لكن بعد ذلك يقترب نوعًا ما تقاربيًّا (Asymptotically) من قيمة ما. إنها في الحقيقة علاقة لوغاريتمية (Logarithmic). حسنًا، آخر شيء أردت ذكره عن التوقيت قبل الانتقال إلى العدّ هو التشغيل البحت. مجرد تشغيل هذه الدوال على حواسيب مختلفة سيعطينا قيمًا مختلفة منذ البداية.

على هذا الحاسوب الحديث نوعًا ما، كم استغرق تشغيل compound هذه؟ ماذا؟ استغرق 3 ثوانٍ أو شيء كهذا، ثانية وشيء. على حاسوب محمول أقدم استغرق 63 ثانية. وعلى حاسوب مكتبي أقدم حتى من ذلك، استغرق 126 ثانية. أنتم توقّتون الأشياء فقط. الجهاز الذي تشغّلونها عليه سيحدث فرقًا، حسنًا؟

وهذا مقبول، صحيح؟ من المهم معرفة المدة التي يستغرقها. لكن إن كنتم تنظرون إلى العلاقة بين المدخل والمدة التي يستغرقها البرنامج فقط، فهي نفسها. لذلك لا يهم الجهاز الذي تشغّلونه عليه. عندما تزيدون المدخل بعامل 10، سيستغرق البرنامج عشرة أمثال الوقت للتشغيل، سواء شغّلتموه على حاسوب محمول سريع، أو محمول قديم، أو مكتبي قديم جدًّا.

ملاحظة المترجم: الوصف الشفهي «يقترب تقاربيًّا من قيمة» لا يعني أن log n محدودة بحد ثابت؛ الدالة اللوغاريتمية تستمر في النمو، لكن ببطء. نُقل الوصف كما ورد في المصدر.

صفحة المصدر 11

إذًا مجرد توقيت برنامج مهم جدًّا، صحيح؟ تريدون معرفة هل البرنامج الذي كتبتموه ستحتاجون إلى الانتظار شهرًا حتى ينتهي، أم دقيقتين حتى ينتهي. هذا شيء مهم معرفته.

لكن ما سنصل إليه قرب نهاية هذه المحاضرة هو شيء مكمّل، وهو فكرة التعقيد التقاربي (Asymptotic Complexity). أي نوعًا ما القول رياضيًّا: أتعلمون؟ تشغيل هذا البرنامج لن يكون سيئًا إلى ذلك الحد، صحيح؟ لن تحتاجوا إلى انتظار تشغيله لأشهر، دون تشغيله فعلًا بالطبع.

ستكونون قادرين على إلقاء نظرة على برنامج والقول إن تشغيل هذا معقول. وسنفعل ذلك من حيث فكرة رتبة النمو هذه، التي سنصل إلى—إليها بعد قليل. حسنًا، هل هناك أسئلة عن التوقيت قبل الوصول إلى العدّ؟ نعم.

أحد الحضور (AUDIENCE):

هل ينبغي أن نفترض أن جميع الدوال المدمجة في Python هي الأمثل من حيث زمن التشغيل؟

آنا بيل (ANA BELL):

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

حسنًا، الآن ما سنفعله هو عدّ العمليات، كما فعلنا في المحاضرة الماضية تمامًا. من الواضح أن التوقيت لطيف، لكنه لا يعطينا علاقة لطيفة غير أن نلاحظها نحن، صحيح؟ لا توجد صيغة. لا توجد علاقة تربط المدخل بالمدة التي يستغرقها البرنامج للتشغيل، صحيح؟

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

عندما نشغّلها، نعم، قد تعمل لمدد زمنية مختلفة. 1 × 10^-9 مقابل ضرب 10^-9، شيء من هذا القبيل. هذا ليس فرقًا كبيرًا جدًّا. لذلك ما سنقوله هو أننا سنعتبر كل واحدة من هذه العمليات ثابتة، صحيح؟ ستستغرق وحدة زمن واحدة.

إذا قلنا ذلك، يمكننا فعلًا الوصول إلى علاقة تخبرنا بحسب—تربط المدخل بعدد العمليات التي سيشغّلها هذا البرنامج. هنا في التحويل إلى كيلومترات ماذا لدينا؟ لدينا عملية ضرب واحدة.

ولمجرد التجربة، في هذه المحاضرة دعونا نقول إن return تُعدّ أيضًا عملية. إذًا في دالة التحويل إلى كيلومتر هذه لدينا عمليتان. لاحظوا أن ذلك غير مرتبط بالمدخل إطلاقًا. لذلك مقدار العمليات التي يستغرقها هذا البرنامج للتشغيل هو دائمًا اثنتان. يوافق ذلك ما قاله توقيتنا، صحيح؟ في الأساس لم يكن مهمًّا ما المدخل. كان يستغرق دائمًا الوقت نفسه تقريبًا للتشغيل.

دالة sum_of. تأخذ قائمة مدخلات وتحصل على مجموع جميع العناصر. هذه ستفعل—سيكون لدينا عملية واحدة لإجراء هذا الإسناد (Assignment). وستكون لديها عملية واحدة للحصول على عنصر في قائمتي L وإسناده إلى i.

صفحة المصدر 12

وسيكون لديها عمليتان لهذا total += i. تذكروا أن total + i على الجانب الأيمن عملية واحدة. وtotal تساوي ذلك هي عمليتي الثانية، إذًا عمليتان. ثم لا ننسَ حلقة for. إنها الجزء المهم نوعًا ما من هذه الدالة. كم مرة ستتكرر هذه العمليات الثلاث، هذه الواحدة زائد هاتين الاثنتين؟ ستتكرر بعدد العناصر التي لدي في L. إذًا عدد مرات يساوي طول L.

ثم مرة أخرى لنقل إننا نعدّ return. ستكون return أيضًا عملية واحدة. إذًا العدد الإجمالي للعمليات لدالة sum_of سيكون 1 من أجل total = 0، زائد طول L ضرب 3، لأن هناك ثلاث عمليات تُنفّذ لكل عنصر من طول L، زائد واحدة أخرى من أجل return. لذلك ستكون 3 * len(L) + 2.

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

هذه دالتنا is_in. ستعدّ عدد العمليات التي لدينا. وسأستخدم شيئًا يسمى متغيرًا عامًا (Global Variable). سأريكم مرة أخرى الفرق بينهما. إنها فقط هذه الأسطر الثلاثة التي أضفتها.

وينبغي ألا تستخدموا أبدًا المتغيرات العامة في برامجكم إلا في هذا الموقف. فكرة المتغيرات العامة هي أنه يمكنكم تعريف متغيرات في البرنامج الرئيسي فقط خارج أي دوال. ويمكنكم الوصول إلى تلك المتغيرات داخل دالة ما، بمجرد القول، إذا عرّفنا count هنا في الخارج، قبل تعريف هذه الدالة، count = 0 أو أيًّا كان، داخل أي دالة يمكننا القول: يا Python، أود الوصول إلى هذا المتغير الذي عرّفته خارج هذه الدالة. تقولون ذلك—تخبرون Python باستخدام global ثم اسم ذلك المتغير.

وستجلب Python ذلك المتغير الذي هو في الأساس «مشترك»، بين علامتي اقتباس، عبر البرنامج كله، وتعدّل ذلك المتغير. لذلك في الجوهر نقول أساسًا إن هذا أصبح متغيرًا مشتركًا. إذا عدّلته داخل هذا العدّاد، داخل هذه الدالة، فسيكون بالطبع معدّلًا لكل شيء آخر.

استخدام المتغيرات العامة مغرٍ جدًّا لأن كل المتغيرات التي قد ترغبون في إنشائها ستكون متاحة للجميع، صحيح؟ لا حاجة إلى تمرير معاملات. لا حاجة إلى فعل—لكنها برمجة سيئة جدًّا، جدًّا. لذلك لن نفعلها قط إلا في هذه الحالة بالتحديد، لأننا نود الاحتفاظ بعدّاد لأشياء تحدث، أو لأغراض التنقيح (Debugging) وأشياء من هذا القبيل.

إذًا متغير count سيتتبّع—سيزيد فقط في أماكن رئيسية تحدث فيها وحدات الزمن الثابتة هذه. لدي count += 1 هنا لأن لدي قيمة return. ولدي count += 2 هنا لأنني أجلب عنصرًا من L وأجري فحص المساواة هنا، ثم هذا كل شيء، حسنًا؟

إذا شغّلت ذلك، فهو في الأسفل هنا. ماذا سنرى؟ لم أحسب فعلًا مقدار الزيادة في التشغيل، لكن يمكننا رؤية العلاقة، صحيح؟ ننتقل من 9 إلى 37 إلى 307 إلى 3007 إلى 30,007 وهكذا. مرة أخرى العلاقة نفسها حيث نزيد المدخل بعامل 10. وعدد العمليات التي ننفّذها أكبر بعشر مرات، تمامًا كما قالت الصيغة إنه سيكون.

ماذا عن البحث الثنائي؟ مرة أخرى سنستخدم هذا المتغير العام وسنجعل العدّاد يتتبع جميع هذه العمليات. هذه الزيادة في count بمقدار 3 تحسب ضبط low إلى صفر، وضبط الحد الأعلى لهذا الشيء، والحصول على قيمة الطول الفعلية هذه.

صفحة المصدر 13

زيادة count داخل حلقة while هذه ستتتبّع هذا الطرح بوصفه عملية واحدة، واختبار أنه أكبر من 1 عملية أخرى. وزيادة العدّ بمقدار 3 هنا تحسب high + low، والقسمة الصحيحة، وإسناد تلك القيمة مرة أخرى إلى mid.

count زائد 3 هنا تحسب الفهرسة داخل هذه L، وفحص الأصغر من أو يساوي. ثم إما إعادة إسناد low هذه أو إعادة إسناد high هذه. إذًا ثلاث عمليات. وأخيرًا تزيد count بمقدار 3 مرة أخرى لأن لدي هذه العمليات هنا. أي الفهرسة عند low، وفحص المساواة، ثم تنفيذ return.

إذًا سيجري تتبع العدد الفعلي للعمليات في—بواسطة العدّاد. كل ما نفعله هو نوعًا ما عرض عدد المرات—عدد العمليات التي نفّذناها. مع زيادة المدخل بعامل 10، كما في التوقيت تمامًا، لا يمكننا تحديد العلاقة تمامًا، صحيح؟ مرة أخرى هي نحو 1 وشيء، صحيح، مع كل زيادة.

حسنًا، هذه هي النتائج. الملاحظة الأولى، كما ذكرت، عندما نزيد المدخل بعامل 10، فهذه القوة الغاشمة—مسحتها، لكن دالة is_in ذات القوة الغاشمة تنفّذ أيضًا عدد عمليات أكبر بعشر مرات. أما البحث الثنائي، مرة أخرى لا نعرف بأي معدل ينمو، لكن يمكننا رسمهما.

هنا لدي الرسوم، كما عندما رسمت حجم المدخل مقابل المدة التي استغرقها البرنامج فعلًا للتشغيل. الآن أرسم حجم المدخل مقابل عدد العمليات المنفّذة فقط فعلًا. إذًا دالة is_in، تلك الطريقة ذات القوة الغاشمة لمعرفة هل عنصر موجود في القائمة، تنمو خطيًّا. لا مفاجأة هنا.

ويا لحسن حظنا؟ البحث الثنائي يطابق—الرسم يطابق الذي كان لدينا للتوقيت، صحيح؟ مع زيادة الحجم لدي في أسلوب البحث الثنائي، يكون عدد العمليات التي أنفّذها لوغاريتميًّا في الزمن، كما رأينا في الوقت الفعلي تمامًا.

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

ربما لاحظتم—تناولت ذلك بإيجاز—أننا طوال هذه المحاضرة كلها والمحاضرة الماضية لم نرَ أساسًا إلا شيئًا مثل ثلاث خوارزميات مختلفة، صحيح؟ رأينا شيئًا ثابتًا، وشيئًا خطيًّا، وشيئًا تربيعيًّا، وشيئًا ثنائيًّا، أو لوغاريتميًّا في هذه الحالة بالتحديد. إذًا هذه أربع خوارزميات مختلفة. لكننا رأينا عددًا أكبر بكثير من الدوال تعمل، صحيح؟

لذلك ما نود فعله هو تقييم الخوارزميات، لا التنفيذات المختلفة (Implementations)، صحيح؟ وما نود فعله هو تقييم هذه الخوارزميات عندما يصبح المدخل كبيرًا جدًّا، جدًّا. لذلك ما سنفعله هو تحديد علاقة بين زمن تشغيل البرنامج والمدخل.

لكن ما سنفعله هو التركيز على أكبر الحدود (Terms) التي تسهم في زمن تشغيل البرنامج، صحيح؟ رأينا هذه الأمثلة في المرة الماضية، صحيح؟ هذه mysum، التي جمعت في الأساس جميع العناصر—آسفة، جميع الأعداد من 0 إلى x. ودالة square السخيفة التي كانت لها حلقات متداخلة، مثل دالة القطر هذه نوعًا ما، صحيح؟

كنا قادرين على قول شيء مثل إنه عندما يزيد المدخل بعامل 10، يستغرق البرنامج عشرة أمثال الوقت للتشغيل، صحيح؟ إذًا كانت كفاءة ذلك البرنامج من رتبة x. عندما زاد المدخل بعامل x، استغرق البرنامج x أمثال الوقت للتشغيل. وكان لـsquare شيء مشابه—كان يمكننا قول ذلك بطريقة مشابهة، صحيح؟

صفحة المصدر 14

عندما يزيد المدخل بعامل x، يستغرق البرنامج x^2 أمثال الوقت للتشغيل. لذلك لا أهتم فعلًا بكل هذه الفروق في الأوقات الدقيقة. 1 × 10^-6. و1.3 × 10^-6. لا أهتم.

ما أهتم به هو رتبة النمو. كيف يعمل البرنامج بالنسبة إلى المدخل، حسنًا؟ وأهتم بذلك عندما يكون المدخل كبيرًا جدًّا، جدًّا. لذلك ما سنفعله هو التعبير عن زمن تشغيل البرنامج بعلاقة من نوع «من رتبة»، لا بعلاقة دقيقة.

لذلك مع أن العدّ كان لطيفًا جدًّا، فقد أخبرنا بعلاقة لطيفة بين المدخل وعدد العمليات. عندما يكون المدخل كبيرًا جدًّا، جدًّا، مثل 3x + 4، عندما تكون x كبيرة جدًّا، جدًّا، لا أهتم بأن عدد العمليات 3x + 4. لأن x عندما تكون كبيرة جدًّا، جدًّا، فقد يكون ذلك الزائد 4 كأنه زائد 0. و3x تلك هي في الأساس مثل x عندما يكون المدخل كبيرًا جدًّا، جدًّا. هذا إذًا ما سنحاول فعله، حسنًا؟

والآن قبل فعل ذلك، نحتاج إلى تحديد ما نقيسه، صحيح؟ لأننا عندما نكتب دوال ستكون لدينا دوال قد تحتوي على مجموعة كبيرة من المدخلات، صحيح؟ فقد يكون المدخل عددًا صحيحًا، كما في التحويل إلى كيلومترات. وقد يكون قائمة، وفي هذه الحالة سنهتم ربما بطول القائمة. وإذا كان لديكم معاملات كثيرة، فعليكم اتخاذ قرار، صحيح؟ ما المعامل الذي يسهم في نمو هذه الدالة؟

هذا مثال. هذه دالتنا is_in. تبحث عن عنصر e في قائمة L، صحيح؟ إذًا هناك معاملان لهذه. يمكن أن نسأل: هل يستغرق البرنامج وقتًا أطول للتشغيل مع زيادة e؟ إنها أحد المعاملات. دعونا نرى ما يحدث عندما نجعل e أكبر.

يمكننا النظر إلى مثال صغير. إذا عرفنا هل 0 موجود في هذه القائمة التي تحتوي على 1 و2 و3، أو هل 1,000 موجود في القائمة 1 و2 و3، هل يستغرق البرنامج وقتًا أطول للتشغيل؟ لا، بالضبط. إذًا e ليست ذات صلة فعلًا بحساب زمن التشغيل لدي هنا.

دعونا ننظر إلى L الآن. عندما نقول إن L ستتغير، يمكن أن تتغير بطريقتين، صحيح؟ قد تكون للعناصر في L قيم مختلفة، أو قد يكون طول القائمة نفسه مختلفًا. في هذه الدالة بالتحديد، لنقل إن العناصر في L أعداد صغيرة مقابل أعداد كبيرة، حسنًا؟ هذا بالتأكيد شيء يمكن أن يحدث. وبالتأكيد مع بعض الدوال سيحدث ذلك فرقًا. لنقل في هذه الدالة بالتحديد، إذا كانت العناصر في L كبيرة مقابل صغيرة، هل سيؤثر ذلك في زمن التشغيل لدي؟ هذا مثال صغير.

لنقل إنني أبحث عن العدد 0 داخل قائمة فيها 1 و2 و3، وعن العدد 0 داخل قائمة فيها 1,000 و2,000 و3,000. هل سيحدث ذلك فرقًا؟ لا، صحيح؟

إذًا حجم العناصر نفسها لا يهم فعلًا. وشيء أخير نسأل أنفسنا عنه هو: ماذا عن طول القائمة؟ إذا كانت لـL أطوال مختلفة، هل سيحدث هذا فرقًا في زمن تشغيلنا؟ إذا كنت أبحث عن صفر في قائمة فيها ثلاثة عناصر، أو عن 0 في قائمة فيها 10 عناصر، حيث من الواضح أن 0 غير موجودة في أي مكان، فهل تلك القائمة المترابطة (Linked List) هي التي ستحدث فرقًا؟ نعم، في هذه الحالة سيفعل. بالضبط.

ملاحظة المترجم: يرد لفظ linked list في النص الرسمي هنا، لكن السؤال والفقرة التالية يتناولان أثر طول القائمة len(L)، لا تقديم بنية بيانات القائمة المترابطة. أُبقي اللفظ ووُضح الالتباس منفصلًا.

إذًا هنا في هذه الدالة بالتحديد، المدخل الذي سأهتم بعرض زمن التشغيل بالنسبة إليه هو طول القائمة. لا العناصر في القائمة، ولا e نفسها، بل طول القائمة، حسنًا؟

صفحة المصدر 15

آخر شيء سأذكره هو أننا في هذا المقرر بالتحديد سنتحدث عن سيناريو أسوأ حالة. ربما لاحظتم في هذا المثال السابق هنا أنني بحثت دائمًا عن عنصر غير موجود أصلًا في القائمة. عندما تواجهون دالة، اسألوا أنفسكم، في هذا المقرر بالتحديد على الأقل، ما سيناريو أسوأ حالة؟ وفي معرفة هل دالة موجودة في قائمة، تكون أسوأ حالة لنا هي أنها ليست في القائمة إطلاقًا، صحيح؟

ملاحظة المترجم: لفظ المصدر هنا هو «دالة» (Function) عند وصف البحث في قائمة؛ المقصود بحسب سياق المثال هو «عنصر» (Element). أُبقيت الزلة في كلام المصدر ووُضحت هنا منفصلًا.

إذًا هذا جانب آخر من زمن التشغيل لا نفعله فعلًا—لن نتحدث عنه. لأننا نهتم دائمًا بأسوأ حالة. لكن هناك تحليلات معينة يمكن أن تنظروا فيها إلى سيناريو أفضل حالة، وهو أن العنصر أول عنصر في القائمة، صحيح؟ في تلك الحالة ستجدونه دائمًا فورًا، لذلك هو ثابت. أو سيناريو الحالة المتوسطة (Average Case)، وهو نوعًا ما ما يفعله الناس في العالم الحقيقي، صحيح؟ لا تصادفون أسوأ حالة دائمًا.

لكن بالنسبة إلينا سننظر إلى سيناريو أسوأ حالة. سيكون هدفنا إذًا وصف كيف ينمو زمن التشغيل مع نمو حجم المدخل بطريقة عامة جدًّا. لذلك لن نهتم بتحديد عدد العمليات الدقيق. لا شيء من نوع 3x + 2 هنا. سنركّز فقط على الحدود التي هي فعلًا—التي تنمو بأسرع معدل. وسنحذف أي ثوابت جمع أو ضرب وأشياء من هذا القبيل. لذلك سنركّز فقط على الحدود الأسرع نموًّا، وهذا سيعطينا رتبة النمو.

إذًا الطريقة التي سنرمز بها إلى رتبة النمو هي استخدام هذا الترميز المسمى O الكبيرة (Big O) وثيتا الكبيرة (Big Theta). حسنًا، الآن، تحذير، ستأتينا بعض الرياضيات. ستكون نحو ثلاث شرائح من الرياضيات البحتة فقط، حسنًا؟ يمكنكم الاسترخاء. لن تحتاجوا إلى معرفتها. لن تحتاجوا إلى معرفة التفاصيل. لكنها ستوفر لنا الدافع لتعطينا فكرة عن رتبة النمو التقاربية هذه.

حسنًا، هذا هو التعريف الرياضي لـO الكبيرة. ما نود فعله—سيكون هناك رسم، لا تقلقوا. ما نود فعله هو تحديد حد أعلى (Upper Bound) لدالتنا. قد تبدو الدالة هكذا. وأعرف أن هذه مجرد f، لكننا نربط ذلك بمقررنا بالقول: أتعلمون؟

إذا أجرينا حساب الرتبة—آسفة، تحليل عدد العمليات لدالة، يمكننا أساسًا الوصول إلى شيء كهذا، صحيح؟ وصلنا إلى 3x + 2. ويمكن أن نصل إلى 3x^2 + 20x + 1 لدالة عشوائية ما كتبناها، صحيح؟ لذلك نعتبر تلك هي الدالة.

والآن O الكبيرة ستكون الحد الأعلى لهذه الدالة. إذا رسمت هذه الدالة على محوري x وy، فهذا ما تبدو عليه. ستكون O الكبيرة دالة أخرى ستضع حدًّا أعلى لهذه، الزرقاء، حسنًا؟ وستحدّها من الأعلى لكل القيم بعد x ما.

إذًا لكل القيم بعد x ما—عدد ما على المحور x، نقطة تقاطع ما، هذه O الكبيرة لـg—هذه g ستكون دائمًا أكبر من f لدي. هذه هي الفكرة هنا. إذًا من الواضح أن x لن تحدّها من الأعلى، صحيح؟ لأن x بعد نقطة التقاطع هذه ستكون تحت دالتي مهما كان الثابت الكبير الذي ألصقه بتلك x. يمكن أن تكون لدي 1,000x. مع ذلك لن تحدّ خطي الأزرق الصغير هنا من الأعلى.

لذلك ما سنفعله هو زيادة الأس (Exponent) هناك. لنأخذ x^2. إنها تقترب. يبدو أن كلتيهما تربيعية. لكن هذا الخط البرتقالي ليس فوق الخط الأزرق لنقطة تقاطع ما 0.2. 2x، تقترب. 3x، تقترب. 4x حد أعلى لـg لدي؛ لأنه بعد نقطة التقاطع الصغيرة هذه هنا عند نحو 20، سيكون خطي البرتقالي، g، دائمًا فوق خطي الأزرق، f. هل الأمر جيد حتى الآن؟ بصريًّا فقط. نعم.

ملاحظة المترجم: يذكر النص الرسمي المستخرج في الفقرة السابقة 2x و3x و4x، ثم يتضح من متابعة الشرح أن المقارنة المقصودة بين 2x^2 و3x^2 و4x^2. كذلك يقول «حد أعلى لـg» أثناء وصف حد أعلى لـf. أُبقيت صيغ الكلام كما في النص مع بيان المقصود، لا بوصف هذه الزلات معادلات صحيحة.

أحد الحضور (AUDIENCE):

هل البرتقالي تحته هناك، أم لا يهم ذلك؟

صفحة المصدر 16

آنا بيل (ANA BELL):

نعم، البرتقالي تحته. لا بأس بذلك إطلاقًا. لأن ما نهتم به هو السلوك عندما يكون المدخل كبيرًا جدًّا. لذلك لا أهتم بالأشياء الغريبة التي تحدث هناك في الأسفل. كل ما أهتم به هو عندما تكون x لدي كبيرة للغاية.

حسنًا، وجدت الآن هذه g. يمكنني القول إنه بعد هذه النقطة، 20، سيكون خطي البرتقالي دائمًا فوق الأزرق. لذلك يمكنني القول إن f لدي هي O الكبيرة لـg—هي O(x^2)، حسنًا؟ لأنني لا أهتم بهذا الأربعة كثيرًا، لأنه مجرد ثابت ضربي (Multiplicative Constant). لأن 4x هذه أكبر دائمًا من دالتي لجميع x الأكبر من نقطة التقاطع هذه هنا.

هذا كل شيء. هذا هو التعريف، صحيح؟ إذًا g هنا هي في الأساس هذه الدالة دون الثابت الضربي أمامها. حسنًا، أقول إن 3x^2 + 20x + 1 هي O(x^2). وبوجه عام—كان ذلك مجرد مثال. بوجه عام O الكبيرة حد أعلى لدالتي، حسنًا؟

وهذا الآن يستخدم فقط متغيرات مثل الثوابت وأشياء من هذا القبيل. لكنه الموقف نفسه تمامًا الذي كان لدينا من قبل، حسنًا؟ لذلك سأحاول مطابقة الأزرق بالأزرق، والبرتقالي بالبرتقالي، والبنفسجي بالبنفسجي، لمساعدتكم على مطابقة ما رأيناه في الشريحة السابقة.

في الأساس نقول إن دالتنا f هي O الكبيرة لهذه g البرتقالية إذا استطعنا إيجاد ثابت أزرق ما، حيث كان هذا الثابت هو هذا الأربعة هنا، بحيث يكون هذا الثابت مضروبًا في g، أي x^2، أكبر من دالتي لجميع القيم بعد نقطة التقاطع هذه، صحيح؟ وجدت الأربعة لدي لأن 4x^2 أكبر دائمًا من دالتي بعد 21. هذا ما رأيناه في الصورة.

بعد ذلك يمكننا القول إن دالتي f هي O الكبيرة لـg(x)، حيث g هي x^2، وقد طابقناها، حسنًا؟ من حيث الصورة هنا، هذا نوعًا ما تكبير بسيط لما يحدث. يمكن أن يحدث أي شيء هنا في الأسفل. لكن بعد نقطة التقاطع، الموجودة هنا في الصورة الكبيرة، تلك النقطة—بعد نقطة التقاطع تلك، البرتقالي لدي دائمًا أكبر من الأزرق.

حسنًا، ماذا يعني هذا؟ سنتحدث عن ذلك بعد بضع شرائح. لكن ربما فكرتم في هذا. أستطيع فعلًا اختيار أي دالة تنمو أسرع من—ما هذه؟ 3x^2، صحيح؟ أستطيع اختيار مضروب x (Factorial)، أي x!. مضروب x ينمو بسرعة هائلة. أو 2^x. هذا ينمو بسرعة هائلة أيضًا. جميع هذه الدوال التي تنمو أسرع بكثير من دالتي هي أيضًا حدود عليا لهذه الدالة، صحيح؟

حسنًا، هذه O الكبيرة. إنها حد أعلى فقط. فما ثيتا إذًا؟ للسبب الذي ذكرته للتو، قلت x^2—أو آسفة، x!، و2^x، كل هذه الدوال التي تنمو أسرع بكثير من دالتي هي كلها حدود عليا. وهذا ليس مفيدًا جدًّا لنا عندما نقول: هذه الدالة هي O الكبيرة لأي شيء، صحيح؟ لأنكم تستطيعون اختيار شيء سريع بشكل سخيف—ينمو بسرعة سخيفة، والقول ذلك لا يحمل معنى.

لذلك بدلًا من ذلك، ما نعرضه عادة هو ثيتا، التي هي فعلًا حد أعلى وحد أدنى (Lower Bound) لدالتنا، حسنًا؟ باستخدام الاستدلال نفسه تمامًا سنجد ثابتًا ما ملصقًا بـg(x) تلك، بحيث تنمو تلك الدالة—تكون دائمًا تحت دالتنا، حسنًا؟

مرة أخرى سأعرض كثيرًا من الرياضيات. لكن في الأساس هذان السطران الأولان هنا، هذا هنا: يوجد، كذا وكذا وكذا وكذا وكذا، ذلك الأول هنا، هذا تعريف O الكبيرة. لذلك نعرف بالفعل ما يعنيه. وكل ما سنفعله هو إضافة شرط آخر، وهو أن نستطيع إيجاد ثابت آخر لـg نفسها، بحيث تكون تلك الدالة بعد نقطة تقاطع ما دائمًا تحت خطي الأزرق.

صفحة المصدر 17

هذا مثال. 4x^2، رأينا أنها نمت أسرع من 3x^2 بعد نقطة تقاطع. يمكن أن نقول إن 2x^2 ستنمو دائمًا أبطأ بعد نقطة التقاطع الخاصة بها. إذًا الثابت 4 هو نفسه الذي رأيناه من قبل، لكن هذا الثابت 2 يصبح الآن حدًّا أدنى، صحيح؟

أحاول في الأساس أن تجعل g نفسها حدًّا أعلى وحدًّا أدنى لدالتي الزرقاء، حسنًا؟ وهذا هو تعريف ثيتا. لذلك الآن لم يعد يمكنني القول إن 2^x الأسية (Exponential) تحدّها من الأعلى ومن الأسفل معًا، لأن 2^x تلك ستنمو أسرع من دالتي مهما كان الثابت الذي ألصقه بها، حسنًا؟

إذًا ما نراه الآن هو أن g(x) ستكون في الحقيقة الحد الذي ينمو بأسرع معدل. ستكون ذلك الحد هنا فقط، صحيح؟ ستكون الشيء دون—أسرع حد نموًّا في دالتي دون الثابت الذي وراءه.

حسنًا، نعم، لن نتذكر كل ذلك أبدًا، لكننا سنجري مجموعة من التمارين وسترون مدى سهولة تحديد رتبة النمو، حسنًا؟ لكن سأذكر هذا، مرة أخرى فقط، لأنه مهم جدًّا.

عندما نتحدث عن الحدود العليا يمكنكم اختيار أي دالة تنمو أسرع من دالتكم، صحيح؟ f(x)، هذا الشيء الذي فيه 3x^2، هو O(x^2). نعم، لكنه أيضًا O(x^3)، وO(x^5)، وO(x^100)، و2^x وx!، كل تلك الأشياء التي تنمو أسرع بكثير.

لكن لـf(x) لدي ثيتا واحدة فقط، وهي Θ(x^2)، صحيح؟ وهذا هو الحد الذي ينمو بأسرع معدل في دالتي هنا. لذلك عندما ننظر إلى دالة، اعتمادًا على عدد العمليات أو مهما كانت الطريقة التي تُعطى بها الدالة، عندما ننظر إلى رتبة نمو الدالة نركز فقط على الحد المسيطر (Dominant Term)، صحيح؟

في الأولى، المدخل هنا n. والدالة n^2 + 2n + 2. أي من هذه الحدود هو الحد المسيطر؟ أخبروني أنتم. نعم، بالضبط، n^2. إذًا ستكون هذه الدالة Θ(n^2) فقط. هذا كل شيء. ماذا عن التالية؟ ما الحد المسيطر هنا؟

نعم، بالضبط، 3x^2. رغم أن 100,000x ستكون ضخمة لبعض الوقت، وأن هذا الثابت سيكون أيضًا ضخمًا لبعض الوقت، عندما تصبح x كبيرة جدًّا، جدًّا، فإن 3x^2 هذه، وفي الحقيقة x^2 وحدها، ستتغلب نوعًا ما على كل شيء آخر، صحيح؟ إذًا التالية هي Θ(x^2).

ماذا عن التالية؟ ما الحد الأسرع نموًّا هنا؟ نعم، بالضبط. اللوغاريتم (Log) أبطأ نموًّا، صحيح؟ إذًا ثيتا هذه الدالة هي Θ(a) فقط. لاحظوا أن ما نفعله هنا هو التركيز على الحد المسيطر فقط. سنحذف الثوابت الضربية، ونحذف كل حد آخر، ونعبّر عن ثيتا بالنسبة إلى المدخل.

لا تستخدموا Θ(n) طوال الوقت فحسب، صحيح؟ في المثال السابق من المغري القول إن الأولى Θ(n^2)، والتالية Θ(n^2)، والأخيرة Θ(n). لكن n ليست دائمًا مدخل دالتكم، صحيح؟ إن كانت كذلك، رائع. وإن لم تكن، فعليكم دائمًا التعبير عنها وفق مدخل الدالة. ربما يكون طول L. وربما يكون شيئًا آخر.

حسنًا، دعونا نجعلكم تجربون مثالين آخرين. ما ثيتا الأولى هنا؟ ما الحد الأسرع نموًّا؟ نعم، Θ(x). التالية. n^3. بالضبط، Θ(n^3). ترون، قلت لكم إن هذا سيكون سهلًا جدًّا. أعرف أن تلك الرياضيات كانت مخيفة. ماذا عن التالية؟

صفحة المصدر 18

هذا هو الحد الأسرع نموًّا، لكن بعد ذلك ثيتا هي—احذفوا أي ثوابت ضربية، وتصبح Θ(y) فقط. الأخيرة ستكون خادعة. ما ثيتا إذا كان المتغير b فقط؟ نعم، 2^b. ماذا إذا كان المتغير a فقط؟ a^3. بالضبط. وإذا كانت دالتي دالة في كل من a وb؟

وa + a^3، صحيح؟ لأن كليهما يسهم في زمن تشغيل هذه الدالة، صحيح؟ ليس b فقط. لذلك إذا كانت هذه الدالة، أيًّا كانت هذه الدالة المجنونة التي كتبتها وتستغرق كل هذا الوقت للتشغيل، لديها المدخلان b وa معًا كمعاملين، فإن ثيتا تلك الدالة تُكتب بالنسبة إلى b وa معًا، صحيح؟ الحدّان المسيطران لكل منهما. نعم.

ملاحظة المترجم: العبارة في النص الرسمي المستخرج هي a plus a cubed، فنُقلت a + a^3 دون استبدال صامت. لكن الشرح المحيط يحدد الحد المسيطر بالنسبة إلى b بأنه 2^b، وبالنسبة إلى a بأنه a^3؛ وعند اعتماد المدخلين معًا تكون الرتبة المقصودة Θ(2^b + a^3).

أحد الحضور (AUDIENCE):

ألا نقلق بشأن المعاملات السالبة (Negative Coefficients)؟

آنا بيل (ANA BELL):

لا. لا حاجة إلى القلق بشأن المعاملات السالبة.

أحد الحضور (AUDIENCE):

آه، انتظري، [غير مسموع] لديه سالب.

آنا بيل (ANA BELL):

نعم.

[ضحك]

نعم. نعم، سؤال.

أحد الحضور (AUDIENCE):

أعتقد أن هذا قد يصبح مربكًا. لنقل إنهم يسألون عن البيانات الكبيرة [غير مسموع].

آنا بيل (ANA BELL):

آه، بعض المعاملات المختلفة—متغير غير موجود هنا أصلًا؟ نعم، إذا كان معامل الدالة c، لنقل لهذه الأخيرة، لكن الصيغة هي هذه، فستكون ثيتا ثابتة فقط، Θ(1). لأنها لا تعتمد أصلًا على هذه المتغيرات، لذلك تُعدّ هذه ذات زمن ثابت فحسب. هذا سؤال رائع. نعم. إذا كان المعامل c أو شيئًا آخر.

حسنًا، الآن يمكننا النظر فعلًا إلى الدوال التي نكتبها. ونفعل الشيء نفسه تمامًا. يمكن أن نبدأ أولًا بقول عدد العمليات الذي تستغرقه هذه الدالة، ونصل إلى تلك العلاقة، ونأخذ ثيتا لها فقط، صحيح؟ كما فعلنا في الشريحة السابقة تمامًا.

هذه دالة تحسب المضروب. ماذا لدينا هنا؟ لدينا هذا ثابت هنا، صحيح؟ لدينا حلقة while واحدة فقط، تحدث فيها خمسة أشياء هنا. هناك المقارنة، وهناك هذا الضرب مع الإسناد، *=، وهو عمليتان، وهذا الطرح مع الإسناد، -=، وهو عمليتان. لذلك هذه الدالة هي 5n + 2 فقط، بالتحليل نفسه الذي أجريناه قبل بضع شرائح، صحيح؟

إذا قلنا: ما ثيتا هذه الدالة؟ ما ثيتا هذا الشيء، 5n + 2؟ سهل للغاية، صحيح؟ إنها Θ(n) فقط. وفي هذه الحالة معامل دالتنا هو فعلًا n.

عندما تكون لدينا دوال أكثر تعقيدًا قليلًا وتكون لدينا أشياء متتابعة (In Series)، مثلًا هنا لدي حلقتا for إحداهما مباشرة بعد الأخرى، نستخدم أساسًا قانون الجمع (Law of Addition) لمعالجة ذلك. يعني هذا أننا نحدد ثيتا حلقة for الأولى، وثيتا حلقة for التالية، ثم نجمع قيمتي ثيتا معًا فقط، حسنًا؟

حلقة for الأولى هنا هي Θ(n)، لأنها شيء يعتمد على المعامل n. وحلقة for التالية هنا هي Θ(n^2)، صحيح؟ لأن المعامل هنا هو n * n. الأشياء داخل حلقات for ثابتة، لذلك لا تسهم بأي شيء في ثيتا لدينا، صحيح؟ لا توجد أشياء أخرى نضرب بها التعقيد هناك.

صفحة المصدر 19

إذا كانت هذه هي دالتي كلها هنا، فإن ثيتا هذه الدالة هي Θ(n) + Θ(n^2)، صحيح؟ وقانون الجمع يقول فقط إن Θ(n) + Θ(n^2) هي ثيتا وضع هذين الاثنين داخلها بوصفهما جزءًا من دالتي، n + n^2. ونعرف كيف نفعل ذلك. يختصر هذا إلى الحد المسيطر، وهو n^2.

حسنًا، هذا قانون الجمع. يكون ذلك عندما تكون لدينا حلقات أو أشياء من هذا القبيل متتابعة. ماذا عندما تكون لدينا حلقات متداخلة، صحيح؟ عندئذ نستخدم قانون الضرب (Law of Multiplication). لأنه لكل واحدة من هذه الأشياء سنضطر إلى تنفيذ هذا بذلك العدد من المرات، صحيح؟

في هذه الحالة بالتحديد علينا أن نكون حذرين. ستكون حلقة for الخارجية Θ(n). وستكون حلقة for الداخلية أيضًا Θ(n)، رغم أنني أقسم n على 2، صحيح؟ 0.5 * n ما زالت Θ(n). ذلك الثابت الضربي أمام n هو 0.5، وهو فقط—فقط—ما زال يقودني إلى Θ(n).

الطباعة ثابتة، لذلك لا يوجد شيء آخر نضرب به هناك. إذًا قانون الضرب يقول فقط إن Θ(n) ضرب Θ(n) هي Θ(n^2) هناك في الداخل. حسنًا، دعونا ننظر إلى هذا البرنامج. ما ثيتا لهذا؟

يمكننا فعل ذلك نوعًا ما بتفصيل كبير جدًّا. لدينا x كمعامل. لذلك لا نعدّ إلا الحلقات وأشياء من هذا القبيل التي هي دالة في x. لو كانت لدي حلقة هي دالة في، لا أعرف، n أو شيء ما، فهي لا تُحتسب لأنها ليست دالة في مدخلي. إذًا انظروا فقط إلى الأشياء التي هي دالة في x.

لدي حلقة for خارجية واحدة تمر x مرة. إذًا هذه Θ(x). ولدي حلقة for داخلية تبدأ من i وتصل إلى x. هذا خادع قليلًا. لكن في النهاية إجمالًا ستكون Θ(x)، لأنها ستكون—في المرة الأولى ستمر x مرة. وفي التالية ستمر x - 1، ثم x - 2، ثم x - 3. لذلك نجمع فعليًّا نوعًا ما على كل هذه الدورات x زائد—آسفة، 1 زائد 2 زائد 3 زائد 4 زائد 5، حتى x. وهذا مجرد دالة ما في x. وبالتأكيد لن يكون ثابتًا. لذلك الحلقة الداخلية هي أيضًا Θ(x).

كل شيء آخر Θ(1). لا شيء آخر يعتمد على x. لذلك هذه الدالة كلها ستكون Θ(1) لهذا الإسناد هنا. وΘ(x) ضرب Θ(x) لهذه الحلقة المتداخلة هنا. وΘ(1) لهذه return في الأسفل هنا. إذًا إجمالًا ستكون فقط ثيتا—حسنًا، هذا هو ذلك. وإجمالًا ستكون Θ(x^2) فقط باستخدام قانوني الضرب والجمع.

ملاحظة المترجم: في الحلقة المثلثية السابقة يتغير طول الحلقة الداخلية بحسب i؛ المجموع 1 + 2 + ... + x = x(x + 1)/2 هو الذي يثبت رتبة Θ(x^2) للعمل الكلي. أُبقي الوصف الشفهي للحلقة الداخلية كما ورد، مع فصل هذا التوضيح عن الأصل.

حسنًا، فكروا في هذا. ثم أخبروني بما تظنونه. ماذا تظنون أنه؟ نعم، ثيتا طول L. صحيح تمامًا. إذًا هذا ثابت. وهذه الأشياء داخل الحلقة ثابتة. وreturn ثابتة. الشيء الوحيد الذي يعتمد على L هو طول القائمة، صحيح؟ هذه الحلقة. إذًا الإجابة هي ثيتا الطول. ممتاز.

ماذا عن هذا؟ هنا نفترض أن جميع المدخلات بالطول نفسه. نعم، ثيتا طول—اختاروا ما تفضلونه. ثيتا طول L معقولة. يمكنكم أيضًا قول ثيتا طول L1 أو ثيتا طول L2. لأن هاتين حلقتان متتابعتان، صحيح؟

هذه تمر فقط على طول L. لكننا في الداخل لا نفعل أي شيء يكلف أكثر من زمن ثابت فقط، صحيح؟ هنا نقارن فقط عددين مثل 3 و2. ونسند فقط شيئًا إلى True. لذلك لا شيء آخر يعتمد فعلًا على طول القائمة. هذه ثيتا طول L. وهذه زائد ثيتا طول L. لذلك هي ثيتا طول L فقط.

صفحة المصدر 20

حسنًا، رأينا مجموعة من الخوارزميات المختلفة، صحيح؟ آسفة، لا، لم نرَ مجموعة—رأينا مجموعة من البرامج المختلفة. لكن كان بإمكاننا تصنيفها كلها ضمن واحدة من هذه الفئات، صحيح؟ وهذا هو كل—في الأساس، جميع الخوارزميات المختلفة التي يمكنكم كتابتها عمومًا، صحيح؟ شيء ثابت، Θ(1). وشيء لوغاريتمي، Θ(log n). وشيء خطي، رأينا كثيرًا من هذه، Θ(n). وشيء خطي لوغاريتمي (Log-Linear)، لم نرَ أيًّا منه بعد، لكن هذا Θ(n log n). وΘ(n^c) لثابت ما، مثل n^2 أو n^3، متعدد الحدود (Polynomial). وثيتا ثابت ما مرفوعًا إلى n، مثل 2^n أو 3^n، أسية (Exponential).

وعندما نكتب برامجنا يمكنكم إجراء تحليل سريع للبرنامج الذي كتبتموه للتو. انظروا إلى الحلقات. انظروا لتروا مدى كفاءة كتابتكم له.

ويمكنكم أساسًا تصنيف برنامجكم ضمن واحدة من هذه الفئات، صحيح؟ إذا كانت لديكم حلقات متداخلة تعتمد كلتاهما على المدخل، فربما كتبتم خوارزمية من نوع متعدد الحدود. وإذا كانت لديكم حلقة واحدة فقط تعتمد على المدخل، فربما كتبتم خوارزمية ذات زمن خطي، صحيح؟

وعندما نكتب هذه الخوارزميات، في المحاولة الأولى نريد أن نكون في مكان ما هنا في الأعلى. لا تريدون فعل شيء متعدد الحدود، وبالتأكيد لا شيء أسي، لأن الأمور تصبح بطيئة بسرعة كبيرة جدًّا مع تلك الأعداد، صحيح؟ لذلك لا نريد أبدًا أن نكون في ذلك الموقف، رغم أنه أحيانًا لا يمكن تجنبه.

حسنًا، هذا كل ما لدي. في المحاضرة القادمة سنمر على مجموعة من فئات التعقيد (Complexity Classes) المختلفة تلك، وننظر إلى برامج مختلفة تقع في تلك الفئات، خاصة اللوغاريتمية والخطية اللوغاريتمية. حسنًا.