مقدمة في علوم الحاسوب والبرمجة باستخدام Python › المحاضرة 5: الأعداد العشرية العائمة وطرق التقريب 7 / 30 السابق التالي

التفريغ الكامل للمحاضرة 5: الأعداد العائمة وطرائق التقريب

التفريغ الكامل للمحاضرة 5: الأعداد العائمة وطرائق التقريب

المصدر الأصلي: التفريغ الإنجليزي الرسمي بصيغة PDF، المرتبط بـصفحة المحاضرة وصفحة الفيديو.

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

منهج المطابقة: عناوين صفحات المصدر أدناه تطابق فواصل الصفحات (formfeeds) في الاستخراج الرسمي، وعددها 11 صفحة غير فارغة. كل فقرة كلامية تقابل فقرة واحدة في المصدر، بما فيها الأسئلة والتكرار والتصحيح الشفهي. لم تُصحَّح زلات الأرقام أو الصياغة العلمية داخل كلام المحاضِرة؛ ينبغي الرجوع إلى الشيفرة والمعادلات في الملاحظات لتمييز المقصود، مثل مربع التخمين بدل التخمين نفسه، وجذر 36 بدل جذر 6.

صفحة المصدر 1

MITOCW | 6.100L Lecture 5 Multi.mp4

[صرير]

[حفيف]

[نقر]

آنا بيل: حسنًا، فلنبدأ. في محاضرة اليوم سنراجع قليلًا المحاضرة السابقة. كنا قد بدأنا الحديث عن الأعداد الثنائية (Binary numbers). ثم سنخوض في الخوارزمية الثانية في هذا المقرر، وهي خوارزمية طريقة التقريب (Approximation method).

فلنتذكر الدافع الذي جعلنا نتحدث أصلًا عن الأعداد الثنائية وعن كيفية تمثيل الأعداد في الحاسوب، وكان الدافع هذه الشيفرة. إنها بسيطة جدًا: لدينا x قيمته الابتدائية 0، ثم حلقة تضيف 0.1 إلى نفسه 10 مرات. ثم طبعنا ما إذا كان ذلك المجموع يساوي 1.

وما رأيناه هو أن النتيجة كانت خطأ، أي False. كانت طباعة كون x مساويًا لـ1 تعطي خطأ. فطبعنا القيمة الفعلية لـx بعد إضافة 0.1 إلى نفسه 10 مرات، ورأينا أن ذلك المجموع كان في الواقع 0.9999999. وطبعًا، بالنسبة إلى Python، فإن 0.99999 لا يساوي 1. ولهذا طبعنا خطأ للتعبير الذي يقول إن x يساوي 1. وهذا هو دافعنا: لماذا يحدث هذا أصلًا في البرمجة وفي Python؟ شيء كهذا قد يوقعنا حقًا في مشكلة إن لم نكن قادرين حتى على مقارنة الأعداد ذات الفاصلة العائمة (Floating-point numbers).

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

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

فلعدد مثل 19، إذا أردنا تحويل 19 بالأساس 10 إلى الأساس 2، فإن ما تفعله الخوارزمية هنا داخل الحلقة هو أن تقول: ما دام num هذا، أيًا كان، وهو 19 في البداية، لا يزال أكبر من 0، فلنحصل على الباقي عند قسمة العدد على 2. وهذا ما يفعله num%2، إذ يعطينا إما 0 أو 1. والباقي عند قسمة 19 على 2 هو 1. وسنضيفه إلى البداية، أي نحوّل هذا العدد الصحيح 1 إلى سلسلة نصية ونضيفه إلى بداية result، التي تكون فارغة في البداية.

وهذا ما يفعله هذا السطر: result تساوي هذا الشيء هنا. ثم سنأخذ عددنا ونقسمه قسمة صحيحة (Integer division) على 2. أي سنأخذ العدد 19 ونقسمه على 2، فيكون الناتج 9.5، لكننا لا نهتم إلا بالجزء الصحيح منه، أي 9.

ثم تعيد الحلقة التحقق: هل لا يزال 9 أكبر من 0؟ نعم. إذن سنقول: ما الباقي عند قسمة 9 على 2؟ إنه 1 آخر، لذلك سنضيف هذا الباقي إلى بداية السلسلة التي نبنيها.

صفحة المصدر 2

آنا بيل: ومرة أخرى سنقسم هذا العدد على 2، فيكون لدينا الآن 4.5، ونأخذ منه الجزء الصحيح فقط، أي 4. ومرة أخرى نسأل: ما الباقي عند قسمة 4 على 2؟ إنه 0، فنضيف 0 إلى بداية هذه السلسلة الثنائية التي نبنيها. نقسمه مجددًا على 2، فيصبح 2. والباقي عند قسمة 2 على 2 هو 0، فهو عدد زوجي. نقسم 2 على 2 مرة أخرى، فيصبح 1، والباقي الذي نحصل عليه عند قسمة 1 على 2 هو 1.

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

الأجزاء داخل المربعات الحمراء هي تعاملنا مع العدد السالب. فإذا أراد المستخدم تحويل سالب 19 إلى تمثيل ثنائي، فإن if else هنا في الأعلى تقول: هل العدد أصغر من 0؟ إن كان كذلك، فلنضبط علم السالب (Negative flag) على صواب، ولنفترض ببساطة أن المستخدم أعطانا عددًا موجبًا. فنحوّل سالب 19 هذا إلى قيمته المطلقة (Absolute value)، أي موجب 19.

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

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

أحد الحضور: كيف يتعامل مع السالب إذن؟ كل شيء سيكون 0 أو 1، فكيف يقرأ السالب؟

آنا بيل: آه، هو لا يقرأه؛ نحن فقط نتظاهر بأننا أُعطينا عددًا موجبًا. ثم نجري العملية نفسها مرة بعد أخرى.

أحد الحضور: حسنًا، إذن الحاسوب لا يعرف أنه سالب؟

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

أحد الحضور: حسنًا. وعندما نتحدث عن الأسس التي نرفع إليها 2، هل ننتقل من اليسار إلى اليمين بأسس تنازلية أم بأسس تصاعدية؟

آنا بيل: في الواقع نستخدم التصاعد عندما نبني السلسلة، لأننا ننتقل من اليمين إلى اليسار.

أحد الحضور: إذن من اليمين إلى اليسار يكون تصاعديًا؟

آنا بيل: نعم، بالضبط. فهذا 2 أس 0، وهذا 2 أس 4، نعم. أما بخصوص الكسور، فإذا فكرنا في معنى الحديث عن كسر بالأساس 10 الذي يستطيع البشر قراءته، أي باستخدام الأرقام من 0 إلى 9، فإننا عندما نكتب 0.abc نقول أساسًا إن هذا هو a مقسومًا على 10، زائد b مقسومًا على 100، زائد c مقسومًا على 1,000، وهكذا.

وفي الأساس 2 سيكون لدينا الشيء نفسه تقريبًا. إذا تحدثنا عن تمثيل بالأساس 2 لعدد 0.abc، حيث تكون a أو b أو c الآن مجرد 0 أو 1، بدلًا من رقم من 0 إلى 9، فسيكون الأمر نفسه. سيكون لدينا a مقسومًا على 2، زائد b مقسومًا على 4، زائد c مقسومًا على 8، وهكذا. إذن نتعامل الآن مع قوى 2 بدلًا من قوى 10، لأن أساسنا أصبح 2 بدلًا من 10.

صفحة المصدر 3

آنا بيل: يعني هذا أن التمثيل الثنائي لكسر عشري هو أساسًا: هل يمكننا إيجاد نوع من تركيب هذه القيم، 0.5 مضروبًا في 0 أو 1، زائد 0.25 مضروبًا في 0 أو 1، زائد 0.125 مضروبًا في 0 أو 1، وهكذا وهكذا؟ فهذه كلها قوى 2.

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

ففكرة تحويل كسر عشري بالأساس 10 إلى كسر ثنائي بالأساس 2 هي كما يلي. لننظر إلى العدد العشري 3 مقسومًا على 8 كمثال فحسب. هذا يساوي 0.375، لكننا نعرف أنه 3 على 8. وبالأساس 10، أي باستخدام الأرقام من 0 إلى 9، نقول في النهاية إنه 3 على 10، زائد 7 على 100، زائد 5 على 1,000. وهذا ببساطة هو الأساس 10.

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

في هذا المثال البسيط، 0.375 يساوي 3 مقسومًا على 8. وهذا يعني أن أصغر قوة لـ2 يمكنني أن أضرب فيها 3 على 8 لتعطيني عددًا كاملًا هي 8، أي 2 أس 3. فإذا ضربت 0.375 في 8، فإن 3 على 8 مضروبًا في 8 يعطيني 3 بالأساس 10.

والآن أعرف كيف أحوّل هذا العدد الكامل إلى الثنائي. لدي وصفة. لقد فعلناها هنا على اللوح. ولدينا الشيفرة في الشريحة السابقة. وكل ما علينا فعله الآن هو تحويل العدد 3 إلى الثنائي، وهو ببساطة 11 بالأساس 2. لكن 11 هذا تمثيل للعدد 3. لذلك، لكي أعود إلى 0.375، أحتاج إلى قسمة 3 على 2 أس 3. أي أحتاج إلى قسمة 11 على 2 أس 3.

وفي الثنائي، تعني القسمة على قوة لـ2 مجرد تحريك الفاصلة، تمامًا كما تعني القسمة على 10 في الأساس 10 تحريك الفاصلة. فإذا كان العدد 3 هو 11، وكنت قد ضربت في 2 أس 3 للحصول على هذه القيمة، فإنني لكي أقسم على 2 أس 3 أحتاج فقط إلى نقل الفاصلة من موضعها بعد 11 مباشرة، خانة واحدة، ثم ثانية، ثم إضافة 0 آخر. فيصبح تمثيل 0.375 هو 0.011؛ لقد حركت هذه الفاصلة فقط ثلاث خانات لأننا نتعامل الآن بالأساس 2.

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

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

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

صفحة المصدر 4

آنا بيل: لنفترض أنني أريد العمل على 0.625 وتحويله إلى قوة لـ2. لقد هيأت x هناك في الأعلى. وهذا الجزء هنا، هذا المربع الكبير هنا، هو الجزء الذي يجد لي تلك القوة السحرية لـ2. إنه مجرد حلقة تستمر في زيادة p، أي الأس، بحيث يكون 2 أس p مضروبًا في x، وهذا %1 يعطيني فقط الجزء العشري من عملية الضرب هذه، مساويًا لـ0.

سأستمر إذن في ضرب 2 مرفوعًا إلى أس ما p في x ما دام هناك جزء عشري في عددي. وما إن يصبح %1 هذا مساويًا لـ0، فإن ذلك يعني أن العدد الذي أصل إليه هو عدد ما يتبعه .0. لم يعد له جزء عشري.

عندئذ أخرج من الحلقة، وأكون قد وجدت الأس p. وهذا سيكون العدد الصحيح؛ فأنا أضرب x في تلك القوة الخاصة، في 2 مرفوعًا إلى ذلك الأس الخاص. والآن لدي هذا العدد، وهو في الشريحة السابقة العدد 3 بالأساس 10.

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

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

إذن، Python Tutor. حسنًا، فلنتقدم خطوة خطوة. هذه 0.625 كما في الشرائح تمامًا. p تساوي 0 في البداية، وهذه الحلقة تزيد p الآن واحدًا واحدًا لتجد النقطة التي يكون فيها الباقي 0.

هنا أطبع الباقي أيضًا بالفعل. وهنا لا يزال لدينا باقٍ غير صفري. فالباقي 0.625، ثم الباقي 0.25، ثم الباقي 0.5. وفي مرحلة ما أكون قد ضربته في 2 أس 3، لأن p تساوي 3، وأحصل على باقٍ 0. والآن خرجت من تلك الحلقة وأعرف أن num تساوي 5. لقد ضربت 2 أس 3 في 0.625 لأحصل على 5.

والآن أحتاج إلى تحويل num، التي تساوي 5، إلى الثنائي باستخدام العملية التي أجريناها هنا. هذا ما تفعله هذه الشيفرة، وهذه هي بالضبط العملية التي كانت لدينا هنا. فأنا أنشئ سلسلة result هذه، ثم أضيف 0 أو 1 إلى بدايتها بحسب ما إذا كان العدد يقبل القسمة على 2 أم لا.

العدد 5 بالثنائي هو 101. وهذا يعني أن لدي 101. بوصفه تمثيلي الثنائي لـ5. والآن ستمر الشيفرة بهذه الحلقة، أي ستحرك الفاصلة إلى اليسار ثلاث خانات، لأنني اضطررت إلى الضرب في 2 أس 3 للحصول على 5.

يمكنكم إذن أن تروا أنها ستمر في الحلقة عبر ثلاث خانات. وها هي، صنعت 0.101. ثم، آسفة، هذا الجزء الذي تجاوزته يضيف النقطة، فيضع النقطة أمامه. والخطوة الأخيرة هي فقط طباعة التمثيل. إذن التمثيل الثنائي لـ0.625 هو 0.101.

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

صفحة المصدر 5

آنا بيل: كان 0.1 هو هذا العدد العشري المزعج، فلنرَ بالضبط ما حدث. حسنًا، كان عليه أن ينفّذ عددًا كبيرًا جدًا من عمليات القسمة، وأن يختبر مجموعة كبيرة من قوى 2 قبل أن يصل فعلًا إلى عدد صحيح. وفي الواقع، نحو 50 منها. ونعرف ذلك لأن هنا نحو 50 من هذه الآصفار والواحدات. إذن كان عليه تقريبًا أن يضرب 0.1 في 2 مرفوعًا للأس 50 قبل أن يصل إلى عدد صحيح.

وما الذي يعنيه هذا بالنسبة لنا؟ إن أي عدد يكون نوعًا من تركيب خطّي (Linear Combination) لقوى 2 يكون حسابه سهلًا وسريعًا جدًا، مثل هذا هنا: 1 في 2 مرفوعًا للأس ناقص 3 يساوي 0.001. لكن شيئًا مثل 0.1، الذي لا يُرى بسهولة ما هو التركيب الخطي لكل هذه القوى، فليس سهلًا الحساب. وفي الواقع اضطررنا إلى استخدام برنامجنا لمعرفة ما هو بالضبط. بالنسبة لنا كان نحو 50 رقمًا طويلًا، وهو طويل نوعًا ما. وبعض هذه الأرقام قد تكون أطول، بل قد تكون لا نهائية.

إذن النقطة هنا هي أن كل شيء في ذاكرة الحاسوب (Computer Memory) ممثَّل في صورة بتات (Bits)، أي آصفار وواحدات. والسبب في أن مررنا بكل هذا الحساب هو أن هناك أرقامًا ستكون ببساطة كبيرة جدًّا على أن تتّسع داخل عتاد الحاسوب، داخل هذه التمثيلات. فأعدادُ الأعداد الصحيحة (Integers) التعامل معها مباشر. كان لدينا طريقة سريعة جدًّا لحساب التمثيل بالأساس 2، لكن الكسور أصعب بكثير، وتكون تلك الأرقام كبيرة جدًّا، جدًّا.

إذن كيف تُمثَّل هذه الأعداد فعليًا داخل ذاكرة الحاسوب؟ إنها تُمثَّل في قطعتين. إحدى القطع هي الرقم الدالّ (Significant Digit)، والقطع الأخرى هي أُس 2. فإذا كان لدينا التمثيل 1، 1 داخل ذاكرة الحاسوب، فالرقم الدالّ هو 1، وأُس 2 هو 1. أي أننا سنأخذ هذه الواحد ونمنحه أُس 2، أي سنضيف صفرًا بعدها. وهذا هو التمثيل الثنائي للعدد 2، لأننا نقلنا الفاصلة من هنا إلى هناك، والعدد 1, 0 بالأساس 2 هو 2.0. وهذا ما لدينا في السطر الأول.

أما 1، ناقص 1، ف هذا التمثيل يعني أنني سآخذ الرقم الدالّ 1، وأُس 2 هو ناقص 1، أي سآخذ هذه الفاصلة العشرية وأنقلها إلى اليسار خانة واحدة. فسيكون هذا 0.1، أي هذا، العدد 0.1، وهو 0.5. هذا بالأساس 2، وهذا بالأساس 10.

ولكي أُبرز الفكرة: العدد 125 هو نفسه سيصير الرقم الدالّ، و2 مرفوعًا للأس ناقص 2، فكيف سيعمل هذا؟ حسنًا، سنأخذ الـ125 ونحوّله إلى صيغة أُس 2. حسنًا، هو 1111101 — لن أتذكّر ما هو بالضبط — هذا هو 125 بالأساس 2.

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

حسنًا، هكذا تمثّل الحاسوبات الأعداد فعليًا داخل الذاكرة. ونسمي هذا نوع الكائن (Object Type)، وهو float أي عدد عشري أو حقيقي، لأن هذه الفاصلة العشرية seemingly تسبح في المكان.

الحضور:

هل هو 5 أم 31.25 لكل عدد بالأساس 10؟

آنا بيل: الأساس 10 هو 31.25، و125 هو كيف يُمثَّل داخل الذاكرة. إذن هو شيء بالأساس 10. ثم ما هو أُس 2.

صفحة المصدر 6

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

الحضور:

نعم، أنا مشوّش قليلًا لأنك ذهبت إلى كل ذلك العناء لتحويل العشري إلى — آه، ذهب ذلك من أجل الكسور.

آنا بيل: ذهب من أجل الكسور. بالتحديد، نعم. حسنًا، في النهاية، فعلنا كل ذلك لأننا نحاول أن نفهم سبب الخطأ: لماذا نحصل على هذا الخطأ داخل برامجنا؟ والسبب في النهاية هو أن للحاسبات عددًا منتهيًا من البتات لتخزين البيانات. فمعظم الحاسبات الحديثة قد تكون لها 32 بت، أو ربما 64 بت، لتمثيل الأرقام الدالّة. فإذا كانت لدينا 32 خانة لوضع هذه الأرقام الدالّة، وكان تمثيل عددنا بالأساس 2 طوله 50 رقمًا، فسنقتطع عند 32. لا يمكننا تخزين تلك البتات الزائدة.

وكذلك فإن عددًا مثل 0.1 ينتهي في الواقع إلى أن يكون تقريبًا (Approximation) بالأساس 2 داخل ذاكرة الحاسوب. لسنا قادرين على تخزين ذلك العدد تخزينًا تامًّا ودقيقًا. فيصبح تقريبًا. والتقريب ينتهي في الواقع عند البت رقم 32، إما أن يكون 0 أو 1 حسب كيف نقرّر الاقتطاع.

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

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

إذن نريد دائمًا أن نختبر ما إذا كان عدد فاصلة عائمة ما ضمن إبسيلون (Epsilon) من عدد فاصلة عائمة آخر، وهذا يقودنا إلى طريقة التقريب (Approximation Method).

في المحاضرة الماضية، رأينا طريقة التخمين والتحقّق (Guess-and-Check) كخوارزمية بسيطة جدًّا لحلّ المسائل. لدينا عدد محدود من الحلول التي يمكننا التحقّق منها، ثم نفحص كل واحد منها واحدًا تلو الآخر، ثمّ في نهاية المطاف إما أن نجد الحل، أو أننا فحصنا كل ما نستطيع فحصه ولم نجد الحل.

عادة ما تكون الأشياء التي نفحصها أعدادًا صحيحة، لكن ما دام لديك مجموعة منتهية من القيم يمكنك الفحص من أجل إيجاد حل، فإن التخمين والتحقّق ينطبق تمامًا. لكن المشكلة أنه مقيَّد بعض الشيء؛ فهو لا يعطينا تقريبًا فعليًا لجذر التربيع. فإذا تذكّرت الشيفرة التي كتبناها المرة الماضية، فإنها لم تقل فعليًا «أقارّب الجذر التربيع للعدد 5 بأنه 1.4-something» أو ما شابه، أو 2-point-something؛ إنها لم تكن سوى أن تقول لي: هذا جذر تربيع كامل، أو أن العدد الذي أعطتني إياه ليس جذرًا تربيعيًا كاملًا.(Project Chosen)، فهذه خوارزمية مقيَّدة حقًّا.

صفحة المصدر 7

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

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

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

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

وماذا يعني أن تكون «جيدة بما يكفي»؟ لنفترض أننا أردنا العثور على هذا التقريب للجذر التربيع؛ التخمين والتحقّق لم يستطع فعل ذلك نيابةً عنا، لكن طريقة التقريب تستطيع. فما نطلبه هو: هل يمكننا إيجاد جذر بحيث يكون هذا الجذر مضروبًا في نفسه مساويًا لـx؟

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

إذن ستكون الخوارزمية كالتالي: سنبدأ بتخمين نعرف أنه أصغر من اللازم. فبالنسبة لجذر تربيعي لعدد، لنبدأ بـ0. ثم سنزيده بمقدار صغير جدًّا. في التخمين والتحقّق كنا نزيده أعدادًا صحيحة؛ وفي هذه الطريقة تحديدًا يمكننا أن نزيده بـ0.5 أو 0.1 أو 0.0001، أو أي شيء نشاء.

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

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

صفحة المصدر 8

آنا بيل: وإذا زدنا الإبسيلون، أي المسافة التي نريد أن نكون عليها من x، فسيكون برنامجنا أسرع، لأننا سندخل حدود زائد ناقص إبسيلون أسرع، لكنه سيكون أقل دقة، لأننا في لحظة ما سندخل الحدّ ونقول: حسنًا بما يكفي، لن أقترب أكثر من x، لأن هناك لا حاجة؛ أنا أصلًا ضمن الإبسيلون. إذن التخمين «الجيد بما يكفي» لجذر العدد 5 كان 1-point-something، لكن في الشريحة السابقة، حين كان إبسيلوننا أصغر، كان التخمين الجيد بما يكفي 2-point-something.

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

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

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

إذن نزيد تخميننا بقيمة الزيادة. ففي البداية كنا عند 0، ثم نكون عند 0.001، ثم نكون عند 0.0002، وهكذا. وnum_guesses مرة أخرى هو فقط ليتتبّع به كم مرّة مررنا فعليًا عبر هذه الحلقة. وفي النهاية يمكننا أن نطبع كم تخمينًا قدّمناه.

حسنًا، إذن إليك الشيفرة، والعدد 36، يمكننا تشغيلها. ماذا نرى؟ هذا هو تقريبنا للجذر التربيعي للعدد 36. الآن، نحن نعرف أنه 6،"Of Course"، ولو واصلنا لأمكننا أن نجد على الأرجح 6 بالضبط، لكن لاحظ أن خوارزمية التقريب هذه تتوقّف بمجرد أن تدخل حدود زائد ناقص إبسيلون. صحيح؟

الحضور:

هل حلقات for تزداد دائمًا بمقادير صحيحة؟

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

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

كان عدد التخمينات هنا نحو 59,992. وهذا منطقي لأن زيادتنا هي 0.0001، وذهبنا كلنا إلى 5.99. فمع كل مرور في الحلقة زدنا بمقدار 0.0001. أي هذا في 10,000. هذا منطقي.

إذن لنجرّبها مع بضع قيم أخرى. فههي مع 24، وهي 4.89. مرة أخرى نرى أخطاء الفاصلة العائمة تظهر في كل مكان حين نرى هذا الغريب مثل 0.00000 ثم مقدار صغير في النهاية؛ هذه هي أخطاء الفاصلة العائمة بسبب جمع الأرقام التي نعمل بها. إليك الجذر التربيع للعدد 2، وهو 1.41. مرة أخرى، خطأ فاصلة عائمة. لكن هذه المرة على الجهة الأخرى، 0.9999.

صفحة المصدر 9

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

أنا متأكّد إلى حدٍّ كبير أنني كنت أتحدّث لأكثر من خمس ثوانٍ وهذا البرنامج لا ينتهي. إذن هناك خطأ ما حدث. سأوقفه. تذكّروا أنكم تستطيعون إيقافه بالنقر على نافذة الـshell، أو بالضغط على Control-C، أو بالمربّع الصغير هنا في الزاوية.

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

عبارات print التي أضيفها موجودة هنا في هذا الموضع فقط. فكل ما عداه غير المربّع هو نفسه في الشريحة السابقة. الشيء الوحيد الجديد الذي أضيفه هو جملة if هنا. فكل 100,000 تخمين، أي كلما مررتُ عبر هذه الحلقة 100,000 مرة، سأطبع ما هو التخمين الحالي وما هو مربّع التخمين ناقص x، أي كم أنا بعيد عن x، الإبسيلون. نعم، ليس الإبسيلون، بل كم أنا بعيد عن x.

إذن لنشغّل تلك الشيفرة. هي في الأسفل هنا. أضفت شيئًا صغيرًا إضافيًا، وهو أنها لا تطبع طوال الوقت، بل تتوقّف لحظة لأتحدّث عمّا يحدث. فهنا لديّ الشيفرة مُشغَّلة، لقد شُغِّلت، فأول 100,000 مرة مررتُ عبر الحلقة كان تخميني نحو 10. وكم أنا بعيد عن x هو نحو 54,000. وأنا أريد أن أكون على بُعد 0.01 من x لأن هذا هو إبسيلوني. إذن هنا أنا على بُعد 54,000 من x. فمن الواضح أن هذا أكثر من اللازم.

لنواصل، إذن نصنع المزيد من التخمينات، ثم هنا، حين كان تخميني 100، أنا على بُعد نحو 44,000 من x أي أن 54، أي ما يبدو جيدًا. لنواصل. فمع 120، أنا على بُعد 39,000 من x.

ومع 200، أنا على بُعد 14,000 من x، فهو يبدو أفضل بكثير. أقترب وأقترب من جعل الفرق صفرًا أو 0.01، لنواصل. مع 210، أنا على بُعد 10,000 من x، ثم أنا على بُعد 6,000 تقريبًا من x، ثم أنا على بُعد 1,000 من x. ثم، مع 230 كتخمين لي،Which bring me 1,400 away from x، في المرة التالية يكون لديّ 240. والمطبعة التالية تجعلني على بُعد 3,000 من x. فكنت على بُعد 1,000 والآن أنا على بُعد 3,000.

ومن هناك فصاعدًا تنهار الأمور بسرعة حقًّا، لأنني أصبحت أبتعد أكثر فأكثر عن x. فهنا أنا أُواصل البرنامج قليلًا، ثم واصلتُ فقط نصنع التخمينات لأنني لم أكن يومًا ضمن ذلك الإبسيلون. فهو 500، والآن أنا على بُعد 200,000 تقريبًا من x. فترى الآن ما يحدث: هذا البرنامج سيبتعد أكثر فأكثر عن حيث يجب أن أكون.

إذن لنُصوّر ما حدث بالضبط. هذا هو x لدينا وهو 54,321، وهذا هو إبسيلوننا، ولنفترض أنه 0.01، فمن-obviously ليس بمقياس رسم. واللون الأزرق سيمثّل تخمينًا واحدًا. فإليك تخمينًا، ثم لدينا مربّع التخمين، باللون الأخضر. فولأغراض التصوير فقط، لنقل إن هذا هو تخميننا، وهذا هو مربّع تخميننا. حسنًا، نحن بعيدون جدًّا عن x، نحن بالتأكيد خارج حدود الإبسيلون. نصنع تخمينًا آخر بزيادته قليلًا. وهذا هو مربّع التخمين.

صفحة المصدر 10

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

هذا هو تخميننا التالي، لكن الآن صار مربّع التخمين على الجهة الأخرى.

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

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

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

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

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

إذن إليك الشيفرة مع 54321، لكن لدينا الآن ذلك الشرط الإضافي هنا، وهو أن مربّع التخمين أقل من x. فنرى أننا فعلنا عددًا كبيرًا من التخمينات، 2,300,000، والرسالة التي نحصل عليها هي أننا أخفقنا في إيجاد الجذر التربيعي، وهذا منطقي لأننا كنا نعرف أننا سنخفق.Arguments ونُبلّغ أيضًا بآخر تخمين، ومربّع آخر تخمين، تحسّبًا إن أراد المستخدم أن يستخدم تلك المعلومة لأي شيء.

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

حسنًا، لقد انتهى، ونرى هذه الفكرة بالضبط في عدد التخمينات. فهنا كان لدينا 2.3 مليون تخمين حين كانت زيادتنا 0.0001، لكن حين كانت زيادتنا 0.00001، أربع أصفار، كان لدينا 23 مليون تخمين. فمن الواضح أن لدينا 10 أضعاف عدد التخمينات، وهذا جعل برنامجنا أبطأ بعشرة أضعاف. لكن الآن، لم نخفق، لأننا استطعنا أن ندخل ضمن ذلك الإبسيلون. فوجدنا أن 233.06864، وهو قريب جدًّا مما وجدناه من قبل، هو ضمن 0.01 من الإبسيلون.

صفحة المصدر 11

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

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

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

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

ثم رأينا أننا في الواقع لدينا مقايضة: يمكننا أن لدينا برنامج ينتهي ويبلّغ عن إجابة صحيحة، ولا يقول «لقد أخفقت»، ويبلّغ عن إجابة صحيحة، لكن ذلك قد يكون برنامجًا أبطأ بكثير. فهو أبطأ بكثير لأننا اضطررنا إلى تقليل زيادتنا.

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

إذن خوارزمية التقريب هذه بطيئة حقًّا. للحصول على إجابة للجذر التربيعي للعدد 54,321 اضطررنا إلى تقليل زيادتنا إلى شيء مثل 0.00001. وشغّلناها، وقد استغرق ذلك البرنامج ربما عشر ثوانٍ على حاسوبي، لأننا بدأنا من 0 وكنا نزيد ذلك المقدار بوحدة ببطء مؤلم، رغم أننا كنا نعرف تقريبًا من العدد الفعلي، 54,000، أن جذره التربيعي لا يمكن أن يكون منخفضًا إلى هذا الحد. لكن هذه هي الخوارزمية التي كان لدينا. كان علينا أن نبدأ من شيء، وهو 0 تحسّبًا لأن يعطينا المستخدم أرقامًا أخرى لا معنى لبدء أعلى منها.

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

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