You are on page 1of 187

‫هياكل البيانات للمربمجني‬

‫مرجع عملي إىل هياكل البيانات والخوارزميات يحتاج إليه كل مهندس برمجيات‬
‫‪Book Title: Think Data Structures‬‬ ‫اسم الكتاب‪ :‬هياكل البيانات للمبرمجين‬

‫‪Author: Allen B. Downey‬‬ ‫المؤلف‪ :‬آلن ب‪ .‬دوني‬

‫‪Translator: Radwa Elaraby‬‬ ‫المترجم‪ :‬رضوى العربي‬

‫‪Editor: Jamil Bailony - Mostafa Almahmoud‬‬ ‫المحرر‪ :‬جميل بيلوني – مصطفى المحمود‬

‫‪Cover Design: Sirin Diraneyya‬‬ ‫تصميم الغالف‪ :‬سيرين ديرانية‬

‫‪Publication Year:‬‬ ‫‪2023‬‬ ‫سنة النشر‪:‬‬

‫‪Edition:‬‬ ‫‪1.0‬‬ ‫رقم اإلصدار‪:‬‬

‫بعض الحقوق محفوظة ‪ -‬أكاديمية حسوب‪.‬‬

‫أكاديمية حسوب أحد مشاريع شركة حسوب محدودة المسؤولية‪.‬‬

‫مسجلة في المملكة المتحدة برقم ‪.07571594‬‬

‫‪https://academy.hsoub.com‬‬

‫‪academy@hsoub.com‬‬
Copyright Notice ‫إشعار حقوق التأليف والنشر‬
The author publishes this work under ‫ينشر المصن ِّف هذا العمل وفقا لرخصة المشاع‬
Creative Commons Attribution- ُ ‫اإلبداعي نَسب‬
‫ الترخيص‬- ‫ غير تجاري‬- ‫المصنَّف‬
NonCommercial-ShareAlike 4.0 .)CC BY-NC-SA 4.0( ‫ دولي‬4.0 ‫بالمثل‬
International (CC BY-NC-SA 4.0).

You are free to: :‫لك مطلق الحرية في‬


• Share — copy and redistribute the ‫• المشاركة — نسخ وتوزيع ونقل العمل ألي‬
material in any medium or format .‫وسط أو شكل‬
• Adapt — remix, transform, and build ‫ واإلضافة عىل‬،‫ التحويل‬،‫• التعديل — المزج‬
upon the material .‫العمل‬

This license is acceptable for Free Cultural .‫هذه الرخصة متوافقة مع أعمال الثقافة الحرة‬
Works. ‫ال يمكن للمرخِّص إلغاء هذه الصالحيات طالما‬
The licensor cannot revoke these freedoms :‫اتبعت شروط الرخصة‬
as long as you follow the license terms:
• Attribution — You must give ‫المصنَّف — يجب عليك نَسب‬ ُ ‫نَسب‬ •
appropriate credit, provide a link to ‫ وتوفير‬،‫العمل لصاحبه بطريقة مناسبة‬
the license, and indicate if changes ‫ وبيان إذا ما قد ُأجريت أي‬،‫رابط للترخيص‬
were made. You may do so in any ‫ يمكنك القيام بهذا‬.‫تعديالت عىل العمل‬
reasonable manner, but not in any ‫ ولكن عىل أال يتم ذلك‬،‫بأي طريقة مناسبة‬
way that suggests the licensor ‫بطريقة توحي بأن المؤلف أو المرخِّص‬
endorses you or your use. .‫مؤيد لك أو لعملك‬
• NonCommercial — You may not use ‫غير تجاري — ال يمكنك استخدام هذا‬ •
the material for commercial .‫العمل ألغراض تجارية‬
purposes. ،‫الترخيص بالمثل — إذا قمت بأي تعديل‬ •
• ShareAlike — If you remix, ‫ فيجب‬،‫ أو إضافة عىل هذا العمل‬،‫تغيير‬
transform, or build upon the ‫عليك توزيع العمل الناتج بنفس شروط‬
material, you must distribute your .‫ترخيص العمل األصلي‬
contributions under the same
license as the original.
No additional restrictions — You may not ‫منع القيود اإلضافية — يجب عليك أال تطبق أي‬
apply legal terms or technological ‫شروط قانونية أو تدابير تكنولوجية تقيد اآلخرين من‬
measures that legally restrict others from .‫ممارسة الصالحيات التي تسمح بها الرخصة‬
doing anything the license permits. :‫اقرأ النص الكامل للرخصة عبر الرابط التالي‬
Read the text of the full license on the
following link:
https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode

The illustrations used in this book is created ‫الصور المستخدمة في هذا الكتاب من إعداد‬
by the author and all are licensed with a ‫المؤلف وهي كلها مرخصة برخصة متوافقة مع‬
license compatible with the previously .‫الرخصة السابقة‬
stated license.
‫عن الناشـر‬
‫ُأنتج هذا الكتاب برعاية شركة حسوب وأكاديمية حسوب‪.‬‬

‫تهدف أكاديمية حسوب إىل تعليم البرمجة باللغة العربية وإثراء المحتوى البرمجي الع‪ww‬ربي ع‪ww‬بر توف‪ww‬ير دورات‬

‫برمجة وكتب ودروس عالية الجودة من متخصصين في مجال البرمجة والمجاالت التقنية األخ‪ww‬رى‪ ،‬باإلض‪ww‬افة إىل‬

‫توفير قسم لألسئلة واألجوبة لإلجابة عىل أي سؤال يواجه المتعلم خالل رحلته التعليمية لتكون معه وتؤهل‪ww‬ه ح‪ww‬تى‬

‫دخول سوق العمل‪.‬‬

‫حسوب شركة تقنية في مهمة لتطوير الع‪ww‬الم الع‪ww‬ربي‪ .‬تب‪ww‬ني حس‪ww‬وب منتج‪ww‬ات تر ِّكز عىل تحس‪ww‬ين مس‪ww‬تقبل‬

‫العمل‪ ،‬والتعليم‪ ،‬والتواصل‪ .‬تدير حسوب أكبر منصتي عمل حر في العالم العربي‪ ،‬مستقل وخمسات ويعمل في‬

‫فيها فريق شاب وشغوف من مختلف الدول العربية‪.‬‬


‫المحتويات باختصار‬
‫‪11‬‬ ‫تمهيد‬

‫‪16‬‬ ‫‪ .1‬الواجهات ‪Interfaces‬‬

‫‪22‬‬ ‫‪ .2‬تحليل الخوارزميات‬

‫‪30‬‬ ‫‪ .3‬قائمة المصفوفة ‪ArrayList‬‬

‫‪42‬‬ ‫‪ .4‬القائمة المترابطة ‪LinkedList‬‬

‫‪52‬‬ ‫‪ .5‬القائمة ازدواجية الترابط ‪Doubly-Linked List‬‬

‫‪60‬‬ ‫‪ .6‬التنقل في الشجرة ‪Tree Traversal‬‬

‫‪70‬‬ ‫‪ .7‬كل الطرق تؤدي إىل روما‬

‫‪77‬‬ ‫‪ .8‬المفهرس ‪Indexer‬‬

‫‪86‬‬ ‫‪ .9‬الواجهة ‪Map‬‬

‫‪91‬‬ ‫‪ .10‬التعمية ‪Hashing‬‬

‫‪99‬‬ ‫‪ .11‬الواجهة ‪HashMap‬‬

‫‪108‬‬ ‫‪ .12‬الواجهة ‪TreeMap‬‬

‫‪116‬‬ ‫‪ .13‬شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫‪126‬‬ ‫‪ .14‬حفظ البيانات عبر ‪Redis‬‬

‫‪137‬‬ ‫‪ .15‬الزحف عىل ويكيبيديا‬

‫‪146‬‬ ‫‪ .16‬البحث المنطقي ‪Boolean Search‬‬

‫‪156‬‬ ‫‪ .17‬الترتيب ‪Sorting‬‬


‫هياكل البيانات للمبرمجين‬ ‫جدول المحتويات‬

‫جدول المحتويات‬
‫‪11‬‬ ‫تمهيد‬

‫‪11‬‬ ‫عن الكتاب‬

‫‪11‬‬ ‫فلسفة الكتاب‬

‫‪12‬‬ ‫المتطلبات األساسية‬

‫‪13‬‬ ‫‪ :0.1‬العمل مع الشيفرة‬

‫‪14‬‬ ‫المساهمون‬

‫‪15‬‬ ‫المساهمة‬

‫‪16‬‬ ‫‪ .1‬الواجهات ‪Interfaces‬‬

‫‪17‬‬ ‫لماذا هنالك نوعان من الصنف ‪List‬؟‬ ‫‪1.1‬‬

‫‪17‬‬ ‫الواجهات في لغة جافا‬ ‫‪1.2‬‬

‫‪18‬‬ ‫الواجهة ‪List‬‬ ‫‪1.3‬‬

‫‪20‬‬ ‫تمرين ‪1‬‬ ‫‪1.4‬‬

‫‪22‬‬ ‫‪ .2‬تحليل الخوارزميات‬

‫‪23‬‬ ‫الترتيب االنتقائي ‪Selection sort‬‬ ‫‪2.1‬‬

‫‪25‬‬ ‫ترميز ‪Big O‬‬ ‫‪2.2‬‬

‫‪26‬‬ ‫تمرين ‪2‬‬ ‫‪2.3‬‬

‫‪30‬‬ ‫‪ .3‬قائمة المصفوفة ‪ArrayList‬‬

‫‪30‬‬ ‫تصنيف توابع الصنف ‪MyArrayList‬‬ ‫‪3.1‬‬

‫‪32‬‬ ‫تصنيف التابع ‪add‬‬ ‫‪3.2‬‬

‫‪35‬‬ ‫حجم المشكلة‬ ‫‪3.3‬‬

‫‪36‬‬ ‫هياكل البيانات المترابطة ‪linked data structures‬‬ ‫‪3.4‬‬

‫‪38‬‬ ‫تمرين ‪3‬‬ ‫‪3.5‬‬

‫‪41‬‬ ‫ملحوظة متعلقة بكنس المهمالت ‪garbage collection‬‬ ‫‪3.6‬‬

‫‪42‬‬ ‫‪ .4‬القائمة المترابطة ‪LinkedList‬‬

‫‪42‬‬ ‫تصنيف توابع الصنف ‪MyLinkedList‬‬ ‫‪4.1‬‬

‫‪45‬‬ ‫الموازنة بين الصنفين ‪ MyArrayList‬و‪MyLinkedList‬‬ ‫‪4.2‬‬

‫‪46‬‬ ‫التشخيص ‪Profiling‬‬ ‫‪4.3‬‬

‫‪7‬‬
‫هياكل البيانات للمبرمجين‬ ‫جدول المحتويات‬

‫‪48‬‬ ‫تفسير النتائج‬ ‫‪4.4‬‬

‫‪50‬‬ ‫تمرين ‪4‬‬ ‫‪4.5‬‬

‫‪52‬‬ ‫‪ .5‬القائمة ازدواجية الترابط ‪Doubly-Linked List‬‬

‫‪52‬‬ ‫نتائج تشخيص األداء‬ ‫‪5.1‬‬

‫‪54‬‬ ‫تشخيص توابع الصنف ‪LinkedList‬‬ ‫‪5.2‬‬

‫‪56‬‬ ‫اإلضافة إىل نهاية قائمة من الصنف ‪LinkedList‬‬ ‫‪5.3‬‬

‫‪57‬‬ ‫القوائم ازدواجية الترابط ‪Doubly-linked list‬‬ ‫‪5.4‬‬

‫‪58‬‬ ‫اختيار هيكل البيانات األنسب‬ ‫‪5.5‬‬

‫‪60‬‬ ‫‪ .6‬التنقل في الشجرة ‪Tree Traversal‬‬

‫‪60‬‬ ‫محركات البحث‬ ‫‪6.1‬‬

‫‪61‬‬ ‫تحليل مستند ‪HTML‬‬ ‫‪6.2‬‬

‫‪63‬‬ ‫استخدام مكتبة ‪jsoup‬‬ ‫‪6.3‬‬

‫‪65‬‬ ‫التنقل في شجرة ‪DOM‬‬ ‫‪6.4‬‬

‫‪65‬‬ ‫البحث بالعمق أوال ‪Depth-first search‬‬ ‫‪6.5‬‬

‫‪66‬‬ ‫المكدسات ‪ Stacks‬في جافا‬ ‫‪6.6‬‬

‫‪68‬‬ ‫التنفيذ التكراري لتقنية البحث بالعمق أوال‬ ‫‪6.7‬‬

‫‪70‬‬ ‫‪ .7‬كل الطرق تؤدي إىل روما‬

‫‪70‬‬ ‫البداية‬ ‫‪7.1‬‬

‫‪71‬‬ ‫الواجهتان ‪ Iterables‬و‪Iterators‬‬ ‫‪7.2‬‬

‫‪73‬‬ ‫الصنف ‪WikiFetcher‬‬ ‫‪7.3‬‬

‫‪75‬‬ ‫تمرين ‪5‬‬ ‫‪7.4‬‬

‫‪77‬‬ ‫‪ .8‬المفهرس ‪Indexer‬‬

‫‪77‬‬ ‫اختيار هيكل البيانات‬ ‫‪8.1‬‬

‫‪79‬‬ ‫الصنف ‪TermCounter‬‬ ‫‪8.2‬‬

‫‪81‬‬ ‫تمرين ‪6‬‬ ‫‪8.3‬‬

‫‪86‬‬ ‫‪ .9‬الواجهة ‪Map‬‬

‫‪86‬‬ ‫تنفيذ الصنف ‪MyLinearMap‬‬ ‫‪9.1‬‬

‫‪87‬‬ ‫تمرين ‪7‬‬ ‫‪9.2‬‬

‫‪88‬‬ ‫تحليل الصنف ‪MyLinearMap‬‬ ‫‪9.3‬‬

‫‪8‬‬
‫هياكل البيانات للمبرمجين‬ ‫جدول المحتويات‬

‫‪91‬‬ ‫‪ .10‬التعمية ‪Hashing‬‬

‫‪91‬‬ ‫التعمية ‪Hashing‬‬ ‫‪10.1‬‬

‫‪94‬‬ ‫كيف تعمل التعمية؟‬ ‫‪10.2‬‬

‫‪95‬‬ ‫التعمية والقابلية للتغيير ‪mutation‬‬ ‫‪10.3‬‬

‫‪97‬‬ ‫تمرين ‪8‬‬ ‫‪10.4‬‬

‫‪99‬‬ ‫‪ .11‬الواجهة ‪HashMap‬‬

‫‪99‬‬ ‫تمرين ‪9‬‬ ‫‪11.1‬‬

‫‪100‬‬ ‫تحليل الصنف ‪MyHashMap‬‬ ‫‪11.2‬‬

‫‪102‬‬ ‫مقايضات ما بين الزمن واألداء‬ ‫‪11.3‬‬

‫‪103‬‬ ‫تشخيص الصنف ‪MyHashMap‬‬ ‫‪11.4‬‬

‫‪104‬‬ ‫إصالح الصنف ‪MyHashMap‬‬ ‫‪11.5‬‬

‫‪106‬‬ ‫مخططات أصناف ‪UML‬‬ ‫‪11.6‬‬

‫‪108‬‬ ‫‪ .12‬الواجهة ‪TreeMap‬‬

‫‪108‬‬ ‫ما هي مشكلة التعمية ‪hashing‬؟‬ ‫‪12.1‬‬

‫‪109‬‬ ‫أشجار البحث الثنائية‬ ‫‪12.2‬‬

‫‪111‬‬ ‫تمرين ‪10‬‬ ‫‪12.3‬‬

‫‪112‬‬ ‫تنفيذ الصنف ‪TreeMap‬‬ ‫‪12.4‬‬

‫‪116‬‬ ‫‪ .13‬شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫‪116‬‬ ‫الصنف ‪MyTreeMap‬‬ ‫‪13.1‬‬

‫‪117‬‬ ‫البحث عن القيم ‪values‬‬ ‫‪13.2‬‬

‫‪119‬‬ ‫تنفيذ التابع ‪put‬‬ ‫‪13.3‬‬

‫‪120‬‬ ‫التنقل بالترتيب ‪In-order‬‬ ‫‪13.4‬‬

‫‪122‬‬ ‫التوابع اللوغاريتمية‬ ‫‪13.5‬‬

‫‪124‬‬ ‫األشجار المتزنة ذاتيا ‪Self-balancing trees‬‬ ‫‪13.6‬‬

‫‪125‬‬ ‫تمرين إضافي‬ ‫‪13.7‬‬

‫‪126‬‬ ‫‪ .14‬حفظ البيانات عبر ‪Redis‬‬

‫‪127‬‬ ‫قاعدة بيانات ‪Redis‬‬ ‫‪14.1‬‬

‫‪128‬‬ ‫خوادم وعمالء ‪Redis‬‬ ‫‪14.2‬‬

‫‪128‬‬ ‫إنشاء مفهرس يعتمد عىل ‪Redis‬‬ ‫‪14.3‬‬

‫‪9‬‬
‫هياكل البيانات للمبرمجين‬ ‫جدول المحتويات‬

‫‪131‬‬ ‫أنواع البيانات في قاعدة بيانات ‪Redis‬‬ ‫‪14.4‬‬

‫‪133‬‬ ‫تمرين ‪11‬‬ ‫‪14.5‬‬

‫‪134‬‬ ‫المزيد من االقتراحات‬ ‫‪14.6‬‬

‫‪135‬‬ ‫تلميحات بسيطة بشأن التصميم‬ ‫‪14.7‬‬

‫‪137‬‬ ‫‪ .15‬الزحف عىل ويكيبيديا‬

‫‪137‬‬ ‫المفهرس المبني عىل قاعدة بيانات ‪Redis‬‬ ‫‪15.1‬‬

‫‪140‬‬ ‫تحليل أداء عملية البحث‬ ‫‪15.2‬‬

‫‪141‬‬ ‫تحليل أداء عملية الفهرسة‬ ‫‪15.3‬‬

‫‪142‬‬ ‫التنقل في مخطط ‪graph‬‬ ‫‪15.4‬‬

‫‪143‬‬ ‫تمرين ‪12‬‬ ‫‪15.5‬‬

‫‪146‬‬ ‫‪ .16‬البحث المنطقي ‪Boolean Search‬‬

‫‪146‬‬ ‫الزاحف ‪crawler‬‬ ‫‪16.1‬‬

‫‪149‬‬ ‫استرجاع البيانات‬ ‫‪16.2‬‬

‫‪149‬‬ ‫البحث المنطقي‪/‬الثنائي ‪Boolean search‬‬ ‫‪16.3‬‬

‫‪150‬‬ ‫تمرين ‪13‬‬ ‫‪16.4‬‬

‫‪152‬‬ ‫الواجهتان ‪ Comparable‬و ‪Comparator‬‬ ‫‪16.5‬‬

‫‪155‬‬ ‫ملحقات‬ ‫‪16.6‬‬

‫‪156‬‬ ‫‪ .17‬الترتيب ‪Sorting‬‬

‫‪157‬‬ ‫الترتيب باإلدراج ‪Insertion sort‬‬ ‫‪17.1‬‬

‫‪159‬‬ ‫تمرين ‪14‬‬ ‫‪17.2‬‬

‫‪160‬‬ ‫تحليل أداء خوارزمية الترتيب بالدمج‬ ‫‪17.3‬‬

‫‪162‬‬ ‫خوارزمية الترتيب بالجذر ‪Radix sort‬‬ ‫‪17.4‬‬

‫‪164‬‬ ‫خوارزمية الترتيب بالكومة ‪Heap sort‬‬ ‫‪17.5‬‬

‫‪165‬‬ ‫المقيدّة ‪Bounded heap‬‬


‫الكومة ُ‬ ‫‪17.6‬‬

‫‪166‬‬ ‫تعقيد المساحة ‪Space complexity‬‬ ‫‪17.7‬‬

‫‪10‬‬
‫تمهيد‬

‫عن الكتاب‬
‫هذا الكتاب مترجم عن الكتاب الشهير ‪ Think Data Structures‬لمؤلفه ‪ Allen B. Downey‬والذي يعد‬

‫مرجعً ا عمل ًيا في شرح موضوعي هياكل البيانات والخوارزميات اللذين يحتاج إىل تعلمهما ك‪ww‬ل م‪ww‬برمج ومهن‪ww‬دس‬

‫برمجيات يتطلع إىل احتراف مهنته وصقل عمله ورفع مستواه‪.‬‬

‫فلسفة الكتاب‬
‫تُع ّد هياكل البيانات ‪ data structures‬والخوارزميات ‪ algorithms‬واحد ًة من أهم االختراعات التي وقعت‬

‫عاما األخيرة‪ ،‬وهي من األدوات األساسية التي الب ُ ّد أن يدرسها مهندسي البرمجي‪ww‬ات‪ .‬غالبً‪ww‬ا م‪ww‬ا تك‪ww‬ون‬
‫بالخمسين ً‬
‫ً‬
‫ضخمة للغاية‪ ،‬كما أنه‪ww‬ا ع‪ww‬اد ًة م‪ww‬ا تُرك ‪w‬زّ عىل الج‪ww‬انب النظ‪ww‬ري‪،‬‬ ‫الكتب المتناولة لتلك الموضوعات ‪-‬وف ًقا للكاتب‪-‬‬

‫وتتبع نهج "من أسفل ألعىل" بشكل مفرط‪.‬‬

‫نظرية للغاية‪ :‬يعتمد التحليل الحس‪ww‬ابي للخوارزمي‪ww‬ات عىل افتراض‪ww‬ات تبس‪ww‬يطية تَحِ ‪ّ w‬د من فائ‪ww‬دتها من‬ ‫•‬
‫الناحية العملية‪ .‬تتناول الكثير من المصادر هذا الموضوع‪ ،‬ولكنها ع‪ً w‬‬
‫‪w‬ادة م‪ww‬ا تُرك‪w‬زّ عىل الج‪ww‬انب الحس‪ww‬ابي‬

‫وتُغفِ ل تلك االفتراضات التبسيطية‪ .‬في المقابل‪ ،‬يُركزّ هذا الكت‪ww‬اب عىل الج‪ww‬انب األك‪ww‬ثر عملي‪ww‬ة من ه‪ww‬ذا‬

‫الموضوع ويتجاهل بقية األجزاء‪.‬‬

‫صل عدد ص‪ww‬فحات الكتب ال‪ww‬تي تتن‪ww‬اول ه‪ww‬ذا الموض‪ww‬وع إىل ‪ 500‬ص‪ww‬فحة عىل‬ ‫ً‬
‫عادة ما يَ ِ‬ ‫ضخمة للغاية‪:‬‬ ‫•‬
‫األقل‪ ،‬بل يَبلُغ البعض منها أكثر من ‪ 1000‬صفحة‪ .‬بالتركيز عىل الجوانب التي يراها الكاتب أك‪ww‬ثر أهمي‪ً w‬‬
‫‪w‬ة‬

‫لمهندسي البرمجيات‪ ،‬ال يتعدى هذا الكتاب ‪ 200‬صفحة‪.‬‬


‫ُ‬
‫هياكل البيانات للمبرمجين‬ ‫تمهيد‬

‫نهج "من أسفل ألعىل" مفرط‪ :‬تهتم كثير من كتب هياكل البيانات بطريقة عمل هياكل البيان‪ww‬ات (أي‬ ‫•‬

‫طريق‪www‬ة تنفي‪www‬ذها ‪ ،)implementations‬وال تُعطِ ي نفس األهمي‪www‬ة لطريق‪www‬ة اِس‪www‬تخدَامها (الواجه‪www‬ات‬

‫‪ .)interfaces‬يَت ِب‪ww‬ع ه‪ww‬ذا الكت‪ww‬اب أس‪ww‬لوبًا مختل ًف‪ww‬ا‪ ،‬حيث يعتم‪ww‬د عىل نهج "من أعىل ألس‪ww‬فل"‪ ،‬فيب‪ww‬دأ‬

‫بالواجهات‪ ،‬وبالتالي يتمكَّن القراء من تعلُّم كيفية استخدام الهياكل المتاحة بإطار عمل جافا للتجميع‪ww‬ات‬

‫‪ Java Collections Framework‬قبل أن يتعمقوا بفهم تفاصيل طريقة عملها‪.‬‬

‫أخيرًا‪ ،‬تُقدِّم بعض الكتب هذه المادة العلمية بدون سياق واضح وبدون أي ح‪ww‬افز‪ ،‬فتَع‪ww‬رِض الهياك‪ww‬ل البياني‪ww‬ة‬
‫ً‬
‫واحدة تلو األخرى‪ .‬يحاول هذا الكتاب تنظيم الموضوعات نوعً‪ w‬ا م‪w‬ا من خالل الترك‪ww‬يز عىل تط‪w‬بيق ُمح‪w‬دّد ‪-‬مح‪w‬رك‬

‫بحث‪ ،-‬ويَستخ ِد م هذا التطبيق هياكل البيانات بشكل مكثف‪ ،‬وهو في الواقع موضوع مهم وشيق بحد ذاته‪.‬‬

‫في الحقيقة‪ ،‬سيدفعنا هذا التطبيق إىل دراسة بعض الموضوعات التي ربما لن تتعرَّض لها ببعض الفصول‬

‫الدراس‪ww‬ية التمهيدي‪ww‬ة الخاص‪ww‬ة بم‪ww‬ادة هياك‪ww‬ل البيان‪ww‬ات‪ ،‬حيث س‪ww‬نتعرَّض هن‪ww‬ا مثاًل ‪ ،‬لحف‪ww‬ظ هياك‪ww‬ل البيان‪ww‬ات‬

‫‪ persistent data structure‬مثل ‪.Redis‬‬

‫وتوص‪w‬ل إىل حل‪ww‬ول‬


‫ّ‬ ‫‪w‬منه الكت‪ww‬اب‪،‬‬
‫اضطرّ الكاتب التخاذ بعض القرارات الصعبة المتعلق‪ww‬ة بم‪ww‬ا ينبغي أال يتض‪َّ w‬‬
‫ً‬
‫إطالقا‪،‬‬ ‫يتضم ن الكتاب القليل من الموضوعات التي لن يحتاج معظم القراء إىل استخدامها‬
‫َّ‬ ‫وسط في العموم‪ ،‬إذ‬
‫‪w‬يتوقع البعض من‪ww‬ك معرفته‪ww‬ا‪ ،‬خاص‪ً w‬‬
‫‪w‬ة بمق‪ww‬ابالت العم‪ww‬ل‪ .‬وبالنس‪ww‬بة لتل‪ww‬ك‬ ‫َّ‬ ‫ولكنها مع ذل‪ww‬ك مهم‪ww‬ة‪ ،‬فغالبً‪ww‬ا م‪ww‬ا س‪w‬‬

‫الموضوعات‪ ،‬سيطرح الكاتب المادة العلمية طرحً‪ww‬ا تقلي‪w‬ديًا‪ ،‬كم‪ww‬ا يَمزِج‪ww‬ه ببعض من آرائ‪w‬ه الخاص‪ww‬ة لي‪w‬دفعك إىل‬

‫التفكير النقدي‪.‬‬

‫أيض‪w‬ا بعض األساس‪w‬يات ال‪w‬تي تُم‪w‬ا َرس ع‪w‬اد ًة بهندس‪w‬ة البرمجي‪w‬ات‪ ،‬بم‪w‬ا في ذل‪w‬ك نظم التحكُّم‬
‫يُقدِّم الكتاب ً‬

‫سمح للقراء‬
‫تتضمن غالبية فصول الكتاب تمرينًا يَ َ‬
‫َّ‬ ‫باإلصدار ‪ ،version control‬واختبار الوحدات ‪.unit testing‬‬

‫بتطبيق ما تعلموه خالل الفصل‪ ،‬حيث يُو ِّفر كل تمرين اختبارات أوتوماتيكية لفحص الحل‪ ،‬وباإلض‪w‬افة إىل ذل‪w‬ك‪،‬‬

‫يُو ِّفر الكاتب حاًل لغالبية التمارين ببداية الفصل التالي‪.‬‬

‫المتطلبات األساسية‬
‫خص‪w‬ص لطلب‪ww‬ة الجامع‪ww‬ات بمج‪ww‬ال عل‪ww‬وم الحاس‪ww‬وب والمج‪ww‬االت المرتبط‪ww‬ة ب‪ww‬ه‪ ،‬ولمهندس‪ww‬ي‬
‫ه‪ww‬ذا الكت‪ww‬اب ُم َّ‬
‫البرمجيات المحترفين‪ ،‬وللمتدربين بمجال هندسة البرمجي‪ww‬ات‪ ،‬وك‪ww‬ذلك لألش‪ww‬خاص ال‪ww‬ذين يس‪ww‬تعدون لمق‪ww‬ابالت‬

‫العمل التقنية‪.‬‬

‫ينبغي أن تكون عىل معرفة جيدة بلغة البرمجة جافا قبل أن تبدأ بقراءة هذا الكتاب‪ .‬وبالتحديد‪ ،‬الب ُ ّد أن تَعرِف‬

‫كيف تُعرِّف صن ًفا ‪ class‬جديدًا يمت ّد ‪ extend‬أو يرث من صنف آخر موجود‪ ،‬إىل ج‪w‬انب إمكاني‪w‬ة تعري‪w‬ف ص‪w‬نف‬

‫يُن ِّفذ واجهة ‪ .interface‬إذا لم تكن لديك تلك المعرفة‪ ،‬ف ُيمكِنك البدء بأي من الكتابين التاليين‪:‬‬

‫‪12‬‬
‫هياكل البيانات للمبرمجين‬ ‫تمهيد‬

‫خصص للقراء الذين ال يملكون‬


‫)‪ُ :Downey and Mayfield, Think Java (O’Reilly Media, 2016‬م َّ‬ ‫•‬

‫أي خبرة بالبرمجة‪.‬‬

‫)‪ :Sierra and Bates, Head First Java (O’Reilly Media, 2005‬مناسب للقراء الذين لديهم اطالع‬ ‫•‬

‫عىل لغة برمجية أخرى فعل ًيا‪.‬‬

‫ً‬
‫مألوف‪ww‬ة بالنس‪ww‬بة ل‪ww‬ك‪ ،‬ف ُيمكِن‪ww‬ك ق‪ww‬راءة درس م‪ww‬ا المقص‪ww‬ود‬ ‫إذا لم تكن الواجه‪ww‬ات ‪ interfaces‬بلغ‪ww‬ة جاف‪ww‬ا‬

‫بالواجهة؟‪.‬‬

‫ملحوظة متعلقة بالمصطلحات‬

‫ً‬
‫مربكة بعض الشيء‪ .‬تشير تلك الكلمة ضمن عبارة واجهة تطوير التطبيقات‬ ‫قد تكون كلمة واجهة ‪interface‬‬

‫‪ application programming interface‬إىل مجموعة من األصناف ‪ classes‬والتوابع ‪ methods‬التي تُو ِّفر‬

‫إمكانيات محددة‪.‬‬

‫عالو ًة عىل ذلك‪ ،‬تشير تلك الكلمة بلغة جافا إىل خاصية ضمن اللغة‪ .‬تُشبه تلك الخاصية األصناف وتُ ِّ‬
‫خصص‬

‫مجموعة من التوابع‪ ،‬ولكي نتجنَّب الخلط بينهما‪ ،‬سنَستخ ِدم كلمة واجهة بنمط الخط العادي لإلش‪ww‬ارة إىل الفك‪ww‬رة‬

‫العامة للواجهة‪ ،‬بينما سنَستخدِم كلمة ‪ interface‬بنمط خط الشيفرة لإلشارة إىل تلك الخاصية‪.‬‬

‫باإلضافة إىل ما سبق‪ ،‬ينبغي أن تك‪ww‬ون عىل علم بك‪ww‬لّ من مع‪ww‬امالت األن‪ww‬واع ‪ type parameters‬واألن‪ww‬واع‬

‫عممة ‪ .generic types‬عىل سبيل المثال‪ ،‬ينبغي أن تَعرِف كيف تُ ِ‬


‫نشئ كائنً‪ww‬ا باس‪ww‬تخدام معام‪ww‬ل ن‪ww‬وع مث‪ww‬ل‬ ‫الم َّ‬
‫ُ‬
‫>‪ ،ArrayList<Integer‬وإن لم يكن ذلك مألو ًفا بالنسبة لك‪ ،‬ف ُيمكِنك القراءة عن معامالت األنواع‪.‬‬

‫ينبغي أن تك‪w‬ون عىل دراي‪w‬ة ً‬


‫أيض‪w‬ا بإط‪w‬ار عم‪w‬ل جاف‪w‬ا للتجميع‪w‬ات ‪ .JCF‬بالتحدي‪w‬د‪ ،‬البُ‪ّ w‬د أن تك‪w‬ون عىل معرف‪w‬ة‬

‫بالواجهة ‪ List‬وبالصنفين ‪ ArrayList‬و ‪.LinkedList‬‬

‫من األفضل لو كنت قد سمعت عن أداة ‪ ،Apache Ant‬وهي أداة بناء أوتوماتيكية للغة جافا‪ ،‬كما ينبغي أن‬

‫تكون عىل معرفة بإطار عمل جافا الختبار الوحدات ‪.JUnit‬‬

‫‪ :0.1‬العمل مع الشيفرة‬
‫س‪w‬مح ل‪w‬ك بتع ُقب الملف‪w‬ات ال‪w‬تي‬
‫َ‬ ‫تتو َّفر شيفرة هذا الكتاب بـ مستودع ‪ .Git‬يُع‪ّ w‬د ‪ Git‬نظ‪w‬ام تحكم باإلص‪w‬دار يَ‬
‫يتك‪َّ ww‬ون منه‪ww‬ا مش‪ww‬روع معين‪ ،‬ويُطلَ‪ww‬ق اس‪ww‬م مس‪ww‬تودع ‪ repository‬عىل مجموع‪ww‬ة الملف‪ww‬ات ال‪ww‬تي يتحكَّم بها‬

‫ذلك النظام‪.‬‬

‫يُع ّد ‪ GitHub‬خدمة استضافة تُو ِّفر مساحة تخزين لمستودعات ‪ Git‬مع واجه‪ww‬ة إن‪ww‬ترنت مناس‪ww‬بة‪ ،‬كم‪ww‬ا تُ‪ww‬و ِّفر‬

‫أساليب متعددة للعمل مع الشيفرة‪ ،‬منها‪:‬‬

‫‪13‬‬
‫هياكل البيانات للمبرمجين‬ ‫تمهيد‬

‫ً‬
‫نسخة من مستودع ُمخزَّن بـ ‪ GitHub‬من خالل النقر عىل زر"اشتق ‪ ."Fork‬إذا لم يكن‬ ‫يُمكِنك أن تُ ِ‬
‫نشئ‬ ‫•‬

‫لديك حساب عىل الموقع فعاًل ‪ ،‬فستحتاج أواًل إىل إنشائه‪ ،‬وبعد إجراء االشتقاق ستحص‪ww‬ل عىل نس‪ww‬ختك‬

‫من المستودع عىل ‪ ،GitHub‬والتي تستطيع أن تَس‪ww‬تخدِمها لتعقب الش‪ww‬يفرة ال‪ww‬تي س‪ww‬تكتبها بنفس‪ww‬ك‪.‬‬

‫بهذا يكون قد أصبح بإمكانك نسخ المستودع أي تحميل نسخة من ملفاته إىل حاسوبك‪.‬‬

‫يُمكِنك ً‬
‫أيضا نسخ المستودع بدون االشتقاق‪ .‬إذا اخترت تلك الطريقة‪ ،‬فلن تكون بحاجة إلنشاء حس‪ww‬اب‬ ‫•‬

‫‪ ،GitHub‬ولكنك لن تتمكَّن من حفظ تعديالتك عىل الموقع‪.‬‬

‫إذا لم تكن ترغب باس‪ww‬تخدام ‪ Git‬عىل اإلطالق‪ ،‬ف ُيمكِن‪ww‬ك أن تُ ِّ‬


‫حمل الش‪ww‬يفرة بهيئ‪ww‬ة مجل‪ww‬د أرش‪ww‬يفي ‪ZIP‬‬ ‫•‬

‫حمل ‪ "Download‬بصفحة ‪ GitHub‬أو عبر الرابط ‪ http://thinkdast.com/zip‬وبع‪ww‬دما‬


‫باستخدام زر " ِّ‬
‫تنتهي من نس‪wwww‬خ المس‪wwww‬تودع أو ف‪wwww‬ك ض‪wwww‬غط المجل‪wwww‬د األرش‪wwww‬يفي‪ ،‬س‪wwww‬تجد مجل‪wwww‬د اس‪wwww‬مه‬

‫‪ ،ThinkDataStructures‬وستجد بداخله مجلدًا فرع ًيا اسمه ‪.code‬‬

‫ممت أمثل‪www‬ة ه‪www‬ذا الكت‪www‬اب واُخت‪www‬ب َرت باس‪www‬تخدام اإلص‪www‬دار الس‪www‬ابع من ع‪www‬دة تط‪www‬وير جافا‬
‫لق‪www‬د ُص‪َّ www‬‬
‫عمل بعض األمثلة؛ أما إذا كنت تَستخدِم‬
‫‪ .Java SE Development Kit‬إذا كنت تَستخدِم إصدا ًرا أقدم‪ ،‬فقد ال تَ َ‬
‫عمل جميعها بشكل سليم‪.‬‬
‫إصدا ًرا أحدث‪ ،‬فينبغي أن يَ َ‬

‫المساهمون‬
‫هذا الكتاب عبارة عن نس‪ww‬خة ُمعدَّل‪ww‬ة من منهج دراس‪ww‬ي كتب‪ww‬ه المؤل‪ww‬ف لمدرس‪ww‬ة فالتري‪ww‬ون ‪ Flatiron‬بمدين‪ww‬ة‬

‫نيويورك‪ .‬تُقدِّم تلك المدرسة العديد من الدورات المتعلقة بالبرمجة وتطوير الويب عبر اإلن‪ww‬ترنت‪ ،‬كم‪ww‬ا أنه‪ww‬ا تُق‪w‬دِّم‬
‫ً‬
‫مبنية عىل مادة هذا الكتاب‪ .‬تُو ِّفر تلك الدورة بيئ‪w‬ة تط‪w‬وير ع‪w‬بر اإلن‪w‬ترنت‪ ،‬ومس‪w‬اعدة من الم‪w‬دربين والطالب‬ ‫ً‬
‫دورة‬

‫اآلخرين‪ ،‬باإلضافة إىل شهادة إكمال الدورة‪.‬‬

‫يُوجِّه المؤل‪ww‬ف ش‪ww‬كره إىل ك‪ww‬ل من ج‪ww‬و ب‪ww‬يرجس ‪ Joe Burgess‬وآن ج‪ww‬ون ‪ Ann John‬وتش‪ww‬ارلز بليتشر‬
‫‪ Charles Pletcher‬بمدرسة ‪ ،Flatiron‬ال‪ww‬ذين وف‪ww‬روا التوجي‪ww‬ه واالقتراح‪ww‬ات والتع‪ww‬ديالت بداي‪ً w‬‬
‫‪w‬ة من التص‪ww‬ورات‬

‫األولية‪ ،‬وانتها ًء بالتنفيذات واالختبارات‪.‬‬

‫يمتن المؤلف لمراجعيه التقنيين باري ويتمان ‪ Barry Whitman‬وباتريك وايت ‪ Patrick White‬وك‪ww‬ريس‬

‫مايفيلد ‪ Chris Mayfield‬الذين ق‪ww‬دموا إلي‪ww‬ه الكث‪ww‬ير من االقتراح‪ww‬ات المفي‪ww‬دة وع‪ww‬ثروا عىل الكث‪ww‬ير من األخط‪ww‬اء‪،‬‬

‫وبالطبع أي أخطاء متبقية فهي من خطأ المؤلف وليس خطأهم‪.‬‬

‫يشكر المؤلف مدربين وطالب دورة هياكل البيانات والخوارزميات بكلية ‪ Olin‬الذين ق‪ww‬رؤوا الكت‪ww‬اب وأرس‪ww‬لوا‬

‫إليه رأيهم المفيد‪.‬‬

‫‪14‬‬
‫هياكل البيانات للمبرمجين‬ ‫تمهيد‬

‫المساهمة‬
‫يرجى إرسال بري‪w‬د إلك‪w‬تروني إىل ‪ academy@hsoub.com‬إذا ك‪w‬ان ل‪w‬ديك اق‪w‬تراح أو تص‪w‬حيح عىل النس‪w‬خة‬

‫‪w‬م َ‬
‫نت ج‪ww‬زءًا من الجمل‪ww‬ة‬ ‫العربية من الكتاب أو أي مالحظة حول أي مصطلح من المصطلحات المس‪ww‬تعملة‪ .‬إذا ض‪َّ w‬‬
‫التي يظهر الخطأ فيها عىل األقل‪ ،‬فهذا يسهِّل علينا البحث‪ ،‬ونشكرك ً‬
‫أيضا إن أضفت أرقام الصفحات واألقسام‪.‬‬

‫‪15‬‬
‫‪ .1‬الواجهات ‪Interfaces‬‬

‫يُقدِّم هذا الكتاب ثالثة موضوعات‪:‬‬

‫هياكل البيانات ‪ :Data Structures‬سنناقش هياكل البيانات ال‪ww‬تي يُو ِّفره‪w‬ا إط‪ww‬ار التجميع‪ww‬ات في لغ‪ww‬ة‬ ‫•‬

‫جافا ‪ Java Collections Framework‬والتي تُختصرُ إىل ‪ ،JCF‬وسنتعلم كيفية استخدام بعض هياك‪ww‬ل‬

‫البيانات مثل القوائم والخرائط وسنرى طريقة عملها‪.‬‬

‫لتقنيات تساعد عىل تحليل الش‪ww‬يفرة وعىل التنب‪ww‬ؤ بس‪ww‬رعة‬


‫ٍ‬ ‫تحليل الخوارزميات ِ‪ :Algorithms‬سنتعرض‬ ‫•‬

‫تنفيذها ومقدار الذاكرة الذي تتطلَّبه‪.‬‬

‫استرجاع المعلومات ‪ :Information retrieval‬سنَس‪ww‬تخدِم الموض‪ww‬وعين الس‪ww‬ابقين‪ :‬هياك‪ww‬ل البيان‪ww‬ات‬ ‫•‬

‫بسيط عبر اإلنترنت‪ ،‬وذل‪ww‬ك لنس‪w‬تفيد منهم‪w‬ا عمل ًّيا ونجع‪ww‬ل التم‪ww‬ارين‬
‫ٍ‬ ‫بحث‬
‫ٍ‬ ‫والخوارزميات إلنشاء محرك‬

‫أكثر تشوي ًقا‪.‬‬

‫وسنناقش تلك الموضوعات وف ًقا للترتيب التالي‪:‬‬

‫سنبدأ بالواجهة ‪ ،List‬وسنكتب صنفين ينفذ كلٌ منهما تلك الواجهة بطريقة مختلف‪ww‬ة‪ ،‬ثم س‪ww‬نوازن بين‬ ‫•‬

‫هذين الصنفين اللذين كتبناهما وبين صنفي جافا ‪ ArrayList‬و‪.LinkedList‬‬

‫بعد ذلك‪ ،‬سنقدِّم هياكل بيانات شجريّة الشكل‪ ،‬ونبدأ بكتاب‪ww‬ة ش‪ww‬يفرة التط‪ww‬بيق األول‪ .‬حيث س‪ww‬يقرأ ه‪ww‬ذا‬ ‫•‬

‫صفحات من موق‪ww‬ع ‪ ،Wikipedia‬ثم يُحلِّل محتوياته‪ww‬ا ويعطي النتيج‪ww‬ة عىل هيئ‪ww‬ة ش‪ww‬جرة‪ ،‬وفي‬
‫ٍ‬ ‫التطبيق‬

‫روابط ومزاي‪w‬ا أخ‪w‬رى‪ .‬سنَس‪w‬تخدِم تل‪w‬ك األدوات الختب‪w‬ار الفرض‪ّ w‬ية‬


‫َ‬ ‫النهاية سيمر عبر تلك الشجرة بح ًثا عن‬

‫الشهيرة "الطريق إىل الفلسفة" الذي يمكنك معرفة الفكرة العامة عن‪ww‬ه بق‪ww‬راءة المق‪ww‬ال باللغ‪ww‬ة اإلنجليزي‪ww‬ة‬

‫‪.Getting to Philosophy‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهات ‪Interfaces‬‬

‫المن ِّفذ له‪ww‬ا‪ ،‬ثم س‪ww‬نكتب أص‪ww‬نا ًفا تُن ِّفذ تل‪ww‬ك الواجه‪ww‬ة‬
‫س‪ww‬نتطرق للواجه‪ww‬ة ‪ Map‬وص‪ww‬نف جاف‪ww‬ا ‪ُ HashMap‬‬ ‫•‬

‫بحث ثنائ ّية‪.‬‬


‫ٍ‬ ‫باستخدام جدول ‪ hash‬وشجرة‬

‫أخيرًا‪ ،‬سنستخ ِد م تلك األصناف وبعض األصناف األخرى التي سنتناولها عبر الكتاب لتنفيذ محرك بحث‬ ‫•‬

‫عبر اإلنترنت‪ .‬س‪ww‬يكون ه‪ww‬ذا المح‪ww‬رك بمنزل‪ww‬ة زاح‪ww‬ف ‪ crawler‬يبحث عن الص‪ww‬فحات ويقرؤه‪ww‬ا‪ ،‬كم‪ww‬ا أن‪ww‬ه‬

‫س ُيفهرِس ويُخزِّن محتويات صفحات اإلنترنت بهيئةٍ تُمكِّنه من إجراء عملية البحث فيها بكفاءة‪ ،‬كما أن‪ww‬ه‬

‫المس‪ww‬تخدِم ويعي‪ww‬د النت‪ww‬ائج ذات‬


‫‪w‬ارات من ُ‬
‫ٍ‬ ‫سترجع للمعلومات‪ ،‬أي أنه س َيستق ِبل استفس‪w‬‬
‫ِ‬ ‫عمل مثل ُم‬
‫س َي َ‬
‫الصلة‪.‬‬

‫ولنبدأ اآلن‪.‬‬

‫‪ 1.1‬لماذا هنالك نوعان من الصنف ‪List‬؟‬


‫عندما يبدأ المبرمجون باستخدام إطار عمل جافا للتجميع‪ww‬ات‪ ،‬ف‪ww‬إنهم ع‪ww‬اد ًة يحت‪ww‬ارون أي الص‪ww‬نفين يخت‪ww‬ارون‬

‫‪ ArrayList‬أم ‪ .LinkedList‬فلماذا تُو ِّفر جافا تنفيذين ‪ implementations‬للواجه‪ww‬ة ‪List‬؟ وكي‪ww‬ف ينبغي‬

‫االختيار بينهما؟ سنجيب عن تلك األسئلة خالل الفصول القليلة القادمة‪.‬‬

‫المن ِّف َذة لها‪ ،‬وسنُقدِّم فكرة البرمجة إىل واجهة‪.‬‬


‫سنبدأ باستعراض الواجهات واألصناف ُ‬

‫ً‬
‫مشابهة للصنفين ‪ ArrayList‬و‪ ،LinkedList‬لكي نتمكَّن من‬ ‫سنن ِّفذ في التمارين القليلة األوىل أصنا ًفا‬

‫ومميزات‪ ،‬فبعض العمليات تك‪ww‬ون أس‪ww‬ر ع وتحت‪ww‬اج إىل مس‪ww‬احة‬


‫ٍ‬ ‫فهم طريقة عملهما‪ ،‬وسنرى أن لكل منهما عيوبًا‬

‫أق‪ww‬ل عن‪ww‬د اس‪ww‬تخدام الص‪ww‬نف ‪ ،ArrayList‬وبعض‪ww‬ها اآلخ‪ww‬ر يك‪ww‬ون أس‪ww‬ر ع وأص‪ww‬غر عن‪ww‬د اس‪ww‬تخدام الص‪ww‬نف‬

‫ن يعتمد عىل نوعية العمليات األك‪ww‬ثر‬


‫لتطبيق مع ّي ٍ‬
‫ٍ‬ ‫‪ ،LinkedList‬وبهذا يمكن القول‪ :‬إن تحديد الصنف األفضل‬

‫استخداما ضمن ذلك التطبيق‪.‬‬


‫ً‬

‫‪ 1.2‬الواجهات في لغة جافا‬


‫صنف يُن ِّفذ تل‪ww‬ك الواجه‪ww‬ة أن يُ‪ww‬و ِّفر تل‪ww‬ك‬
‫ٍ‬ ‫ً‬
‫مجموعة من التوابع ‪ ،methods‬وال ب ُ ّد ألي‬ ‫تُحدّد الواجهة بلغة جافا‬

‫المعرَّفة ضمن الحزمة ‪:java.lang‬‬


‫التوابع‪ .‬عىل سبيل المثال‪ ،‬انظر إىل شيفرة الواجهة ‪ُ Comparable‬‬

‫{ >‪public interface Comparable<T‬‬


‫;)‪public int compareTo(T o‬‬
‫}‬

‫نوع ‪ type parameter‬اسمه ‪ ،T‬وبذلك تكون تلك الواجهة من الن‪ww‬وع‬


‫ٍ‬ ‫يَستخ ِدم تعريف تلك الواجهة معاملَ‬

‫صنف يُن ِّفذ تلك الواجهة أن‪:‬‬


‫ٍ‬ ‫عمم ‪ ،generic type‬وينبغي ألي‬
‫الم َّ‬
‫ُ‬

‫يُحدد النوع الذي يشير إليه معامل النوع ‪.T‬‬ ‫•‬

‫‪17‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهات ‪Interfaces‬‬

‫ً‬
‫قيمة من النوع ‪.int‬‬ ‫يُو ِّفر تابعً ا اسمه ‪ compareTo‬يَستق ِبل كائنًا كمعامل ‪ parameter‬ويعيد‬ ‫•‬

‫مثال عىل ما نقول‪ ،‬انظر إىل الشيفرة المصدرية للصنف ‪ java.lang.Integer‬فيما يلي‪:‬‬
‫ٍ‬ ‫وفي‬

‫‪public final class Integer extends Number implements‬‬


‫{ >‪Comparable<Integer‬‬

‫{ )‪public int compareTo(Integer anotherInteger‬‬


‫;‪int thisVal = this.value‬‬
‫;‪int anotherVal = anotherInteger.value‬‬
‫‪return (thisVal<anotherVal ? -1 : (thisVal==anotherVal ? 0 :‬‬
‫;))‪1‬‬
‫}‬

‫ُحذفت التوابع الأخرى ‪//‬‬


‫}‬

‫ُّسخ ‪instance variables‬‬


‫يمت ُّد هذا الصنف من الصنف ‪ ،Number‬وبالتالي فإنه يَرِث التوابع ومتغيرات الن َ‬
‫أيضا الواجهة >‪ ،Comparable<Integer‬ولذلك فإنه يُو ِّفر تابعً ‪ w‬ا اس‪ww‬مه‬
‫المعرَّفة في ذلك الصنف‪ ،‬كما أنه يُن ِّفذ ً‬
‫ُ‬
‫ً‬
‫قيمة من النوع ‪.int‬‬ ‫‪ compareTo‬ويَستق ِبل معاماًل من النوع ‪ Integer‬ويُعيد‬

‫المصرِّف ‪ compiler‬يتأ َّكد من أن ذلك الصنف يُ‪ww‬و ِّفر‬ ‫ً‬


‫واجهة معينة‪ ،‬فإن ُ‬ ‫صنف مع ّينٌ بأنه يُن ِّفذ‬
‫ٌ‬ ‫عندما يُصرِّ ح‬

‫المعرَّفة في تلك الواجهة‪.‬‬


‫جميع التوابع ُ‬

‫الحِ ظ أنّ تنفيذ التابع ‪ compareTo‬الوارد في األعىل يَستخدِم عاماًل ثالث ًّيا ‪ ternary operator‬يُكتَب أحيانًا‬
‫ٌ‬
‫فكرة عن العوامل الثالثي‪w‬ة‪ ،‬ف ُيمكِن‪w‬ك ق‪w‬راءة مق‪w‬ال م‪w‬ا ه‪w‬و العام‪w‬ل الثالثي؟‬ ‫عىل النحو التالي ‪ .?:‬إذا لم يكن لديك‬

‫(باللغة اإلنجليزية)‪.‬‬

‫‪ 1.3‬الواجهة ‪List‬‬
‫يحتوي إطار التجميعات في لغة جافا ‪ JCF‬عىل واجهة اسمها ‪ ،List‬ويُو ِّفر تنفي‪ww‬ذين له‪ww‬ا هم‪ww‬ا ‪ArrayList‬‬

‫ثم‬ ‫و‪ .LinkedList‬تُعرِّف تلك الواجهة ما ينبغي أن يكون عليه الكائن لكي يُم ِث‪ww‬ل قائم‪ً w‬‬
‫‪w‬ة من الن‪ww‬وع ‪ ،List‬ومن ّ‬
‫‪w‬ددة من التواب‪ww‬ع‪ ،‬منه‪ww‬ا ‪ add‬و‪ get‬و‪ ،remove‬باإلض‪ww‬افة‬ ‫ً‬
‫مجموعة مح‪ً w‬‬ ‫صنف يُن ِّفذ تلك الواجهة‬
‫ٍ‬ ‫فال ب ُ ّد أن يُو ِّفر أي‬

‫إىل ‪ 20‬تابعً ا آخر‪.‬‬

‫يو ّفر كال الصنفين ‪ ArrayList‬و ‪ LinkedList‬تلك التوابع‪ ،‬وبالتالي يُمكِن التب‪ww‬ديل بينهم‪ww‬ا‪ .‬ويَعنِي ذل‪ww‬ك‬

‫‪w‬ائن من الن‪ww‬وع‬ ‫كائن من النوع ‪ ،List‬ف‪ww‬إن بإمكان‪ww‬ه العم‪ww‬ل ً‬


‫أيض ‪w‬ا م‪ww‬ع ك‪ٍ w‬‬ ‫ٍ‬ ‫عمل مع‬
‫صممٍ ل َي َ‬
‫تابع ُم َّ‬
‫ٍ‬ ‫أنه في حالة وجود‬

‫نوع آخرَ يُن ِّفذ الواجهة ‪.List‬‬


‫ي ٍ‬ ‫‪ ArrayList‬أو من النوع ‪ ،LinkedList‬أو من أ ّ‬

‫‪18‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهات ‪Interfaces‬‬

‫يُ ِّ‬
‫وضح المثال التالي تلك الفكرة‪:‬‬

‫{ ‪public class ListClientExample‬‬


‫;‪private List list‬‬

‫{ )(‪public ListClientExample‬‬
‫;)(‪list = new LinkedList‬‬
‫}‬

‫{ )(‪private List getList‬‬


‫;‪return list‬‬
‫}‬

‫{ )‪public static void main(String[] args‬‬


‫;)(‪ListClientExample lce = new ListClientExample‬‬
‫;)(‪List list = lce.getList‬‬
‫;)‪System.out.println(list‬‬
‫}‬
‫}‬

‫‪w‬ل مفي‪ww‬د‪ ،‬غ‪ww‬ير أن‪ww‬ه يحت‪ww‬وي عىل بعض العناص‪ww‬ر‬


‫كم‪ww‬ا ن‪ww‬رى‪ ،‬ال يق‪ww‬وم الص‪ww‬نف ‪ ListClientExample‬بعم‪ٍ w‬‬
‫‪w‬من متغ‪ww‬ير نس‪ww‬خةٍ من الن‪ww‬وع ‪ .List‬سنس‪ww‬تخدِم ه‪w‬ذا الص‪ww‬نف‬
‫الضرورية لتغليف قائمة من النوع ‪ ،List‬إذ يتض‪َّ w‬‬
‫لتوضيح فكر ٍة معينة‪ ،‬ثم سنحتاج إىل استخدامه في التمرين األول‪.‬‬

‫‪w‬ائن‬
‫يُهيئ باني الصنف ‪ ListClientExample‬القائم‪ww‬ة ‪ list‬باستنس‪ww‬اخ ‪- instantiating‬أي بإنش‪ww‬اء‪ -‬ك‪ٍ w‬‬
‫المم ِث‪ww‬ل‬
‫‪w‬داخلي ُ‬
‫ّ‬ ‫جدي ٍد من النوع ‪ ،LinkedList‬بينما يعيد الجالب ‪ getList‬مرجعً ‪ w‬ا ‪ reference‬إىل الك‪ww‬ائن ال‪w‬‬

‫للقائمة‪ ،‬في حين يحتوي التابع ‪ main‬عىل أسطر ٍ قليلةٍ من الشيفرة الختبار تلك التوابع‪.‬‬

‫النقطة األساسية التي أردنا اإلشارة إليها في هذا المثال هو أنه يحاول استخدام ‪ ، List‬دون أن يلجأ لتحديد‬

‫نوع القائمة هل هي ‪ LinkedList‬أم ‪ ArrayList‬ما لم تكن هناك ضرورة‪ ،‬فكما نرى متغير النسخة كيف أن‪ww‬ه‬
‫ً‬
‫قيمة من النوع ‪ ،List‬دون التط‪ww‬رق لن‪ww‬وع القائم‪ww‬ة‬ ‫ُمعرَّف ليكون من النوع ‪ ،List‬كما أن التابع ‪ getList‬يعيد‬

‫ي منهما‪ .‬وبالتالي إذا غ‪w‬يرّت رأي‪w‬ك مس‪w‬تقباًل وق‪w‬ررت أن تَس‪w‬تخدِم كائنً‪w‬ا من الن‪w‬وع ‪ ،ArrayList‬فك‪w‬ل م‪w‬ا‬
‫في أ ٍّ‬
‫تعديالت أخرى‪.‬‬
‫ٍ‬ ‫ستحتاج إليه هو تعديل الباني دون الحاجة إلجراء أي‬

‫تُطلَ‪ww‬ق عىل ه‪ww‬ذا األس‪ww‬لوب تس‪ُ w‬‬


‫‪w‬مية البرمج‪ww‬ة المعتم‪ww‬دة عىل الواجه‪ww‬ات أو البرمج‪ww‬ة إىل واجه‪ww‬ة‪ .‬وللمزي‪ww‬د من‬

‫المعلومات عنها‪ ،‬يمكنك قراءة المقال البرمجة المعتمدة عىل الواجهات المتاح بنس‪w‬خته اإلنجليزي‪w‬ة للتع‪w‬رف أك‪w‬ثر‬

‫‪19‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهات ‪Interfaces‬‬

‫عىل ه‪ww‬ذا األس‪ww‬لوب‪ .‬تج‪ww‬در اإلش‪ww‬ارة هن‪ww‬ا إىل أنّ الكالم هن‪ww‬ا عن الواجه‪ww‬ات بمفهومه‪ww‬ا الع‪ww‬ام وليس مقتص ‪w‬رًا عىل‬

‫الواجهات بلغة جافا‪.‬‬

‫في أسلوب البرمجة المعتمدة عىل الواجهات‪ ،‬تعتمد الشيفرة المكتوبة عىل الواجهات فقط مث‪ww‬ل ‪ ،List‬وال‬

‫تنفيذات مع ّينةٍ لتلك الواجهات‪ ،‬مثل ‪ .ArrayList‬وبهذا‪ ،‬ستعمل الش‪ww‬يفرة ح‪ww‬تى ل‪ww‬و تغ ّي‪ww‬ر التنفي‪ww‬ذ‬
‫ٍ‬ ‫تعتمد عىل‬

‫المعتمد عليها في المستقبل‪.‬‬

‫وفي المقابل‪ ،‬إذا تغ ّيرت الواجهة‪ ،‬فال ب ُ ّد ً‬


‫أيض‪w‬ا من تع‪ww‬ديل الش‪ww‬يفرة ال‪ww‬تي تعتم‪ww‬د عىل تل‪ww‬ك الواجه‪ww‬ة‪ ،‬وله‪ww‬ذا‬

‫السبب يتجنَّب مطورو المكتبات تعديل الواجهات إال عند الضرورة القصوى‪.‬‬

‫‪ 1.4‬تمرين ‪1‬‬

‫انسخ الشيفرة الموجودة في القسم السابق‪ ،‬وأجر ِ‬


‫نظ ًرا ألن هذا التمرين هو األول‪ ،‬فقد حرصنا عىل تبسيطه‪َ .‬‬
‫التبديل التالي‪ :‬ضع الصنف ‪ ArrayList‬بداًل من الص‪ww‬نف ‪ .LinkedList‬الح‪w‬ظ هن‪ww‬ا أن الش‪ww‬يفرة تُطبِّق مب‪w‬دأ‬

‫البرمجة إىل واجهة‪ ،‬ولذا فإنك لن تحتاج لتعديل أكثر من سطر ٍ واح ٍد فقط وإضافة تعليمة ‪.import‬‬

‫أيضا أن تك‪ww‬ون عار ًف‪ww‬ا بكيفي‪ww‬ة تص‪ww‬ريف‬


‫لكن قبل كل شيء‪ ،‬يجب ضبط بيئة التطوير المستخدمة؛ كما يجب ً‬

‫شيفرات جافا وتشغيلها لكي تتمكَّن من حل التمارين‪ .‬وقد ُطو َّرت أمثلة هذا الكتاب باس‪ww‬تخدام اإلص‪ww‬دار الس‪ww‬ابع‬

‫من عدة تطوير جافا ‪ ،Java SE Development Kit‬فإذا كنت تَستخدِم إص‪ww‬دا ًرا أح‪w‬دث‪ ،‬فينبغي أن يَ َ‬
‫عم‪ w‬ل ك‪ww‬ل‬
‫ً‬
‫متوافقة مع عدة التطوير لديك‪.‬‬ ‫شي ٍء عىل ما يرام؛ أما إذا كنت تَستخدِم إصدا ًرا أقدم‪ ،‬فربما ال تكون الشيفرة‬

‫فضل استخدام بيئ‪w‬ة تط‪w‬وير تفاعلي‪ww‬ة ‪ IDE‬ألنه‪w‬ا تُ‪w‬و ِّفر مزاي‪w‬ا إض‪w‬افية مث‪w‬ل فحص قواع‪w‬د الص‪w‬ياغة ‪syntax‬‬
‫يُ ّ‬

‫واإلكمال التلق‪w‬ائي لتعليم‪w‬ات الش‪w‬يفرة وتحس‪w‬ين هيكل‪w‬ة الش‪w‬يفرة المص‪w‬درية ‪ ،refactoring‬وه‪w‬ذا من ش‪w‬أنه أن‬

‫‪w‬دما بطلب‬
‫يُساعدك عىل تجنُّب الكثير من األخط‪ww‬اء‪ ،‬وعىل العث‪ww‬ور عليه‪ww‬ا بس‪ww‬رعة إن ُو ِج‪ w‬دت‪ ،‬ولكن إذا كنت متق‪ً w‬‬
‫وظيفة في شركة ما مثاًل وتنتظرك مقابل‪ww‬ة عم‪ww‬ل تقني‪ww‬ة‪ ،‬فه‪ww‬ذه األدوات لن تك‪ww‬ون تحت تص‪w‬رّفك غالبً‪ww‬ا في أثن‪ww‬اء‬

‫المقابلة‪ ،‬ولهذا لعلّ من األفضل التع ّود عىل كتابة الشيفرة بدونها ً‬
‫أيضا‪.‬‬

‫حملت الشيفرة المصدرية للكتاب إىل اآلن‪ ،‬فانظر إىل التعليمات في القسم ‪.0.1‬‬
‫إذا لم تكن قد َّ‬

‫َجد الملفات والمجلدات التالية داخل مجلد اسمه ‪ code‬من مستودع شيفرات الكتاب‪:‬‬
‫ست ِ‬

‫‪ :build.xml‬هو ملف ‪ Ant‬يساعد عىل تصريف الشيفرة وتشغيلها‪.‬‬ ‫•‬

‫‪ :lib‬يحتوي عىل المكتبات الالزمة لتشغيل األمثلة (مكتبة ‪ JUnit‬فقط في هذا التمرين)‪.‬‬ ‫•‬

‫‪ :src‬يحتوي عىل الشيفرة المصدرية‪.‬‬ ‫•‬

‫إذا ذهبت إىل المجلد ‪ src/com/allendowney/thinkdast‬فستجد ملفات الش‪w‬يفرة التالي‪w‬ة الخاص‪ww‬ة‬

‫بهذا التمرين‪:‬‬

‫‪20‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهات ‪Interfaces‬‬

‫‪ :ListClientExample.java‬يحتوي عىل الشيفرة المصدرية الموجودة في القسم السابق‪.‬‬ ‫•‬

‫‪ :ListClientExampleTest.java‬يحتوي عىل اختبارات ‪ JUnit‬للصنف‬ ‫•‬

‫‪.ListClientExample‬‬

‫راجع الصنف ‪ ListClientExample‬وبعد أن تتأ َّكد أن‪w‬ك فهمت كي‪w‬ف يعم‪w‬ل‪ ،‬ص‪w‬رِّفه وش‪ّ w‬غله؛ وإذا كنت‬

‫تستخدم أداة ‪ ،Ant‬فاذهب إىل مجلد ‪ code‬ون ِّفذ األمر ‪.ant ListClientExample‬‬

‫ربما تتلقى تحذيرًا يشبه التالي‪:‬‬

‫>‪List is a raw type. References to generic type List<E‬‬


‫‪should be parameterized.‬‬

‫سبب ظهور هذا التحذير هو أننا لم نُحدّد نوع عناصر القائمة‪ ،‬وق‪ww‬د فعلن‪ww‬ا ذل‪ww‬ك به‪ww‬دف تبس‪ww‬يط المث‪ww‬ال‪ ،‬لكن‬

‫يُمكِن ح‪wwww‬ل إش‪wwww‬كال ّية ه‪wwww‬ذا التح‪wwww‬ذير بتع‪wwww‬ديل ك‪wwww‬ل ‪ List‬أو ‪ LinkedList‬إىل >‪ List<Integer‬أو‬

‫>‪ LinkedList<Integer‬عىل الترتيب‪.‬‬

‫نش‪www‬ئ من خالل‪www‬ه كائنً‪www‬ا من الن‪www‬وع‬


‫يُج‪www‬رِي الص‪www‬نف ‪ ListClientExampleTest‬اختب‪www‬ا ًرا واح‪www‬دًا‪ ،‬يُ ِ‬

‫‪ ،ListClientExample‬ويَستدعِ ي تابعه الجالب ‪ ،getList‬ثم يَفحَص ما إذا كانت القيمة المع‪ww‬ادة من‪ww‬ه هي‬
‫ً‬
‫قيمة من الن‪ww‬وع ‪LinkedList‬‬ ‫َ‬
‫سيفشل هذا االختبار في البداية ألن التابع سيعيد‬ ‫كائن من النوع ‪.ArrayList‬‬

‫ال من النوع ‪ ،ArrayList‬لهذا ش ِّغل االختبار والحظ كيف أنه سيفشل‪.‬‬

‫بشكل عا ٍّم ليس مثااًل جيدًا عىل االختبارات؛‬


‫ٍ‬ ‫قد يكون هذا االختبار مناسبًا لهذا التمرين عىل وجه الخصوص‪ ،‬ولكنه‬

‫فاالختبارات الجيدة ينبغي أن تتأ َّكد من تلبية الصنف الذي يجري اختباره لمتطلبات الواجهة‪ ،‬ال أن تكون هذه‬
‫ً‬
‫مبنية عىل تفاصيل التنفيذ‪.‬‬ ‫االختبارات‬

‫واآلن لنع‪wwwww‬دّل الش‪wwwww‬يفرة كم‪wwwww‬ا يلي‪ :‬ضع‪ LinkedList‬ب‪wwwww‬داًل من ‪ ArrayList‬ض‪wwwww‬من الص‪wwwww‬نف‬

‫‪ ،ListClientExample‬وربم‪wwwww‬ا تحت‪wwwww‬اج ً‬
‫أيض‪wwwww‬ا إىل إض‪wwwww‬افة تعليم‪wwwww‬ة ‪ .import‬ص‪wwwww‬رِّف الص‪wwwww‬نف‬

‫ُ‬
‫فترض أن ينجح االختبار بعد هذا التعديل‪.‬‬‫‪ ListClientExample‬وش ِّغله‪ ،‬ثم ش ِّغل االختبار مر ًة أخرى‪ .‬يُ‬

‫‪w‬ديل الس‪ww‬م‬
‫إن سبب نجاح ه‪ww‬ذا االختب‪ww‬ار ه‪ww‬و تع‪ww‬ديلك للتس‪ww‬مية ‪ LinkedList‬في ب‪ww‬اني الص‪ww‬نف‪ ،‬دون تع‪ٍ w‬‬
‫مكان آخر‪ .‬لكن ماذا سيحدث لو فعلت؟ دعنا نجرب‪ .‬عدِّل اس‪ww‬م الواجه‪ww‬ة ‪ List‬في مك‪ww‬ان‬
‫ٍ‬ ‫الواجهة ‪ List‬في أي‬

‫واحد أو أكثر إىل الصنف ‪ ،ArrayList‬عندها ستجد أن البرنامج ما يزال بإمكان‪ww‬ه العم‪ww‬ل بش‪ww‬كل ص‪ww‬حيح‪ ،‬ولكن‪ww‬ه‬

‫‪w‬طر إىل‬ ‫زائدة عن الحاجة‪ ،‬وبالتالي إذا أردت أن تُبدِّل الواجهة ً‬


‫مرة أخرى في المستقبل‪ ،‬فستض‪ّ w‬‬ ‫ً‬ ‫اآلن يحدد تفاصيلَ‬

‫تعديالت أكثر عىل الشيفرة‪.‬‬


‫ٍ‬ ‫إجراء‬

‫تُ‪wwww‬رى‪ ،‬م‪wwww‬اذا س‪wwww‬يحدث ل‪wwww‬و اِس‪wwww‬تخدَمت ‪ List‬ب‪wwww‬داًل من ‪ ArrayList‬داخ‪wwww‬ل ب‪wwww‬اني الص‪wwww‬نف‬

‫‪ListClientExample‬؟ ولماذا ال تستطيع إنشاء نسخة من ‪List‬؟‬

‫‪21‬‬
‫‪ .2‬تحليل الخوارزميات‬

‫كما رأينا في الفصل السابق‪ ،‬تُو ِّفر جاف‪ww‬ا تنفي‪ww‬ذين ‪ implementations‬للواجه‪ww‬ة ‪ ،List‬هم‪ww‬ا ‪ArrayList‬‬

‫و‪ ،LinkedList‬حيث يك‪ww‬ون الن‪ww‬وع ‪ LinkedList‬أس‪ww‬ر ع بالنس‪ww‬بة لبعض التطبيق‪ww‬ات‪ ،‬بينم‪ww‬ا يك‪ww‬ون الن‪ww‬وع‬

‫لتطبيقات أخرى‪.‬‬
‫ٍ‬ ‫‪ ArrayList‬أسر ع بالنسبة‬

‫وإذا أردنا أن نُحدِّد أيهما أفضل لالستخدام في تطبيق معين‪ ،‬فيمكننا تجربة كلٍّ منهما عىل حد ٍة ل‪w‬نرى ال‪w‬زمن‬

‫الذي س َيستغرِقه‪ .‬يُطلَق عىل هذا األسلوب اسم التشخيص ‪ ،profiling‬ولكنّ له بعض اإلشكال ّيات‪:‬‬

‫سنضطر إىل تنفيذ الخوارزميتين كلتيهما لكي نتمكَّن من الموازنة بينهما‪.‬‬


‫ّ‬ ‫‪ .1‬أننا‬

‫المستخدَم‪ ،‬فقد تعمل خوارزمية معينة بكفاء ٍة عالي‪ww‬ة عىل حاس‪ww‬وب‬


‫‪ .2‬قد تعتمد النتائج عىل نوع الحاسوب ُ‬
‫ٌ‬
‫خوارزمية أخرى بكفاء ٍة عاليةٍ عىل حاسوب مختلف‪.‬‬ ‫معين‪ ،‬في حين قد تَ َ‬
‫عمل‬

‫الم ْدخَلة‪.‬‬
‫‪ .3‬قد تعتمد النتائج عىل حجم المشكلة أو البيانات ُ‬

‫المشكلةِ باالستعانة بما يُعرَف باسم تحليل الخوارزميات‪ ،‬ال‪ww‬ذي يُمكِّنن‪ww‬ا من‬
‫يُمكِننا معالجة بعض هذه النقاط ُ‬
‫سنضطر عندئ ٍذ لوضع بعض االفتراضات‪:‬‬
‫ّ‬ ‫خوارزميات دون الحاجة إىل تنفيذها فعل ًيا‪ ،‬ولكننا‬
‫ٍ‬ ‫الموازنة بين عدة‬

‫‪ .1‬فلكي نتجنَّب التفاصيل المتعلقة بعتاد الحاسوب‪ ،‬س‪w‬نُحدِّد العملي‪ww‬ات األساس‪w‬ية ال‪w‬تي تت‪w‬ألف منه‪ww‬ا أي‬

‫حسب عدد العمليات التي تتطلّبها كل خوارزمية‪.‬‬


‫خوارزميةٍ مثل الجمع والضرب وموازنة عددين‪ ،‬ثم ن َ ِ‬

‫الم ْدخَل‪ww‬ة‪ ،‬ف‪ww‬إن الخي‪ww‬ار األفض‪ww‬ل ه‪ww‬و تحلي‪ww‬ل متوس‪ww‬ط األداء‬


‫‪ .2‬ولكي نتجنَّب التفاصيل المتعلق‪ww‬ة بالبيان‪ww‬ات ُ‬

‫للم ْدخَالت التي نتوقع التعامل معها‪ .‬فإذا لم يَكُن ذلك متا ً‬
‫حا‪ ،‬فسيكون تحليل الحال‪ww‬ة األس‪ww‬وأ ه‪ww‬و الخي‪ww‬ار‬ ‫ُ‬
‫البديل األكثر شيوعً ا‪.‬‬
‫هياكل البيانات للمبرمجين‬ ‫تحليل الخوارزميات‬

‫‪ .3‬أخيرًا‪ ،‬سيتع ّين علينا التعامل مع احتمالية أن يكون أداء خوارزميةٍ معينةٍ فعّااًل عند التعامل مع مشكالت‬
‫ً‬
‫ع‪w‬ادة م‪w‬ا‬ ‫مشكالت كبيرة‪ .‬وفي تل‪ww‬ك الحال‪ww‬ة‪،‬‬
‫ٍ‬ ‫صغيرة وأن يكون أداء خوارزميةٍ أخرى فعّااًل عند التعامل مع‬

‫نُر ِّكز عىل المشكالت الكبيرة‪ ،‬ألن االختالف في األداء ال يكون كبيرًا مع المشكالت الصغيرة‪ ،‬ولكن‪ww‬ه يك‪ww‬ون‬

‫كذلك مع المشكالت الكبيرة‪.‬‬

‫يقودنا هذا النوع من التحليل إىل تصنيف بس‪ww‬يط للخوارزمي‪ww‬ات‪ .‬عىل س‪ww‬بيل المث‪ww‬ال‪ ،‬إذا ك‪ww‬ان زمن تش‪ww‬غيل‬

‫خوارزمية ‪ A‬يتناسب مع حجم المدخالت ‪ ،n‬وكان زمن تشغيل خوارزمية أخ‪ww‬رى ‪ B‬يتناس‪ww‬ب م‪ww‬ع ‪ ،n2‬ف ُيمكِنن‪ww‬ا أن‬

‫نقول إن الخوارزمية ‪ A‬أسر ع من الخوارزمية ‪ B‬لقيم ‪ n‬الكبيرة عىل األقل‪.‬‬

‫يُمكِن تصنيف غالبية الخوارزميات البسيطة إىل إحدى التصنيفات التالية‪:‬‬

‫ذات زمن ثابت‪ :‬تكون الخوارزمية ثابت‪ww‬ة ال‪ww‬زمن إذا لم يعتم‪ww‬د زمن تش‪ww‬غيلها عىل حجم الم‪ww‬دخالت‪ .‬عىل‬ ‫•‬

‫ي‬ ‫مصفوفة مك َّو ٌ‬


‫نة من عدد ‪ n‬من العناص‪ww‬ر‪ ،‬واس‪ww‬تخدمنا العام‪ww‬ل [] لق‪ww‬راءة أ ٍّ‬ ‫ٌ‬ ‫سبيل المثال‪ ،‬إذا كان لدينا‬

‫ّ‬
‫بغض النظر عن حجم المصفوفة‪.‬‬ ‫من عناصرها‪ ،‬فإن ذلك يتطلَّب نفس عدد العمليات‬

‫خطي‪ :‬تكون الخوارزمية خط ّي ًة إذا تناسب زمن تشغيلها مع حجم المدخالت‪ .‬فإذا كنا نحسب‬
‫ذات زمن ّ‬ ‫•‬

‫حاصل مجموع العناصر الموجودة ضمن مصفوفة مثاًل ‪ ،‬فعلين‪w‬ا أن نس‪w‬ترجع قيم‪w‬ة ع‪w‬دد ‪ n‬من العناص‪w‬ر‪،‬‬

‫الكلي للعمليات (االس‪ww‬ترجاع والجم‪ww‬ع) ه‪ww‬و‬


‫ّ‬ ‫وأن نُن ِّفذ عدد ‪ n-1‬من عمليات الجمع‪ ،‬وبالتالي يكون العدد‬

‫‪ ،2*n-1‬وهو عد ٌد يتناسب مع ‪.n‬‬

‫ذات زمن تربيعي‪ :‬تكون الخوارزمية تربيعية أو من الدرجة الثانية إذا تناسب زمن تشغيلها م‪ww‬ع ‪ .n2‬عىل‬ ‫•‬

‫ي عنصر ٍ ضمن قائمةٍ معينةٍ ُمكرَّ ًرا‪ ،‬ف‪ww‬إن بإمك‪ww‬ان‬


‫سبيل المثال‪ ،‬إذا كنا نريد أن نفحص ما إذا كان هنالك أ ُّ‬
‫خوارزميةٍ بسيطةٍ أن توازن كل عنصر ٍ ضمن القائمة بجميع العناصر األخرى‪ ،‬وذلك نظ ًرا لوجود عدد ‪ n‬من‬

‫الكلي لعملي‪ww‬ات‬
‫ّ‬ ‫العناصر‪ ،‬والتي ال ب ُ ّد من موازنة ُكلٍّ منها مع عدد ‪ n-1‬من العناصر األخرى‪ ،‬يكون الع‪ww‬دد‬

‫الموازنة هو ‪ ،n2-n‬وهو عد ٌد يتناسب مع ‪.n2‬‬

‫‪ 2.1‬الرتتيب االنتقائي ‪Selection sort‬‬


‫ً‬
‫بسيطة تُعرَف باسم الترتيب االنتقائي (باللغة اإلنجليزية)‪:‬‬ ‫ً‬
‫خوارزمية‬ ‫تُن ِّفذ الشيفرة المثال التالية‬

‫{ ‪public class SelectionSort‬‬

‫**‪/‬‬
‫بدل العنصرين الموجودين بالفهرس ‪ i‬والفهرس ‪* j‬‬
‫‪*/‬‬
‫{ )‪public static void swapElements(int[] array, int i, int j‬‬
‫;]‪int temp = array[i‬‬

‫‪23‬‬
‫هياكل البيانات للمبرمجين‬ ‫تحليل الخوارزميات‬

‫;]‪array[i] = array[j‬‬
‫;‪array[j] = temp‬‬
‫}‬

‫**‪/‬‬
‫مرر *‬ ‫بدءا من الفهرس ُ‬
‫الم َّ‬ ‫ً‬ ‫اعثر على فهرس أصغر عنصر‬
‫عبر المعامل ‪ index‬وحتى نهاية المصفوفة *‬
‫‪*/‬‬
‫{ )‪public static int indexLowest(int[] array, int start‬‬
‫;‪int lowIndex = start‬‬
‫{ )‪for (int i = start; i < array.length; i++‬‬
‫{ )]‪if (array[i] < array[lowIndex‬‬
‫;‪lowIndex = i‬‬
‫}‬
‫}‬
‫;‪return lowIndex‬‬
‫}‬

‫**‪/‬‬
‫رتب المصفوفة باستخدام خوارزمية الترتيب الانتقائي *‬
‫‪*/‬‬
‫{ )‪public static void selectionSort(int[] array‬‬
‫{ )‪for (int i = 0; i < array.length; i++‬‬
‫;)‪int j = indexLowest(array, i‬‬
‫;)‪swapElements(array, i, j‬‬
‫}‬
‫}‬
‫}‬

‫يُبدِّل التابع األول ‪ swapElements‬عنصرين ضمن المصفوفة‪ ،‬وتَستغرِق عمليت‪ww‬ا ق‪ww‬راءة العناص‪ww‬ر وكتابته‪ww‬ا‬

‫زمنًا ثابتًا؛ ألننا لو عَ رَفنا حجم العناصر وموضع العنصر األول ضمن المصفوفة‪ ،‬فسيكون بإمكاننا حس‪ww‬اب موض‪ww‬ع‬

‫وجمع فقط‪ ،‬وكلتاهما من العمليات التي تَستغرِق زمنًا ثابتًا‪ّ .‬‬


‫ولم‪ ww‬ا ك‪ww‬انت‬ ‫ٍ‬ ‫ضرب‬
‫ٍ‬ ‫أي عنصر ٍ آخ َر باستخدام عمليتي‬

‫جميع العمليات ضمن التابع ‪ swapElements‬تَستغرِق زمنًا ثابتًا‪ ،‬فإن التابع بالكامل يَستغرِق بدوره زمنًا ثابتًا‪.‬‬

‫معين‬
‫ٍ‬ ‫فه‪ww‬رس‬
‫ٍ‬ ‫فهرس ‪ index‬أصغر ِ عنصر ٍ في المصفوفة ب‪ww‬دءًا من‬
‫ِ‬ ‫ُ‬
‫يبحث التابع الثاني ‪ indexLowest‬عن‬

‫خصصه المعامل ‪ ،start‬ويقرأ كل تكرارٍ ضمن الحلقة التكراريّة عنصرين من المصفوفة ويُوازن بينهم‪ww‬ا‪ ،‬ونظ‪ww‬رًا‬
‫يُ ِّ‬

‫‪24‬‬
‫هياكل البيانات للمبرمجين‬ ‫تحليل الخوارزميات‬

‫ألن ك‪ww‬ل تل‪ww‬ك العملي‪ww‬ات تس‪ww‬تغرِق زمنً‪ww‬ا ثاب ًت‪ww‬ا‪ ،‬فال يَهُ ّم أيُه‪ww‬ا نَعُ ‪ w‬دّ‪ .‬ونحن هن‪ww‬ا به‪ww‬دف التبس‪ww‬يط سنحس‪ww‬ب ع‪ww‬دد‬

‫عمليات الموازنة‪:‬‬

‫ساوي الصفر‪ ،‬فس َي ُمرّ التابع ‪ indexLowest‬عبر المصفوفة بالكام‪ww‬ل‪ ،‬وبالت‪ww‬الي يك‪ww‬ون‬
‫‪ .1‬إذا كان ‪ start‬يُ ِ‬
‫المجراة ُمساويًا لعدد عناصر المصفوفة‪ ،‬وليكن ‪.n‬‬
‫عدد عمليات الموازنة ُ‬

‫ساوي ‪.n-1‬‬
‫ساوي ‪ ،1‬فإن عدد عمليات الموازنة يُ ِ‬
‫‪ .2‬إذا كان ‪ start‬يُ ِ‬

‫‪ .3‬في العم‪ww‬وم‪ ،‬يك‪ww‬ون ع‪ww‬دد عملي‪ww‬ات الموازن‪ww‬ة مس‪ww‬اويًا لقيم‪ww‬ة ‪ ،n-start‬وبالت‪ww‬الي‪ ،‬يَس‪ww‬تغرِق الت‪ww‬ابع‬

‫خط ًيا‪.‬‬
‫‪ indexLowest‬زمنًا ّ‬

‫يُرتِّب التابع الثالث ‪ selectionSort‬المصفوفة‪ .‬ويُن ِّفذ التابع حلقة تكرار من ‪ 0‬إىل ‪ ،n-1‬أي يُنف ‪ِّ w‬‬
‫ذ الحلق‪ww‬ة‬

‫عدد ‪ n‬من المرات‪ .‬وفي ك‪ww‬ل م‪ww‬رة يَس‪ww‬تدعِ ي خالله‪ww‬ا الت‪ww‬ابع ‪ ،indexLowest‬ثم يُن ِّفذ العملي‪ww‬ة ‪swapElements‬‬

‫التي تَستغرِق زمنًا ثابتًا‪.‬‬

‫ل م‪ww‬رة‪ ،‬فإن‪ww‬ه يُن ِّفذ ع‪ww‬ددًا من عملي‪ww‬ات الموازن‪ww‬ة مق‪ww‬داره ‪ ،n‬وعن‪ww‬د‬


‫عن‪ww‬د اس‪ww‬تدعاء الت‪ww‬ابع ‪ indexLowest‬أل ّو ِ‬
‫استدعائه للمرة الثانية‪ ،‬فإنه يُن ِّفذ عددًا من عمليات الموازنة مقداره ‪ ،n-1‬وهكذا‪ .‬وبالتالي سيكون العدد اإلجمالي‬

‫لعمليات الموازنة هو‪:‬‬

‫‪n + n-1 + n-2 + ... + 1 + 0‬‬

‫يبلُغ مجموع تلك السلسلة مق‪ww‬دا ًرا يُس‪ِ w‬‬


‫‪w‬اوي ‪ ،n(n+1)/2‬وه‪ww‬و مق‪ww‬دارٌ يتناس‪ww‬ب م‪ww‬ع ‪ ،n2‬مم‪ww‬ا يَعنِي أن الت‪ww‬ابع‬

‫‪ selectionSort‬يقع تحت التصنيف التربيعي‪.‬‬

‫يُمكِننا الوصول إىل نفس النتيجة بطريقة أخرى‪ ،‬وهي أن ننظر للتابع ‪ indexLowest‬كما لو كان حلقة تكرارٍ‬
‫ً‬
‫مجموعة من العملي‪ww‬ات يك‪ww‬ون‬ ‫ً‬
‫متداخلة ‪ ،nested‬ففي كل مرة نَستدعِ ي خاللها التابع ‪ ،indexLowest‬فإنه يُن ِّفذ‬

‫الكلي للعمليات يكون متناس‪ww‬بًا‬


‫ّ‬ ‫عددها متناسبًا مع ‪ ،n‬ونظ ًرا ألننا نَستدعيه عددًا من المرات مقداره ‪ ،n‬فإن العدد‬

‫مع ‪.n2‬‬

‫‪ 2.2‬ترمزي ‪Big O‬‬


‫تنتمي جميع الخوارزمي‪ww‬ات ال‪ww‬تي تَس‪ww‬تغرِق زمنً‪ww‬ا ثاب ًت‪ww‬ا إىل مجموع‪ww‬ةٍ يُطلَ‪ww‬ق عليه‪ww‬ا اس‪ww‬م )‪ ،O(1‬ف‪ww‬إذا قلن‪ww‬ا إن‬
‫ً‬
‫معينة تنتمي إىل المجموعة )‪ ،O(1‬فهذا يعني ض‪ww‬من ًّيا أنه‪ww‬ا تس‪ww‬تغرِق زمنً‪ww‬ا ثاب ًت‪ww‬ا‪ .‬وعىل نفس المن‪ww‬وال‪،‬‬ ‫ً‬
‫خوارزمية‬

‫تنتمي جمي‪ww‬ع الخوارزمي‪ww‬ات الخط ّي‪ww‬ة ‪-‬ال‪ww‬تي تس‪ww‬تغرِق زمنً‪ww‬ا خط ًي‪ww‬ا‪ -‬إىل المجموع‪ww‬ة )‪ ،O(n‬بينم‪ww‬ا تنتمي جمي‪ww‬ع‬

‫الخوارزمي‪ww‬ات التربيعي‪ww‬ة إىل المجموع‪ww‬ة )‪ .O(n2‬تطلَ‪ww‬ق عىل تص‪ww‬نيف الخوارزمي‪ww‬ات به‪ww‬ذا األس‪ww‬لوب تس‪ww‬مية‬

‫ترميز ‪.Big O‬‬

‫‪25‬‬
‫هياكل البيانات للمبرمجين‬ ‫تحليل الخوارزميات‬

‫الرياضي منه فبإمكانك االطالع عىل‬


‫ّ‬ ‫التعمق في الجزء‬
‫ّ‬ ‫ً‬
‫عارضا‪ ،‬ولكن لو أردت‬ ‫لقد عرَّفنا هنا ترميز ‪ big O‬تعري ًفا‬

‫مقال ما هو ترميز ‪.Big O‬‬

‫ً‬
‫خوارزمية‬ ‫يُو ِّفر هذا الترميز أسلوبًا سهاًل لكتابة القواعد العامة التي تسلُكها الخوارزميات في العموم‪ .‬فلو ن َّفذنا‬
‫ً‬
‫خطية وتبعناها بخوارزميةٍ ثابتة الزمن عىل سبيل المثال‪ ، ،‬فإن زمن التشغيل اإلجمالي يكون خط ًيا‪ .‬وننبّه هنا إىل‬

‫أنّ ∈ تَعنِي "ينتمي إىل"‪:‬‬

‫)‪If f ∈ O(n) and g ∈ O(1), f+g ∈ O(n‬‬

‫إذا أجرينا عمليتين خطيتين‪ ،‬فسيكون المجموع اإلجمالي خط ًيا‪:‬‬

‫)‪If f ∈ O(n) and g ∈ O(n), f+g ∈ O(n‬‬

‫عملية خط ّي ًة أي عد ٍد من المرات‪ ،‬وليكن ‪ ،k‬ف‪ww‬إن المجم‪ww‬وع اإلجم‪ww‬الي س‪ww‬يبقى خط ًي‪ww‬ا‬


‫ً‬ ‫في الحقيقة‪ ،‬إذا أجرينا‬

‫طالما أن ‪ k‬قيمة ثابتة ال تعتمد عىل ‪:n‬‬

‫)‪If f ∈ O(n) and k is constant, kf ∈ O(n‬‬

‫ً‬
‫تربيعية‪:‬‬ ‫ً‬
‫خطية عدد ‪ n‬من المرات‪ts ،‬تكون النتيجة‬ ‫ً‬
‫عملية‬ ‫في المقابل‪ ،‬إذا أجرينا‬

‫)‪If f ∈ O(n), nf ∈ O(n^2‬‬

‫‪w‬اوي ‪ ،2n+1‬فإن‪ww‬ه إجم‪ww‬ااًل‬


‫الكلي للعمليات يُس‪ِ w‬‬ ‫أس لألساس ‪ ،n‬فإذا كان العدد‬
‫وفي العموم‪ ،‬ما يهمنا هو أكبر ٍّ‬
‫ّ‬
‫ينتمي إىل )‪ ،O(n‬وال أهمية للثابت ‪ 2‬وال للقيمة المض‪ww‬افة ‪ 1‬في ه‪ww‬ذا الن‪w‬وع من تحلي‪ww‬ل الخوارزمي‪ww‬ات‪ .‬وبالمث‪ww‬ل‪،‬‬

‫أهمية لألرقام الكبيرة التي تراها‪.‬‬


‫ّ‬ ‫ينتمي ‪ n2+100n+1000‬إىل )‪ .O(n2‬وال‬

‫‪w‬رتيب نم ‪ٍّ w‬و معين إىل‬


‫ُ‬ ‫ً‬
‫طريقة أخرى للتعبير عن نفس الفكرة‪ ،‬ويشير ت‪w‬‬ ‫يُع ّد ترتيب النمو ‪Order of growth‬‬

‫مجموع‪ww‬ة الخوارزمي‪ww‬ات ال‪ww‬تي ينتمي زمن تش‪ww‬غيلها إىل نفس تص‪ww‬نيف ترم‪ww‬يز ‪ ،big O‬حيث تنتمي جمي‪ww‬ع‬

‫الخوارزميات الخطية مثاًل إىل نفس ترتيب النمو؛ وذلك ألن زمن تشغيلها ينتمي إىل المجموعة )‪.O(n‬‬

‫قص د بكلمة "ترتيب" ضمن هذا السياق "مجموعة"‪ ،‬مثل اِستخدَامنا لتلك الكلمة في عبار ٍة مثل "ت‪ww‬رتيب‬
‫ويُ َ‬
‫قصد بهذا أنهم مجموعة من الفرسان‪ ،‬وليس طريقة ص ّفهم أو ترتيبهم‪ ،‬أي يُمكِن‪ww‬ك‬
‫فرسان المائدة المستديرة"‪ .‬ويُ َ‬
‫أن تنظر إىل ترتيب الخوارزميات الخطية وكأنها مجموعة من الخوارزميات التي تتمتّع بكفاء ٍة عالية‪.‬‬

‫‪ 2.3‬تمرين ‪2‬‬
‫يشتمل التمرين التالي عىل تنفيذ الواجهة ‪ List‬باستخدام مصفوفةٍ لتخزين عناصر القائمة‪.‬‬

‫ستجد الملفات التالية في مستودع الشيفرة الخاص بالكتاب ‪-‬انظر القسم ‪:-0.1‬‬

‫‪26‬‬
‫هياكل البيانات للمبرمجين‬ ‫تحليل الخوارزميات‬

‫ُ‬
‫أربعة توابعَ غير مكتملة علي‪ww‬ك‬ ‫‪ : MyArrayList.java‬يحتوي عىل تنفيذ جزئي للواجهة ‪ ،List‬فهناك‬ ‫•‬

‫أن تكمل كتابة شيفرتها‪.‬‬

‫‪ :MyArrayListTest.java‬يحتوي عىل مجموعة من اختبارات ‪ ،JUnit‬والتي يُمكِنك أن تَس‪ww‬تخدِمها‬ ‫•‬

‫للتحقق من صحة عملك‪.‬‬

‫كما س‪ww‬تجد المل‪ww‬ف ‪ .build.xml‬يُمكِن‪ww‬ك أن تُن ِّفذ األم‪ww‬ر ‪ant MyArrayList‬؛ لكي تتمكَّن من تش‪ww‬غيل‬

‫اختب‪w‬ارات بس‪w‬يطة‪.‬‬
‫ٍ‬ ‫الص‪w‬نف ‪ MyArrayList.java‬وأنت م‪w‬ا ت‪w‬زال في المجل‪w‬د ‪ code‬ال‪w‬ذي يحت‪w‬وي عىل ع‪w‬دة‬

‫اختبارات ‪.JUnit‬‬
‫ِ‬ ‫ويُمكِنك بداًل من ذلك أن تُن ِّفذ األمر ‪ ant MyArrayListTest‬لكي تُش ِّغل‬

‫عندما تُش ِّغل تلك االختبارات فسيفشل بعضها‪ ،‬والسبب هو وجود توابع ينبغي عليك إكمالها‪ .‬إذا نظرت إىل‬

‫الشيفرة‪ ،‬فستجد ‪ 4‬تعليقات ‪ TODO‬تشير إىل هذه موضع كل من هذه التوابع‪.‬‬

‫ً‬
‫سريعة عىل بعض أجزاء الش‪ww‬يفرة‪ .‬تحت‪ww‬وي الش‪ww‬يفرة‬ ‫ولكن قبل أن تبدأ في إكمال تلك التوابع‪ ،‬دعنا نلق نظر ًة‬

‫ومتغيرات الكائنات المنشأة ‪ instance variables‬وباني الصنف ‪:constructor‬‬


‫ِ‬ ‫التالية عىل تعريف الصنف‬

‫{ >‪public class MyArrayList<E> implements List<E‬‬


‫;‪int size‬‬ ‫احتفظ بعدد العناصر ‪//‬‬
‫;‪private E[] array‬‬ ‫ِّ‬
‫خزن العناصر ‪//‬‬

‫{ )(‪public MyArrayList‬‬
‫;]‪array = (E[]) new Object[10‬‬
‫;‪size = 0‬‬
‫}‬
‫}‬

‫يحتفظ المتغير ‪- size‬كما يُ ِّ‬


‫وضح التعليق‪ -‬بع‪ww‬دد العناص‪ww‬ر ال‪ww‬تي يَحمِ له‪ww‬ا ك‪ww‬ائنٌ من الن‪ww‬وع ‪،MyArrayList‬‬

‫بينما يُمثِل المتغير ‪ array‬المصفوفة التي تحتوي عىل تلك العناصر ذاتها‪.‬‬

‫مصفوفة مك َّو ً‬
‫نة من عشرة عناصر تَحمِ ل مبدئ ًّيا القيمة الفارغة ‪ ،null‬كما يَضبُط قيمة المتغير‬ ‫ً‬ ‫نشئ الباني‬
‫يُ ِ‬

‫‪ size‬إىل ‪ .0‬غالبًا ما يكون طول المصفوفة أكبر من قيمة المتغير ‪ ،size‬مما يَعنِي وجود أماكنَ غير ُمس‪ww‬تخدَمةٍ‬

‫في المصفوفة‪.‬‬

‫مالحظة متعلقة بقواعد لغة جافا‬

‫ال ي ُمكِنك إنشاء مصفوفة باستخدام معامل نوع ‪ ،type parameter‬وهكذا فالتعليمة التالية مثاًل لن تَ َ‬
‫عمل‪:‬‬

‫;]‪array = new E[10‬‬

‫‪27‬‬
‫هياكل البيانات للمبرمجين‬ ‫تحليل الخوارزميات‬

‫ً‬
‫مص‪ww‬فوفة من الن‪ww‬وع ‪ ،Object‬ثم تُح‪ِّ ww‬ول نوعه‪ww‬ا‬ ‫لكي تتمكَّن من تخطِ ي تل‪ww‬ك العقب‪ww‬ة‪ ،‬علي‪ww‬ك أن تُ ِ‬
‫نش‪ww‬ئ‬

‫المعممة (باللغة اإلنجليزية)‪.‬‬


‫ّ‬ ‫‪ .typecast‬يُمكِنك قراءة المزيد عن ذلك في ما المقصود باألنواع‬

‫ً‬
‫نظرة اآلن عىل التابع المسؤول عن إضافة العناصر إىل القائمة‪:‬‬ ‫ُلق‬
‫ولن ِ‬

‫{ )‪public boolean add(E element‬‬


‫{ )‪if (size >= array.length‬‬
‫أنشئ مصفوفة أكبر وانسخ إليها العناصر ‪//‬‬
‫;]‪E[] bigger = (E[]) new Object[array.length * 2‬‬
‫;)‪System.arraycopy(array, 0, bigger, 0, array.length‬‬
‫;‪array = bigger‬‬
‫}‬
‫;‪array[size] = element‬‬
‫;‪size++‬‬
‫;‪return true‬‬
‫}‬

‫سنضطر إىل إنشاء مصفوفةٍ أكبر ن َ َ‬


‫نسخ إليه‪ww‬ا‬ ‫ّ‬ ‫في حالة عدم وجود المزيد من األماكن الشاغرة في المصفوفة‪،‬‬

‫العناصر الموجودة س‪ww‬اب ًقا‪ ،‬وعندئ‪ٍ w‬ذ س‪ww‬نتمكَّن من إض‪ww‬افة العنص‪ww‬ر الجدي‪ww‬د إىل تل‪ww‬ك المص‪ww‬فوفة‪ ،‬م‪ww‬ع زي‪ww‬ادة قيمة‬

‫المتغير ‪.size‬‬

‫ً‬
‫قيمة من النوع ‪ .boolean‬قد ال يكون سبب ذلك واضحًا‪ ،‬فلربما تظن أن‪ww‬ه س‪ww‬يعيد القيم‪ww‬ة‬ ‫يعيد ذلك التابع‬

‫دائما‪ .‬قد ال تتضح لنا الكيفية التي ينبغي أن نُحلِّل أداء التابع عىل أساسها‪ .‬في األح‪ww‬وال العاديّ‪ww‬ة يس‪ww‬تغرق‬
‫ً‬ ‫‪true‬‬

‫نضطر خاللها إىل إعادة ض‪ww‬بط حجم المص‪ww‬فوفة سيس‪ww‬تغرِق زمنً‪ww‬ا خط ًي‪ww‬ا‪.‬‬
‫ّ‬ ‫التابع زمنًا ثابتًا‪ ،‬ولكنه في الحاالت التي‬

‫وسنتطرّق إىل كيفية معالجة ذلك في القسم ‪.3.2‬‬

‫ُلق نظر ًة عىل التابع ‪ ،get‬وبعدها يُمكِنك البدء في حل التمرين‪:‬‬


‫في األخير‪ ،‬لن ِ‬

‫{ )‪public T get(int index‬‬


‫{ )‪if (index < 0 || index >= size‬‬
‫;)(‪throw new IndexOutOfBoundsException‬‬
‫}‬
‫;]‪return array[index‬‬
‫}‬

‫بسيط للغاية ويعمل كم‪ww‬ا يلي‪ :‬إذا ك‪ww‬ان الفه‪ww‬رس المطل‪ww‬وب خ‪ww‬ارج نط‪ww‬اق المص‪ww‬فوفة‪،‬‬
‫ٌ‬ ‫كما نرى‪ ،‬فالتابع ‪get‬‬

‫فس ُيبلِّغ التابع عن اعتراض ‪exception‬؛ أما إذا ضمن نطاق المصفوفة‪ ،‬ف‪ww‬إن الت‪w‬ابع يس‪w‬ترجع عنص‪w‬ر المص‪ww‬فوفة‬

‫‪28‬‬
‫هياكل البيانات للمبرمجين‬ ‫تحليل الخوارزميات‬

‫ويعيده‪ .‬الحِ ظ أن التابع يَفحَص م‪ww‬ا إذا ك‪ww‬انت قيم‪ww‬ة الفه‪ww‬رس أق‪ww‬ل من قيم‪ww‬ة ‪ size‬ال قيم‪ww‬ة ‪،array.length‬‬

‫المستخدَمة‪.‬‬
‫وبالتالي ال يعيد التابع قيم عناصر المصفوفة غير ُ‬

‫ستجد التابع ‪ set‬في الملف ‪ MyArrayList.java‬عىل النحو التالي‪:‬‬

‫{ )‪public T set(int index, T element‬‬


‫‪// TODO: fill in this method.‬‬
‫;‪return null‬‬
‫}‬

‫اق‪ww‬رأ توثي‪ww‬ق ‪ set‬باللغ‪ww‬ة اإلنجليزية‪ ،‬ثم أكم‪ww‬ل متن الت‪ww‬ابع‪ .‬ال ب ُ ‪ّ w‬د أن ينجح االختب‪ww‬ار ‪ testSet‬عن‪ww‬دما تُش ‪ِّ w‬غل‬
‫‪ً MyArrayListTest‬‬
‫مرة أخرى‪.‬‬

‫تجنَّب تكرار الشيفرة المسؤولة عن فحص الفهرس‪.‬‬

‫الخطوة التالية هي إكمال التابع ‪ ،indexOf‬وبالمثل نحيلك إىل مقالة توثيق الت‪ww‬ابع ‪ List indexOf‬لتقرأه‪ww‬ا‬

‫أواًل وذلك لتعرف ما ينبغي عليك القيام به‪ .‬وأعِ ر انتباهًا لكيفية معالجته للقيمة الفارغة ‪.null‬‬

‫و َّفرنا لك ً‬
‫أيضا التابع المساعد ‪ equals‬للموازنة بين قيمة عنصر ضمن المصفوفة وبين قيمة معينة أخ‪ww‬رى‪.‬‬

‫يعيد ذلك التابع القيمة ‪ true‬إذا كانت القيمتان متساويتين كما يُعالِج القيمة الفارغة ‪ null‬بشكل س‪ww‬ليم‪ .‬الحِ ‪ w‬ظ‬

‫المع‪ww‬دِّل ‪private‬؛ ألن‪ww‬ه ليس ج‪ww‬زءًا من الواجه‪ww‬ة ‪ ،List‬ويُس‪ww‬تخدَم فقط‬


‫أن ه‪ww‬ذا الت‪ww‬ابع ُمع‪ww‬رَّف باس‪ww‬تخدام ُ‬
‫داخل الصنف‪.‬‬

‫ش‪ِّ wwww‬غل االختب‪wwww‬ار ‪ MyArrayListTest‬م‪wwww‬رة أخ‪wwww‬رى عن‪wwww‬دما تنتهي‪ ،‬واآلن ينبغي أن ينجح االختب‪wwww‬ار‬

‫‪ testIndexOf‬وكذلك االختبارات األخرى التي تعتمد عليه‪.‬‬

‫ما يزال هناك تابع‪ww‬ان آخ‪ww‬ران علي‪ww‬ك إكمالهم‪ww‬ا لكي تنتهي من التم‪ww‬رين‪ ،‬حيث أن الت‪ww‬ابع األول ه‪ww‬و عب‪ww‬ارة عن‬

‫‪w‬طر أثن‪w‬اء ذل‪w‬ك إىل‬ ‫ً‬


‫قيم‪w‬ة جدي‪w‬دة‪ .‬ق‪w‬د تض‪ّ w‬‬ ‫فهرسا وتُخ‪w‬زِّن في‪ww‬ه‬
‫ً‬ ‫بصمة أخرى من التابع ‪ .add‬تَستق ِبل تلك البصمة‬

‫تحريك العناصر األخرى لكي تُو ِّفر مكانًا للعنصر الجديد‪.‬‬

‫مثلما سبق‪ ،‬اقرأ التوثيق باللغ‪ww‬ة اإلنجليزية أواًل ثم ن ِّفذ الت‪ww‬ابع‪ ،‬بع‪w‬دها ش‪ِّ w‬غل االختب‪ww‬ارات لكي تتأ ّك‪ww‬د من أن‪ww‬ك‬

‫تنفيذك سليم‪.‬‬

‫تجنَّب تكرار الشيفرة المسؤولة عن زيادة‪/‬إعادة ضبط حجم المصفوفة‪.‬‬

‫لننتق‪www‬ل اآلن إىل الت‪www‬ابع األخ‪www‬ير‪ :‬أكم‪www‬ل متن الت‪www‬ابع ‪ .remove‬اق‪www‬رأ أواًل التوثي‪www‬ق باللغ‪www‬ة اإلنجليزية‬

‫‪ ،http://thinkdast.com/listrem‬وعندما تنتهي من إكمال هذا التابع‪ ،‬فالمتوقع أن تنجح جميع االختبارات‪.‬‬

‫عمل بكفاءة‪ ،‬يُمكِنك االطالع عىل الشيفرة الّلتي كتبها المؤلف‪.‬‬


‫نهي جميع التوابع وتتأ َّكد من أنها تَ َ‬
‫بعد أن تُ ِ‬

‫‪29‬‬
‫‪ .3‬قائمة المصفوفة ‪ArrayList‬‬

‫يضرب هذا الفصل عصفورين بحجر ٍ واحدٍ‪ ،‬حيث س‪ww‬نحل في‪ww‬ه تم‪ww‬رين الفص‪ww‬ل الس‪ww‬ابق‪ ،‬وس‪ww‬نتطرق لوس‪ww‬يلة‬

‫يسمى التحليل بالتسديد ‪.amortized analysis‬‬


‫ّ‬ ‫نصنّف من خاللها الخوارزميات باستخدام ما‬

‫‪ 3.1‬تصنيف توابع الصنف ‪MyArrayList‬‬


‫يُمكِننا تحديد ترتيب نمو ‪ order of growth‬غالبية التوابع بالنظر إىل شيفرتها‪ .‬عىل سبيل المثال‪ ،‬انظر إىل‬

‫المعرَّف بالصنف ‪:MyArrayList‬‬


‫تنفيذ التابع ‪ُ get‬‬

‫{ )‪public E get(int index‬‬


‫{ )‪if (index < 0 || index >= size‬‬
‫;)(‪throw new IndexOutOfBoundsException‬‬
‫}‬
‫;]‪return array[index‬‬
‫}‬

‫تستغرِق كل تعليمة من تعليمات التابع ‪ get‬زمنًا ثابتًا‪ ،‬وبن‪ww‬ا ًء عىل ذل‪ww‬ك يس‪ww‬تغرِق الت‪ww‬ابع ‪ get‬في المجم‪ww‬ل‬

‫زمنًا ثابتًا‪.‬‬

‫اآلن وقد صنّفنا التابع ‪ ،get‬يمكننا بنفس الطريقة أن نصنّف الت‪w‬ابع ‪ set‬ال‪w‬ذي يَس‪w‬تخدِمه‪ .‬انظ‪w‬ر إىل تنفي‪w‬ذ‬

‫التابع ‪ set‬من التمرين السابق الذي مرّ بنا في الفصل الثاني‪:‬‬

‫{ )‪public E set(int index, E element‬‬


‫;)‪E old = get(index‬‬
‫;‪array[index] = element‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫;‪return old‬‬
‫}‬

‫ً‬
‫صراحة‪ ،‬فهو يعتم‪ww‬د في ذل‪ww‬ك عىل اس‪ww‬تدعائه للت‪ww‬ابع‬ ‫ربما الحظت أن التابع ‪ set‬ال يفحص نطاق المصفوفة‬

‫‪ get‬الذي يُبلِّغ عن اعتراض ‪ exception‬عندما ال يكون الفهرس صالحًا‪.‬‬

‫تَستغرق كل تعليمة من تعليمات التابع ‪- set‬بما في ذلك استدعاؤه للتابع ‪ -get‬زمنًا ثابتًا‪ ،‬وعليه يُع ّد التابع‬

‫‪ set‬ثابت الزمن ً‬
‫أيضا‪.‬‬

‫ولننتقل اآلن إىل بعض التوابع الخط ّية‪ .‬انظر مثال ً إىل تنفيذنا للتابع ‪:indexOf‬‬

‫{ )‪public int indexOf(Object target‬‬


‫{ )‪for (int i = 0; i<size; i++‬‬
‫{ ))]‪if (equals(target, array[i‬‬
‫;‪return i‬‬
‫}‬
‫}‬
‫;‪return -1‬‬
‫}‬

‫ي في تل‪w‬ك الحلق‪ww‬ة يس‪w‬تدعي الت‪w‬ابعَ‬


‫يحتوي التابع ‪ indexOf‬عىل حلقة تكرارية كما نرى‪ ،‬وفي كل مرورٍ تكرار ٍّ‬
‫‪ .equals‬علينا إ ًذا أن نُصنّف التابع ‪ equals‬أواًل لنتمكن من تصنيف التابع‪ .indexOf‬لننظر إىل تعريف‬

‫ذلك التابع‪:‬‬

‫{ )‪private boolean equals(Object target, Object element‬‬


‫{ )‪if (target == null‬‬
‫;‪return element == null‬‬
‫{ ‪} else‬‬
‫;)‪return target.equals(element‬‬
‫}‬
‫}‬

‫يستدعي الت‪ww‬ابعُ الس‪ww‬ابق الت‪ww‬ابعَ ‪ target.equals‬ال‪ww‬ذي يعتم‪ww‬د زمن تنفي‪ww‬ذه عىل حجم المتغ‪ww‬ير ‪target‬‬

‫و‪ ،element‬ولكن‪www‬ه ال يعتم‪www‬د عىل حجم المص‪www‬فوفة‪ ،‬ول‪www‬ذلك س‪www‬نَ ُعدّه ث‪www‬ابت ال‪www‬زمن لكي نُكمِ‪ www‬ل تحليل‬

‫التابع ‪.indexOf‬‬

‫لنعُ د اآلنَ إىل التابع ‪ .indexOf‬تَستغرق كل تعليمة ضمن الحلقة زمنًا ثابتًا‪ ،‬مما يقودنا إىل الس‪ww‬ؤال الت‪ww‬الي‪:‬‬

‫كم عدد مرات تنفيذ الحلقة؟‬

‫‪31‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫إذا حالفنا الحظ‪ ،‬قد نج‪ww‬د الك‪ww‬ائن المطل‪ww‬وب مباش‪ً w‬‬


‫‪w‬رة ونع‪ww‬ود بع‪ww‬د اختب‪ww‬ار عنص‪ww‬ر واح‪ww‬د فق‪ww‬ط؛ أم‪ww‬ا إذا لم نكن‬

‫محظوظين‪ ،‬فقد نضطرّ الختبار جميع العناصر‪ .‬لنقلْ إننا سنحتاج وسط ًّيا إىل اختبار نصف عدد العناص‪ww‬ر‪ ،‬ومن ثم‬

‫أيضا باستثناء الحالة األقل احتمااًل ‪ ،‬والتي يك‪ww‬ون فيه‪ww‬ا العنص‪ww‬ر‬


‫ي ً‬‫خط ٌ‬
‫يمكن القول بأن هذا التابع يصنّف بأنه تابع ّ‬
‫المطلوب هو أول عنصر في المصفوفة‪.‬‬

‫وهكذا يتشابه تحليل التابع ‪ remove‬مع التابع السابق‪ .‬وفيما يلي تنفيذه‪:‬‬

‫{ )‪public E remove(int index‬‬


‫;)‪E element = get(index‬‬
‫{ )‪for (int i=index; i<size-1; i++‬‬
‫;]‪array[i] = array[i+1‬‬
‫}‬
‫;‪size--‬‬
‫;‪return element‬‬
‫}‬

‫ً‬
‫بداي‪w‬ة من الفه‪w‬رس‬ ‫يَستدعِ ي الت‪w‬ابعُ الس‪w‬ابق الت‪w‬ابعَ ‪ get‬ذا ال‪w‬زمن الث‪w‬ابت‪ ،‬ثم يُم‪w‬رّ ع‪w‬بر عناص‪w‬ر المص‪w‬فوفة‬

‫‪ .index‬وإذا حذفنا العنصر الموجود في نهاية القائمة‪ ،‬فلن يُن ِّفذ الت‪ww‬ابع حلق‪ww‬ة التك‪ww‬رار عىل اإلطالق‪ ،‬وسيس‪ww‬تغرِق‬

‫التابع عندئ ٍذ زمنًا ثابتًا‪ .‬في المقابل‪ ،‬إذا حذفنا العنصر األول فسيمرّ التابع عبر جميع العناص‪ww‬ر المتبقي‪ww‬ة‪ ،‬وبالت‪ww‬الي‬

‫سيستغرِق التابع زمنًا خط ًيا‪ .‬لذلك يُمكِننا أن نَ ُع ّد التابع خط ًيا في المجمل‪ ،‬باس‪w‬تثناء الحال‪ww‬ة الخاص‪ww‬ة ال‪ww‬تي يك‪w‬ون‬

‫خاللها العنصر المطلوب حذفه واقعً ا في نهاية المصفوفة أو عىل بعد مسافةٍ ثابتةٍ من نهايتها‪.‬‬

‫‪ 3.2‬تصنيف التابع ‪add‬‬


‫فهرسا وعنص ًرا كمعامالت ‪:parameters‬‬
‫ً‬ ‫تستقبل النسخة التالية من التابع ‪add‬‬

‫{ )‪public void add(int index, E element‬‬


‫{ )‪if (index < 0 || index > size‬‬
‫;)(‪throw new IndexOutOfBoundsException‬‬
‫}‬
‫عنصرا للتأ ّكد من ضبط حجم المصفوفة ‪//‬‬
‫ً‬ ‫أضف‬
‫;)‪add(element‬‬

‫حرك العناصر الأخرى ‪//‬‬


‫{ )‪for (int i=size-1; i>index; i--‬‬
‫;]‪array[i] = array[i-1‬‬
‫}‬

‫‪32‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫ضع العنصر الجديد في المكان الصحيح ‪//‬‬


‫;‪array[index] = element‬‬
‫}‬

‫َ‬
‫النسخة ذات المعام‪ww‬ل الواح‪ww‬د )‪ add(E‬أواًل لكي تض‪ww‬ع‬ ‫ُ‬
‫النسخة ذات المعاملين )‪add(int, E‬‬ ‫تستدعي‬

‫العنصر الجديد في نهاية المصفوفة‪ ،‬وبعد ذلك تُح‪w‬رِّك العناص‪ww‬ر األخ‪ww‬رى إىل اليمين‪ ،‬وتض‪ww‬ع العنص‪ww‬ر الجدي‪ww‬د في‬

‫المكان الصحيح‪.‬‬

‫س‪www‬نُحلّل أواًل زمن النس‪www‬خة ذات المعام‪www‬ل الواح‪www‬د )‪ add(E‬قب‪www‬ل أن ننتق‪www‬ل إىل تحلي‪www‬ل النس‪www‬خة ذات‬

‫المعاملين )‪:add(int, E‬‬

‫{ )‪public boolean add(E element‬‬


‫{ )‪if (size >= array.length‬‬
‫أنشئ مصفوفة أكبر وانسخ العناصر إليها ‪//‬‬
‫;]‪E[] bigger = (E[]) new Object[array.length * 2‬‬
‫;)‪System.arraycopy(array, 0, bigger, 0, array.length‬‬
‫;‪array = bigger‬‬
‫}‬
‫;‪array[size] = element‬‬
‫;‪size++‬‬
‫;‪return true‬‬
‫}‬

‫تتضح لنا هنا صعوبة تحليل زمن النسخة ذات المعامل الواحد؛ ألنه لو كانت هناك مس‪ww‬احة غ‪ww‬ير ُمس‪ww‬تخدَمةٍ‬

‫في المصفوفة‪ ،‬فسيستغرِق التابع زمنًا ثابتًا؛ أما لو اضطرّرنا إلعادة ض‪ww‬بط حجم المص‪ww‬فوفة‪ ،‬فسيس‪ww‬تغرِق الت‪ww‬ابع‬

‫زمنًا خط ًيا؛ ألن التابع ‪ System.arraycopy‬يستغرِق بدوره زمنًا يتناسب مع حجم المصفوفة‪.‬‬

‫إذا ً فهل هذا التابع ثابت أم خطي؟ يُمكِننا أن نُصن ِّفه بالتفكير في متوسط عدد العمليات التي تتطلَّبها عملية‬
‫ً‬
‫مص‪ww‬فوفة بإمكانه‪ww‬ا تخ‪ww‬زين‬ ‫اإلض‪ww‬افة خالل ع‪ww‬دد من اإلض‪ww‬افات مق‪ww‬داره ‪ .n‬وس‪ww‬نفترض للتبس‪ww‬يط ب‪ww‬أن ل‪ww‬دينا‬

‫عنصرين فقط‪.‬‬

‫ً‬
‫‪w‬اغرة في المص‪ww‬فوفة‪ ،‬وس‪ُ w‬يخزِّن‬ ‫في المرة األوىل التي سنستدعي خاللها ‪ ،add‬س‪ww‬يجد الت‪ww‬ابع مس‪ً w‬‬
‫‪w‬احة ش‪w‬‬ ‫•‬

‫عنصرًا واحدًا‪.‬‬

‫ً‬
‫مساحة شاغر ًة في المصفوفة‪ ،‬وس ُيخزِّن عنصرًا واحدًا‪.‬‬ ‫في المرة الثانية‪ ،‬سيجد التابع‬ ‫•‬

‫في المرة الثالثة‪ ،‬سيعيد التابع ضبط حجم المصفوفة‪ ،‬وينس‪ww‬خ العنص‪ww‬رين الس‪ww‬ابقين‪ ،‬ثم يخ‪ww‬زن العنص‪ww‬ر‬ ‫•‬

‫الجديد وهو الثالث‪ ،‬وس ُيص ِبح بإمكان المصفوفة تخزين ‪ 4‬عناصر في المجمل‪.‬‬

‫‪33‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫ستخُزِّن المرة الرابعة عنصرًا واحدًا‪.‬‬ ‫•‬

‫ستعيد المرة الخامسة ضبط حجم المصفوفة‪ ،‬وتنسخ أربعة العناصر السابقة‪ ،‬وتخزّن عنصرًا جدي ‪w‬دًا وه‪ww‬و‬ ‫•‬

‫الخامس‪ ،‬وسيكون في إمكان المصفوفة تخزين ثمانية عناصر إجمااًل ‪.‬‬

‫ستُخزِّن المرات الثالث التالية ثالثة عناصر‪.‬‬ ‫•‬

‫ستنسخ المرة التالية ثماني‪ww‬ة العناص‪ww‬ر الس‪ww‬ابقة وتُخ‪w‬زِّن عنص‪w‬رًا جدي‪w‬دًا وه‪ww‬و التاس‪ww‬ع‪ ،‬وس ُيص‪ِ w‬بح بإمك‪ww‬ان‬ ‫•‬

‫المصفوفة تخزين ستة عشر عنصرًا‪.‬‬

‫َ‬
‫سبعة عناصر وهكذا دواليك‪.‬‬ ‫ستُخزِّن سبعُ المرات التالية‬ ‫•‬

‫وإذا أردنا أن نلخص ما سبق‪:‬‬

‫فإننا بعد ‪ 4‬إضافات‪ ،‬سنكون قد خزَّنا ‪ 4‬عناصر ونسخنا عنصرين‪.‬‬ ‫•‬

‫بعد ‪ 8‬إضافات‪ ،‬سنكون قد خزَّنا ‪ 8‬عناصر ونسخنا ‪ 6‬عناصر‪.‬‬ ‫•‬

‫ً‬
‫إضافة‪ ،‬سنكون قد خزَّنا ‪ 16‬عنص ًرا ونسخنا ‪ 14‬عنص ًرا‪.‬‬ ‫بعد ‪16‬‬ ‫•‬

‫يُفترَض أن تكون قد استقرأت سير العملية وحصلت عىل ما يلي‪ :‬لكي نُضيف عدد مق‪ww‬داره ‪ n‬من العناص‪ww‬ر‪،‬‬

‫سنضطرّ إىل تخزين عدد ‪ n‬من العناصر ونسخ عدد ‪ n-2‬من العناصر‪ ،‬وبالتالي يكون عدد العمليات اإلجم‪ww‬الي ه‪ww‬و‬

‫‪ n+n-2‬أي ‪.2n-2‬‬

‫نقسم الع‪ww‬دد الكلي للعملي‪ww‬ات عىل‬


‫ِّ‬ ‫لكي نحسب متوسط عدد العمليات المطلوبة لعملية اإلضافة‪ ،‬ينبغي أن‬

‫عدد اإلضافات ‪ ،n‬وبذلك ستكون النتيجة هي ‪ .2-2/n‬الحِ ظ أنه كلما ازدادت قيمة ‪ ،n‬ستقل قيم‪ww‬ة الج‪ww‬زء الث‪ww‬اني‬

‫األس األكبر لألساس ‪ ،n‬ف ُيمكِننا أن ن َ ُع ّد التابع ‪ add‬ثابت الزمن‪.‬‬


‫ّ‬ ‫‪ .2/n‬ونظرا ً ألن ما يهمنا هنا هو‬

‫قد يبدو من الغريب لخوارزمية تحتاج إىل زمن خطي أحيانًا أن تكون ثابتة الزمن في المتوس‪ww‬ط‪ .‬والفك‪ww‬رة هي‬

‫أننا نضاعف طول المصفوفة في كل مرة نضطرّ فيها إىل إعادة ضبط حجمها‪ .‬يُقلِّل ذلك عدد المرات التي نَن َ‬
‫َس‪ww‬خ‬

‫خاللها جميع العناصر‪ ،‬أما لو كنا نضيف مقدا ًرا ثابتًا إىل طول المصفوفة بداًل من مضاعفتها بمق‪ww‬دارٍ ث‪ww‬ابت‪ ،‬ف‪ww‬إنّ‬

‫هذا التحليل ال يصلح‪.‬‬

‫يُطلَق عىل تصنيف الخوارزميات وف ًقا لتلك الطريقة ‪-‬أي بحساب متوسط الزمن الذي تستغرقه متتالي‪ww‬ة من‬

‫االستدعاءات‪ -‬باسم التحليل بالتسديد‪ ،‬والذي يُمكِنك ق‪ww‬راءة المزي‪ww‬د عن‪ww‬ه في مق‪ww‬ال م‪ww‬ا ه‪ww‬و التحلي‪ww‬ل بالتس‪ww‬ديد؟‬

‫(باللغة اإلنجليزية)‪ .‬تتلخص فكرته األساسية في توزيع‪/‬تسديد التكلفة اإلضافية لنسخ المصفوفة عبر سلس‪ww‬لة من‬

‫االستدعاءات‪.‬‬

‫‪add(int,‬؟ يُن ِّفذ الت‪www‬ابع‬ ‫اآلن وق‪www‬د عرفن‪www‬ا أنّ الت‪www‬ابع )‪ add(E‬ث‪www‬ابت ال‪www‬زمن‪ ،‬م‪www‬اذا عن الت‪www‬ابع )‪E‬‬

‫تمرّ عبر جز ٍء من عناصر المصفوفة وتُحرِّكها‪ .‬تستغرِق تلك‬ ‫ً‬


‫حلقة ُ‬ ‫)‪- add(int, E‬بعد استدعائه للتابع )‪-add(E‬‬

‫‪34‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫الحلق‪ww‬ة زمنً‪ww‬ا خط ًي‪ww‬ا باس‪ww‬تثناء الحال‪ww‬ة ال‪ww‬تي نض‪ww‬يف خالله‪ww‬ا عنص‪ww‬رًا إىل نهاي‪ww‬ة المص‪ww‬فوفة‪ ،‬وعلي‪ww‬ه يك‪ww‬ون‬

‫التابع )‪ add(int, E‬بدوره خط ًيا‪.‬‬

‫‪ 3.3‬حجم المشكلة‬
‫ولننتق‪ww‬ل اآلن إىل المث‪ww‬ال األخ‪ww‬ير في ه‪ww‬ذا الفص‪ww‬ل‪ .‬انظ‪ww‬ر فيم‪ww‬ا يلي إىل تنفي‪ww‬ذ الت‪ww‬ابع ‪ removeAll‬ض‪ww‬من‬

‫الصنف ‪:MyArrayList‬‬

‫{ )‪public boolean removeAll(Collection<?> collection‬‬


‫;‪boolean flag = true‬‬
‫{ )‪for (Object obj: collection‬‬
‫;)‪flag &= remove(obj‬‬
‫}‬
‫;‪return flag‬‬
‫}‬

‫يستدعِ ي التابع ‪ removeAll‬في كلّ تك‪ww‬رارٍ ض‪ww‬من الحلق‪ww‬ة الت‪ww‬ابعَ ‪ remove‬ال‪ww‬ذي يس‪ww‬تغرِق زمنً‪ww‬ا ّ‬
‫خط ًّيا‪ .‬ق‪ww‬د‬

‫يدفعك ذلك إىل الظن بأنّ التابعَ ‪ removeAll‬تربيعي‪ ،‬ولكن ليس بالضرورة أن يكون كذلك‪.‬‬

‫يُن ِّفذ التابع ‪ removeAll‬الحلقة مر ًة واحد ًة لكل عنصر في المتغير ‪ .collection‬فإذا كان المتغير يحت‪ww‬وي‬
‫عىل عدد ‪ m‬من العناصر‪ ،‬وكانت القائمة التي نح ِذف منها العنصر مك َّو ً‬
‫نة من عدد ‪ n‬من العناصر‪ ،‬فإن هذا الت‪ww‬ابع‬

‫ينتمي إىل المجموعة )‪ .O(nm‬لو افترض‪w‬نا أن حجم ‪ collection‬ث‪w‬ابت‪ ،‬فس‪w‬يكون الت‪w‬ابع ‪ removeAll‬خط ًي‪w‬ا‬

‫بالنس‪ww‬بة لـ ‪ ،n‬ولكن إذا ك‪ww‬ان حجم ‪ collection‬متناس ‪w‬بًا م‪ww‬ع ‪ ،n‬فس‪ww‬يكون الت‪ww‬ابع ‪ removeAll‬تربيع ًي‪ww‬ا‪ .‬عىل‬

‫دائما عىل ‪ 100‬عنصر أو أق‪ww‬ل‪ ،‬ف‪ww‬إن الت‪ww‬ابع ‪ removeAll‬يس‪ww‬تغرِق‬


‫ً‬ ‫سبيل المثال‪ ،‬إذا كان ‪ collection‬يحتوي‬

‫زمنًا خط ًيا؛ أما إذا كان ‪ collection‬يحتوي في العموم عىل ‪ %1‬من عناصر القائمة‪ ،‬ف‪ww‬إن الت‪ww‬ابع ‪removeAll‬‬

‫يستغرِق زمنًا تربيع ًيا‪.‬‬

‫عند الحديث عن حجم المشكلة‪ ،‬ينبغي أن ننتبه إىل ماهية الحجم أو األحجام التي نحن بص‪ww‬ددها‪ .‬ي‪ww‬بين ه‪ww‬ذا‬

‫المثال إحدى مشاكل تحليل الخوارزميات‪ ،‬وهي االختصار المغري الناجم عن ع ّد الحلقات‪ ،‬ففي حالة وجود حلق‪ww‬ة‬

‫واحدة‪ ،‬غالبًا ما تكون الخوارزمية خطية‪ ،‬وفي حالة وجود حلقتين متداخلتين‪ ،‬فغالبًا ما تكون الخوارزمي‪ww‬ة تربيعي‪ww‬ة‪،‬‬

‫ولكن انتبه وفكر أواًل في عدد مرات تنفيذ كل حلقة‪ ،‬فإذا كان عددها يتناسب م‪ww‬ع ‪ n‬لجمي‪ww‬ع الحلق‪ww‬ات‪ ،‬فيمكن‪ww‬ك‬

‫دائما مع ‪- n‬كما هو الحال في هذا المثال‪ -‬فعليك أن تتريث‬


‫ً‬ ‫االكتفاء بع ّد الحلقات؛ أما إذا لم يكن عددها متناسبًا‬

‫وتمنح األمر مزيدًا من التفكير‪.‬‬

‫‪35‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫‪ 3.4‬هياكل البيانات المرتابطة ‪linked data structures‬‬


‫‪w‬ة مترابط‪ً w‬‬
‫‪w‬ة ‪linked list‬‬ ‫سنقدم في التمرين التالي تنفي ًذا جزئ ًيا للواجهة ‪ .List‬يَس‪ww‬تخدِم ه‪ww‬ذا التنفي‪ww‬ذ قائم‪ً w‬‬

‫لتخزين العناصر‪ ،‬وإذا لم تكن لديك فكرة عن القوائم المترابطة‪ ،‬ف ُيمكِنك القراءة عنها في مقال القوائم المترابطة‪،‬‬

‫وسنتطرق لها باختصار هنا‪.‬‬

‫ً‬
‫ع‪w‬ادة اس‪ww‬م عُق‪w‬د ‪ ،nodes‬حيث تحت‪w‬وي‬ ‫مترابطا إذا كان ُمؤل ًفا من كائن‪ww‬ات يُطلَ‪w‬ق عليه‪w‬ا‬
‫ً‬ ‫يُع ّد هيكل البيانات‬

‫العقد عىل مراجع ‪ references‬تشير إىل عقد أخرى‪ .‬وفي الق‪ww‬وائم المترابط‪ww‬ة‪ ،‬تحت‪ww‬وي ك‪ww‬ل عق‪ww‬دة عىل مرج‪ww‬ع إىل‬

‫أنواع أخرى من هياكل البيان‪ww‬ات المترابط‪ww‬ة عىل مراج‪ww‬ع تش‪ww‬ير إىل‬


‫ٍ‬ ‫العقدة التالية في القائمة‪ .‬قد تحتوي العقد في‬
‫ُ‬
‫والشعب ‪.graphs‬‬ ‫عدة عقد‪ ،‬مثل األشجار ‪trees‬‬

‫بسيطا لصنف عقدة‪:‬‬


‫ً‬ ‫تعرض الشيفرة التالية تنفي ًذا‬

‫{ ‪public class ListNode‬‬

‫;‪public Object data‬‬


‫;‪public ListNode next‬‬

‫{ )(‪public ListNode‬‬
‫;‪this.data = null‬‬
‫;‪this.next = null‬‬
‫}‬

‫{ )‪public ListNode(Object data‬‬


‫;‪this.data = data‬‬
‫;‪this.next = null‬‬
‫}‬

‫{ )‪public ListNode(Object data, ListNode next‬‬


‫;‪this.data = data‬‬
‫;‪this.next = next‬‬
‫}‬

‫{ )(‪public String toString‬‬


‫;")" ‪return "ListNode(" + data.toString() +‬‬
‫}‬
‫}‬

‫‪36‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫‪w‬ع يش‪ww‬ير إىل ك‪ww‬ائن‬


‫يتضمن الصنف ‪ ListNode‬متغيري نسخة هما‪ data :‬و‪ .next‬يحتوي ‪ data‬عىل مرج‪ٍ w‬‬
‫َّ‬
‫ما من النوع ‪ ،Object‬بينما يحتوي ‪ next‬عىل مرجع يشير إىل العق‪ww‬دة التالي‪ww‬ة في القائم‪ww‬ة‪ .‬ويحت‪ww‬وي ‪ next‬في‬

‫العقدة األخيرة من القائمة عىل القيمة الفارغة ‪ null‬كما هو متعارف عليه‪.‬‬

‫ً‬
‫مجموع‪ww‬ة من الب‪ww‬واني ‪ constructors‬ال‪ww‬تي تُمكِّن‪ww‬ك من تمري‪ww‬ر قيمٍ مبدئي‪ww‬ةٍ‬ ‫يُع‪ww‬رِّف الص‪ww‬نف ‪ListNode‬‬

‫للمتغيرين ‪ data‬و‪ ،next‬أو تمكّنك من مجرد تهيئتهما إىل القيمة االفتراضية ‪ .null‬ويُمكِنك أن تُفكِر في عقد ٍة‬
‫قائمة ُمك َّون‪ٌ w‬‬
‫‪w‬ة من عنص‪ww‬ر ٍ واح‪w‬دٍ‪ ،‬ولكن عىل العم‪ww‬وم‪ ،‬يُمكِن ألي قائم‪ww‬ة أن‬ ‫ٌ‬ ‫واحد ٍة من النوع ‪ ListNode‬كما لو أنها‬

‫تحتوي عىل أي عدد من العقد‪.‬‬

‫هناك الكثير من الطرق المستخدمة إلنشاء قائمة جدي‪w‬دة‪ ،‬وتتك‪َّ w‬ون إح‪w‬داها من إنش‪w‬اء مجموع‪w‬ة من كائن‪w‬ات‬

‫الصنف ‪ ListNode‬عىل النحو التالي‪:‬‬

‫;)‪ListNode node1 = new ListNode(1‬‬


‫;)‪ListNode node2 = new ListNode(2‬‬
‫;)‪ListNode node3 = new ListNode(3‬‬

‫ثم ربطها ببعض‪:‬‬

‫;‪node1.next = node2‬‬
‫;‪node2.next = node3‬‬
‫;‪node3.next = null‬‬

‫نشئ عقد ًة وتربطها في نفس الوقت‪ .‬عىل سبيل المث‪ww‬ال‪ ،‬إذا أردت أن تض‪ww‬يف‬
‫وهناك طريقة أخرى هي أن تُ ِ‬

‫عقد ًة جديد ًة إىل بداية قائمة‪ ،‬يُمكِنك كتابة ما يلي‪:‬‬

‫;)‪ListNode node0 = new ListNode(0، node1‬‬

‫ُ‬
‫أربعة عق ٍد تحتوي عىل األعداد الصحيحة ‪ 0‬و ‪ 1‬و ‪2‬‬ ‫واآلن‪ ،‬بعد تنفيذ سلسلة التعليمات السابقة‪ ،‬أصبح لدينا‬
‫ٌ‬
‫ومربوطة معً ا بترتيب تصاعدي‪ .‬الحِ ظ أن قيم‪ww‬ة ‪ next‬في العق‪ww‬دة األخ‪ww‬يرة تحت‪ww‬وي عىل القيم‪ww‬ة‬ ‫و ‪ 3‬مثل بيانات‪،‬‬

‫الفارغة ‪.null‬‬

‫‪37‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫لك‪w‬ائن يُ ِّ‬
‫وض‪w‬ح المتغ‪w‬يرات والكائن‪w‬ات ال‪w‬تي تش‪w‬ير إليه‪w‬ا تل‪w‬ك‬ ‫ٍ‬ ‫الرسمة التوض‪w‬يحية الس‪w‬ابقة هي رس‪w‬م بي‪w‬انيٌّ‬
‫وضح ما تشير إلي‪ww‬ه المتغ‪ww‬يرات‪ ،‬بينم‪ww‬ا تَظهَ‪ w‬ر‬
‫صناديق مع أسهمٍ تُ ِّ‬
‫َ‬ ‫المتغيرات‪ .‬تَظهَ ر المتغيرات بهيئة أسما ٍء داخل‬

‫الكائنات بهيئة صناديق تَ ِ‬


‫جد خارجها النوع الذي تنتمي إليه (مث‪ww‬ل ‪ ListNode‬و‪ ،)Integer‬وداخله‪ww‬ا متغ‪ww‬يرات‬

‫المعرَّفة بها‪.‬‬
‫النسخ ُ‬

‫‪ 3.5‬تمرين ‪3‬‬
‫ستجد ملفات الشيفرة المطلوبة لهذا التمرين في مستودع الكتاب‪.‬‬

‫ً‬
‫مترابط‪w‬ة لتخ‪w‬زين‬ ‫ً‬
‫قائم‪w‬ة‬ ‫‪ :MyLinkedList.java‬يحتوي عىل تنفيذ جزئي للواجه‪w‬ة ‪ ،List‬ويَس‪w‬تخدِم‬ ‫•‬

‫العناصر‪.‬‬

‫‪ :MyLinkedListTest.java‬يحتوي عىل اختبارات ‪ JUnit‬للصنف ‪.MyLinkedList‬‬ ‫•‬

‫ن ِّفذ األمر ‪ ant MyArrayList‬لتشغيل ‪ MyArrayList.java‬الذي يحتوي عىل عدة اختبارات بسيطة‪.‬‬

‫‪ ant‬لتش‪ww‬غيل اختب‪ww‬ارات ‪ JUnit‬ال‪ww‬تي سيفش‪ww‬ل البعض منه‪ww‬ا‪ .‬إذا نظ‪ww‬رت إىل‬ ‫ثم ن ِّفذ ‪MyArrayListTest‬‬

‫الشيفرة‪ ،‬ستجد ثالثة تعليقات ‪ TODO‬لإلشارة إىل التوابع التي ينبغي عليك إكمالها‪.‬‬

‫المعرَّف‪wwww‬ة في‬
‫لننظ‪wwww‬ر إىل بعض أج‪wwww‬زاء الش‪wwww‬يفرة قب‪wwww‬ل أن تب‪wwww‬دأ‪ .‬انظ‪wwww‬ر إىل المتغ‪wwww‬يرات والب‪wwww‬واني ُ‬
‫الصنف ‪:MyLinkedList‬‬

‫{ >‪public class MyLinkedList<E> implements List<E‬‬

‫;‪private int size‬‬ ‫احتفظ بعدد العناصر ‪//‬‬


‫;‪private Node head‬‬ ‫مرجع إلى العقدة الأولى ‪//‬‬

‫{ )(‪public MyLinkedList‬‬
‫;‪head = null‬‬

‫‪38‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫;‪size = 0‬‬
‫}‬
‫}‬

‫يحتفظ المتغير ‪- size‬كما يُ ِّ‬


‫وضح التعليق‪ -‬بعدد العناصر ال‪ww‬تي يَحمِ له‪ww‬ا ك‪ww‬ائن من الن‪ww‬وع ‪،MyLinkedList‬‬

‫بينما يشير المتغير ‪ head‬إىل العقدة األوىل في القائمة أو يحمل القيمة الفارغة ‪ null‬إذا كانت القائمة فارغة‪.‬‬

‫بمعلومات إضافية في العموم أمرًا جيدًا؛ والسبب‬


‫ٍ‬ ‫ليس من الضروري تخزين عدد العناصر‪ .‬وال يُع ّد االحتفاظ‬

‫تحملنا عبء التحديث المستمر لها‪ ،‬ما قد يتسبَّب في وقوع أخطاء‪ ،‬كما أنه‪ww‬ا تحت‪ww‬ل‬
‫ّ‬ ‫هو أن المعلومات اإلضافية‬

‫حيزًا إضاف ًيا من الذاكرة‪.‬‬

‫ً‬
‫صراحة‪ ،‬فإننا سنتمكَّن من كتابة تنفي ٍذ للتابع ‪ ،size‬بحيث يَستغرِق تنفي‪ww‬ذه زمنً‪ww‬ا ثاب ًت‪ww‬ا؛‬ ‫لكننا لو خزَّنا ‪size‬‬

‫أما لو لم نفعل ذلك‪ ،‬فسنضطرّ إىل المرور عبر القائمة وع ّد عناصرها في كل مرة‪ ،‬وهذا يتطلَّب زمنًا خط ًيا‪.‬‬

‫ً‬
‫صراحة‪ ،‬فإننا سنضطرّ إىل تحديثه في كل مرة نضيف فيها عنصرًا إىل‬ ‫من جهة أخرى‪ ،‬نظرًا ألننا نُخزِّن ‪size‬‬

‫القائمة أو نحذفه منها‪ .‬يؤدي ذلك إىل إبطاء تلك التوابع نوعً ا ما‪ ،‬ولكن‪ww‬ه لن يُ‪ww‬ؤثر عىل ت‪ww‬رتيب النم‪ww‬و الخ‪ww‬اص به‪ww‬ا‪،‬‬

‫ولذلك فلربما األمر يستحق‪.‬‬

‫يضبُط الباني قيمة ‪ head‬إىل ‪ ،null‬مما يشير إىل كون القائمة فارغة‪ ،‬كما يضبُط ‪ size‬إىل صفر‪.‬‬

‫يَستخدِم هذا الصنف معام‪ww‬ل ن‪ww‬وع ‪ type parameter‬اس‪ww‬مه ‪ E‬لتخص‪ww‬يص ن‪ww‬وع العناص‪ww‬ر‪ .‬إذا لم تكن عىل‬

‫معرفة بمعامالت األنواع‪ ،‬اقرأ هذا الدرس (باللغة التي تريد)‪.‬‬

‫ضمن داخل الصنف ‪ .MyLinkedList‬انظر تعريفه‪:‬‬


‫الم َّ‬ ‫يَظهَ ر معامل النوع ً‬
‫أيضا بتعريف الصنف ‪ُ Node‬‬

‫{ ‪private class Node‬‬


‫;‪public E data‬‬
‫;‪public Node next‬‬

‫{ )‪public Node(E data, Node next‬‬


‫;‪this.data = data‬‬
‫;‪this.next = next‬‬
‫}‬
‫}‬

‫تماما للصنف ‪ ListNode‬في األعىل‪.‬‬


‫ً‬ ‫أضف إىل هذا أن الصنف ‪ Node‬مشاب ٌه‬

‫واآلن‪ ،‬انظر إىل تنفيذ التابع ‪:add‬‬

‫‪39‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫{ )‪public boolean add(E element‬‬


‫{ )‪if (head == null‬‬
‫;)‪head = new Node(element‬‬
‫{ ‪} else‬‬
‫;‪Node node = head‬‬
‫نفذ الحلقة حتى تصل إلى العقدة الأخيرة ‪//‬‬
‫}{ )‪for ( ; node.next != null; node = node.next‬‬
‫;)‪node.next = new Node(element‬‬
‫}‬
‫;‪size++‬‬
‫;‪return true‬‬
‫}‬

‫يُ ِّ‬
‫وضح هذا المثال نمطين ستحتاج لمعرفتهما إلكمال حل التمرين‪:‬‬

‫‪ .1‬في كثير من التوابع‪ ،‬عاد ًة ما نضطرّ إىل معالجة أول عنصر ٍ في القائمة بطريقةٍ خاصة‪ .‬وفي ه‪ww‬ذا المث‪ww‬ال‪،‬‬

‫إذا كنا نضيف العنصر األول اىل القائمة‪ ،‬فعلينا أن نُعدِّل قيمة ‪ ،head‬أما في الحاالت األخرى‪ ،‬فعلينا أن‬

‫نجتاز القائمة‪ ،‬حتى نصل إىل نهايتها‪ ،‬ثم نضيف العقدة الجديدة‪.‬‬

‫‪ .2‬يُب ّين ذلك التابع طريقة استخدام حلقة التكرار ‪ for‬من أج‪ww‬ل اجتي‪ww‬از أو التنق‪ww‬ل بين العق‪ww‬د الموج‪ww‬ودة في‬

‫‪w‬خ عدي‪ww‬د ٍة من تل‪ww‬ك‬


‫القائمة‪ .‬في مثالنا هذا لدينا حلقة واح‪ww‬دة‪ .‬ولكن في الواق‪ww‬ع ق‪ww‬د تحت‪ww‬اج إىل كتاب‪ww‬ة نس‪ٍ w‬‬
‫الحلقة ضمن الحلول الخاصة بك‪ .‬من جه‪ww‬ة أخ‪ww‬رى‪ ،‬الحِ‪ w‬ظ كي‪ww‬ف ص‪w‬رّحنا عن ‪ node‬قب‪ww‬ل بداي‪ww‬ة الحلق‪ww‬ة؛‬

‫والهدف من ذلك هو أن نتمكَّن من استرجاعها بعد انتهاء الحلقة‪.‬‬

‫واآلن حان دورك‪ ،‬أكمل متن التابع ‪ .indexOf‬ويجب أن تقرأ مق‪ww‬ال توثي‪ww‬ق ‪ List indexOf‬لكي تع‪ww‬رف م‪ww‬ا‬

‫ينبغي عليك القيام به‪ .‬انتبه تحديدًا للطريقة التي يُفت َرض له معالجة القيمة الفارغة ‪ null‬بها‪.‬‬

‫كما هو الحال في تمرين الفصل السابق‪ ،‬و َّفرنا الت‪ww‬ابع المس‪ww‬اعد ‪ equals‬للموازن‪ww‬ة بين قيم‪ww‬ة عنص‪ww‬ر ٍ ض‪ww‬من‬

‫المصفوفة وبين قيمةٍ معينةٍ أخ‪ww‬رى‪ ،‬و َف ْ‬


‫حص م‪ww‬ا إذا ك‪ww‬انت القيمت‪ww‬ان متس‪ww‬اويتين‪ .‬يُع‪ww‬الِج الت‪ww‬ابع القيم‪ww‬ة الفارغ‪ww‬ة‬
‫معالج‪ً w‬‬
‫‪w‬ة س‪ww‬ليمة‪ .‬الحِ ‪ w‬ظ أن الت‪ww‬ابع ُمع ‪w‬رَّ ٌف باس‪ww‬تخدام ُ‬
‫المع ‪w‬دِّل ‪private‬؛ ألن‪ww‬ه ليس ج‪ww‬زءًا من الواجه‪ww‬ة ‪،List‬‬

‫ويُستخدَم فقط داخل الصنف‪.‬‬

‫ش‪ِّ w‬غل االختب‪ww‬ارات م‪ww‬ر ًة أخ‪ww‬رى عن‪ww‬دما تنتهي‪ .‬ينبغي أن ينجح االختب‪ww‬ار ‪ testIndexOf‬وك‪ww‬ذلك االختب‪ww‬ارات‬

‫األخرى التي تعتمد عليه‪.‬‬

‫فهرس‪w‬ا وتُخ‪w‬زِّن القيم‪w‬ة‬


‫ً‬ ‫واآلن‪ ،‬عليك أن تكم‪w‬ل نس‪w‬خة الت‪w‬ابع ‪ add‬ذات المع‪w‬املين‪ .‬تَس‪w‬تق ِبل تل‪w‬ك النس‪w‬خة‬

‫الممرَّر‪ .‬وبالمثل‪ ،‬اقرأ أواًل التوثيق ‪ List add‬ثم ن ِّفذ الت‪w‬ابع‪ ،‬وأخ‪w‬يرًا‪ ،‬ش‪ِّ w‬غل االختب‪w‬ارات لكي‬
‫الجديدة في الفهرس ُ‬
‫تتأ ّكد من أنك ن ّفذتها بشكل سليم‪.‬‬

‫‪40‬‬
‫هياكل البيانات للمبرمجين‬ ‫قائمة المصفوفة ‪ArrayList‬‬

‫لننتقل اآلن إىل التابع األخير‪ :‬أكمل متن التابع ‪ .remove‬اقرأ توثيق التابع ‪ .List remove‬بع‪ww‬دما تنتهي من‬

‫إكمال هذا التابع‪ ،‬ينبغي أن تنجح جميع االختبارات‪.‬‬

‫نهي جميع التواب‪w‬ع وتتأ َّكد من أنه‪ww‬ا تَ َ‬


‫عم‪ w‬ل بكف‪w‬اءة‪ ،‬يُمكِن‪ww‬ك مقابته‪ww‬ا م‪w‬ع النس‪ww‬خ المتاح‪w‬ة في مجل‪w‬د‬ ‫بعد أن تُ ِ‬
‫‪ solution‬في مستودع الكتاب‪.‬‬

‫‪ 3.6‬ملحوظة متعلقة بكنس المهمالت ‪garbage collection‬‬


‫تنمو المصفوفة في الصنف ‪- MyArrayList‬من التمرين المشار إليه‪ -‬عند الضرورة‪ ،‬ولكنها ال تتقلص أبدًا‪،‬‬

‫وبالتالي ال تُكنَس المصفوفة وال يُكنَس أي من عناص‪ww‬رها ح‪ww‬تى يحين موع‪ww‬د ت‪ww‬دمير القائم‪ww‬ة ذاته‪ww‬ا‪ .‬في المقاب‪ww‬ل‪،‬‬
‫المس‪ww‬تخدَمة مباش‪ً w‬‬
‫‪w‬رة‪ ،‬وه‪ww‬و م‪ww‬ا يُم ِث‪ww‬ل‬ ‫تتقلص القائمة المترابطة عند حذف العناصر منها‪ ،‬كما تُكنَس العق‪ww‬د غ‪ww‬ير ُ‬
‫ً‬
‫واحدة من مميزات هذا النوع من هياكل البيانات‪.‬‬

‫انظر إىل تنفيذ التابع ‪:clear‬‬

‫{ )(‪public void clear‬‬


‫;‪head = null‬‬
‫;‪size = 0‬‬
‫}‬

‫عندما نضبُط قيمة الرأس ‪ head‬بالقيمة ‪ ،null‬فإننا نحذف المرجع إىل العقدة األوىل‪ .‬إذا لم تكن هن‪ww‬اك أي‬

‫مراجع أخرى إىل ذلك الكائن (من المفترض أال يكون هناك أي مراجع أخرى)‪ ،‬فس ُيكنَس الكائن مباشر ًة‪ .‬في تل‪ww‬ك‬

‫اللحظة‪ ،‬س ُيح َذف المرجع إىل الكائن ُ‬


‫الممثِل للعقدة الثانية‪ ،‬وبالتالي يُكنَس بدوره أيَ ًضا‪ .‬وستستمر تل‪ww‬ك العملي‪ww‬ة‬

‫إىل أن تُكنَس جميع العقد‪.‬‬

‫يتضمن التابع عمليتين ثابتتي الزمن‪ ،‬ويبدو لهذا كما لو أنه‬


‫َّ‬ ‫بنا ًء عىل ما سبق‪ ،‬ما هو تصنيف التابع ‪clear‬؟‬

‫يستغرِق زمنًا ثابتًا‪ ،‬ولكنك عندما تستدعيه ستُحفزّ كانس المهمالت عىل إجراء عملية تتناسب مع عدد العناص‪ww‬ر‪،‬‬

‫ي الزمن‪.‬‬ ‫ولذلك ربما علينا أن نَعُ دّه ّ‬


‫خط َّ‬

‫يُع ّد هذا مثااًل عىل ما نُسميه أحيانًا بـمشكلة برمجية في األداء‪ ،‬أي أن البرنامج يفعل الشيء الصحيح‪ ،‬ولكن‪ww‬ه‬
‫ً‬
‫خاصة في اللغات التي تُن ِّفذ أعم‪ww‬ااًل‬ ‫الم َّ‬
‫توقع‪ .‬يَص ُعب العثور عىل هذا النوع من األخطاء‬ ‫ال ينتمي إىل ترتيب النمو ُ‬
‫كثير ًة وراء الكواليس مثل عملية كنس المهمالت مثالً‪ ،‬وتُع ّد لغة جافا واحد ًة من تلك اللغات‪.‬‬

‫‪41‬‬
‫‪ .4‬القائمة المرتابطة ‪LinkedList‬‬

‫سنتناول في هذا الفصل حل تمرين الفصل الثالث‪ ،‬ثم نتابع مناقشة تحليل الخوارزميات‪.‬‬

‫‪ 4.1‬تصنيف توابع الصنف ‪MyLinkedList‬‬


‫تَعرِض الشيفرة التالية تنفيذ التابع ‪ .indexOf‬اقرأها وحاول تحديد ترتيب نم‪ww‬و ‪ order of growth‬الت‪ww‬ابع‬

‫قبل المتابعة‪:‬‬

‫{ )‪public int indexOf(Object target‬‬


‫;‪Node node = head‬‬
‫{ )‪for (int i=0; i<size; i++‬‬
‫{ ))‪if (equals(target, node.data‬‬
‫;‪return i‬‬
‫}‬
‫;‪node = node.next‬‬
‫}‬
‫;‪return -1‬‬
‫}‬

‫المتحكِّم‬ ‫تُسنَد ‪ head‬إىل ‪ node‬أواًل ‪ ،‬ويعني هذا أنّ كليهما يشير اآلن إىل نفس العقدة‪ .‬يَ ُع‪ّ w‬د ُ‬
‫المتغ ّي‪ w‬رُ ‪ i‬ه‪ww‬و ُ‬
‫بالحلق‪ww‬ة من ‪ 0‬إىل ‪ ،size-1‬ويَس‪ww‬تدعِ ي في ك‪ww‬ل تك‪ww‬رارٍ الت‪ww‬ابعَ ‪ equals‬ليفحص م‪ww‬ا إذا ك‪ww‬ان ق‪ww‬د وج‪ww‬د القيم‪ww‬ة‬

‫المطلوبة‪ .‬فإذا وجدها‪ ،‬فسيعيد قيمة ‪ i‬عىل الفور؛ أما إذا لم يجدها‪ ،‬فسينتقل إىل العقدة التالية ضمن القائمة‪.‬‬
‫هياكل البيانات للمبرمجين‬ ‫القائمة المترابطة ‪LinkedList‬‬

‫عادة ما نتأ َّكد أواًل مما إذا كانت العقدة التالية ال تحتوي عىل قيمة فارغة ‪ ،null‬ولكن ليس هذا ض‪ww‬روريًا في‬
‫ً‬
‫ٌ‬
‫ّسقة مع العدد الفعلي للعقد‬
‫حالتنا؛ ألن الحلقة تنتهي بمجرد وصولنا إىل نهاية القائمة (بفرض أن قيمة ‪ُ size‬مت‬

‫الموجودة ضمن القائمة)‪.‬‬

‫إذا ن َّفذنا الحلقة بالكامل دون العثور عىل القيمة المطلوبة‪ ،‬فسيعيد التابع القيمة ‪.1-‬‬

‫واآلن‪ ،‬ما هو ترتيب نمو هذا التابع؟‬

‫‪ .1‬إننا نستدعِ ي في كل تكرار التابع ‪ equals‬الذي يَس‪ww‬تغرِق زمنً‪ww‬ا ثاب ًت‪ww‬ا (ق‪ww‬د يعتم‪ww‬د عىل حجم ‪ target‬أو‬

‫‪ ،data‬ولكنه ال يعتمد عىل حجم القائمة)‪ .‬تستغرق جميع التعليم‪w‬ات األخ‪w‬رى ض‪ww‬من الحلق‪ww‬ة زمنً‪w‬ا ثابتً‪w‬ا‬

‫ً‬
‫أيضا‪.‬‬

‫نضطر إىل التنقل في القائمة بالكامل في الحالة األسوأ‪.‬‬


‫ّ‬ ‫‪ .2‬تُن َّفذ الحلقة عددًا من المرات مقدراه ‪ n‬ألننا قد‬

‫وبالتالي يتناسب زمن تنفيذ ذلك التابع مع طول القائمة‪.‬‬

‫واآلن‪ ،‬انظر إىل تنفيذ التابع ‪ add‬ذي المعاملين‪ ،‬وحاول تصنيفه قبل متابعة القراءة‪.‬‬

‫{ )‪public void add(int index, E element‬‬


‫{ )‪if (index == 0‬‬
‫;)‪head = new Node(element, head‬‬
‫{ ‪} else‬‬
‫;)‪Node node = getNode(index-1‬‬
‫;)‪node.next = new Node(element, node.next‬‬
‫}‬
‫;‪size++‬‬
‫}‬

‫الصفر‪ ،‬فإننا نضيف العقدة الجديدة إىل بداي‪ww‬ة القائم‪ww‬ة‪ ،‬وله‪ww‬ذا علين‪ww‬ا أن نُع‪ww‬الِج ذل‪ww‬ك‬
‫ساوي ِّ‬
‫إذا كان ‪ index‬يُ ِ‬
‫نص ‪w‬ل إىل العنص‪ww‬ر الموج‪ww‬ود في الفه‪ww‬رس‬
‫مثل حالة خاصة‪ .‬وبخالف ذلك سنضطرّ إىل التنقل في القائمة إىل أن ِ‬

‫‪ ،index-1‬كما سنستخدِم لهذا الهدف التابعَ المساع َد ‪ ،getNode‬وفيما يلي شيفرته‪:‬‬

‫{ )‪private Node getNode(int index‬‬


‫{ )‪if (index < 0 || index >= size‬‬
‫;)(‪throw new IndexOutOfBoundsException‬‬
‫}‬
‫;‪Node node = head‬‬
‫{ )‪for (int i=0; i<index; i++‬‬
‫;‪node = node.next‬‬

‫‪43‬‬
‫هياكل البيانات للمبرمجين‬ ‫القائمة المترابطة ‪LinkedList‬‬

‫}‬
‫;‪return node‬‬
‫}‬

‫يفحص التابع ‪ getNode‬ما إذا كانت قيمة ‪ index‬خارج النطاق المسموح به‪ ،‬فإذا كانت ك‪ww‬ذلك‪ ،‬فإن‪ww‬ه يُبلِّغ‬

‫عن اعتراض ‪exception‬؛ أما إذا لم تكن كذلك‪ ،‬فإنه يمرّ عبر عناصر القائمة ويعيد العقدة المطلوبة‪.‬‬

‫‪w‬دة جدي‪ً w‬‬


‫‪w‬دة‪ ،‬ونض‪ww‬عها بين العق‪ww‬دتين‬ ‫اآلن وقد حصلنا عىل العقدة المطلوبة‪ ،‬نعود إىل الت‪ww‬ابع ‪ add‬وننش‪ww‬ئ عق‪ً w‬‬

‫‪ node‬و‪ .node.next‬قد يساعدك رسم هذه العملية عىل التأكد من فهمها بوضوح‪.‬‬

‫واآلن‪ ،‬ما هو ترتيب نمو التابع ‪add‬؟‬

‫خطيٌّ لنفس السبب‪.‬‬


‫‪ .1‬يشبه التابع ‪ getNode‬التابع ‪ ،indexOf‬وهو ّ‬

‫‪ .2‬تَستغرِق جميع التعليمات زمنًا ثابتًا سوا ٌء قبل استدعاء التابع ‪ getNode‬أوبعد اس‪ww‬تدعائه ض‪ww‬من الت‪ww‬ابع‬

‫‪.add‬‬

‫وعليه‪ ،‬يكون التابع ‪ add‬خط ًّيا‪.‬‬

‫ً‬
‫نظرة عىل التابع ‪:remove‬‬ ‫ُلق‬
‫وأخي ًرا‪ ،‬لن ِ‬

‫{ )‪public E remove(int index‬‬


‫;)‪E element = get(index‬‬
‫{ )‪if (index == 0‬‬
‫;‪head = head.next‬‬
‫{ ‪} else‬‬
‫;)‪Node node = getNode(index-1‬‬
‫;‪node.next = node.next.next‬‬
‫}‬
‫;‪size--‬‬
‫;‪return element‬‬
‫}‬

‫يَستدعِ ي ‪ remove‬التابع ‪ get‬للعث‪ww‬ور عىل العنص‪ww‬ر الموج‪ww‬ود في الفه‪ww‬رس ‪ index‬ثم عن‪ww‬دما يج‪ww‬ده يح‪ww‬ذف‬

‫العقدة التي تتضمنه‪.‬‬

‫إذا كان ‪ index‬يُساوي الصفر‪ ،‬نُعالِج ذلك مثل حالة خاصة‪ .‬وإذا لم يكن يساوي الصفر فسنذهب إىل‬

‫العقدة الموجودة في الفهرس ‪ ،index-1‬وهي العقدة التي تقع قبل العقدة المستهدفة بالحذف‪ ،‬ونُعدِّل حقل‬
‫ً‬
‫مباشرة إىل العقدة ‪ ،node.next.next‬وبذلك نكون قد حذفنا العقدة ‪ node.next‬من‬ ‫‪ next‬فيها ليشير‬

‫‪44‬‬
‫هياكل البيانات للمبرمجين‬ ‫القائمة المترابطة ‪LinkedList‬‬

‫ثم تُحرّر الذاكرة التي كانت تحتّلها عن طريق الكنس ‪ .garbage collection‬وأخيرًا‪ ،‬يُنقِ ص التابع‬
‫القائمة‪ ،‬ومن َّ‬
‫جع في البداية‪.‬‬
‫المستر َ‬
‫قيمة ‪ size‬ويُعيد العنصر ُ‬

‫واآلن بنا ًء عىل ما سبق‪ ،‬ما هو ترتيب نمو التابع ‪remove‬؟ تَستغرِق جميع التعليمات في ذل‪ww‬ك الت‪ww‬ابع زمنً‪ww‬ا‬

‫خط ًّيا‪ .‬وبن‪ww‬ا ًء عىل ذل‪w‬ك يك‪ww‬ون الت‪ww‬ابع‬


‫ثابتًا باستثناء استدعائه للت‪w‬ابعين ‪ get‬و‪ getNode‬الل‪w‬ذين يس‪ww‬تغرقان زمنً‪w‬ا ّ‬
‫خط ًّيا هو اآلخر‪.‬‬
‫‪ّ remove‬‬

‫خط ّيتين أن النتيجة اإلجمالية ستكون تربيع ّي‪ً w‬‬


‫‪w‬ة‪ ،‬ولكن ه‪ww‬ذا ليس‬ ‫يظن بعض األشخاص عندما يرون عمليتين ّ‬
‫المحص‪ww‬لة بجم‪ww‬ع‬
‫ُحس ‪w‬ب ُ‬
‫صحيحًا إال إذا كانت إحداهما داخل األخرى؛ أما إذا استُدعِ يت إحداهما تلو األخرى‪ ،‬فست َ‬
‫زمنيهما‪ ،‬وألن كليهما ينتميان إىل المجموعة )‪ ،O(n‬فسينتمي المجموع إىل )‪ً O(n‬‬
‫أيضا‪.‬‬

‫‪ 4.2‬الموازنة بني الصنفني ‪ MyArrayList‬و‪MyLinkedList‬‬


‫يُلخ ِّص الج‪www‬دول الت‪www‬الي االختالف‪www‬ات بين الص‪www‬نفين ‪ MyArrayList‬و‪ .MyLinkedList‬يش‪www‬ير ‪ 1‬إىل‬

‫الخطي‪:‬‬
‫ّ‬ ‫المجموعة )‪ O(1‬أو الزمن الثابت‪ ،‬بينما يشير ‪ n‬إىل المجموعة )‪ O(n‬أو الزمن‬

‫‪MyLinkedList‬‬ ‫‪MyArrayList‬‬
‫‪n‬‬ ‫‪1‬‬ ‫‪( add‬في النهاية)‬

‫‪1‬‬ ‫‪n‬‬ ‫‪( add‬في البداية)‬

‫‪n‬‬ ‫‪n‬‬ ‫‪( add‬في العموم)‬

‫‪n‬‬ ‫‪1‬‬ ‫‪get / set‬‬

‫‪n‬‬ ‫‪n‬‬ ‫‪indexOf / lastIndexOf‬‬

‫‪1‬‬ ‫‪1‬‬ ‫‪isEmpty / size‬‬

‫‪n‬‬ ‫‪1‬‬ ‫‪( remove‬من النهاية)‬

‫‪1‬‬ ‫‪n‬‬ ‫‪( remove‬من البداية)‬

‫‪n‬‬ ‫‪n‬‬ ‫‪( remove‬في العموم)‬

‫في حالتي إضافة عنصر أو حذف‪w‬ه من نهاي‪w‬ة القائم‪w‬ة‪ ،‬ف‪w‬إنّ الص‪w‬نف ‪ MyArrayList‬ه‪w‬و أفض‪w‬لُ من نظ‪w‬يره‬

‫أما في حالتي إض‪ww‬افة عنص‪ww‬ر أو حذف‪ww‬ه من مقدّم‪ww‬ة‬


‫‪ ،MyLinkedList‬وكذلك في عمليتي االسترجاع والتعديل؛ ّ‬
‫القائمة‪ ،‬فإن الصنف ‪ MyLinkedList‬هو أفضل من نظيره ‪.MyArrayList‬‬

‫يحظى الصنفان بنفس ترتيب النمو بالنسبة للعمليات األخرى‪.‬‬

‫إ ًذا‪ ،‬أيهما أفضل؟ يعتمد ذلك عىل العمليات التي يُحتمل استخدامها أكثر‪ ،‬وهذا السبب هو الذي يجعل جافا‬

‫تُو ِّفر أكثر من تنفي ٍذ ‪ implementation‬وحيد‪.‬‬

‫‪45‬‬
‫هياكل البيانات للمبرمجين‬ LinkedList ‫القائمة المترابطة‬

Profiling ‫ التشخيص‬4.3
‫ ِّغل‬w‫ا أن تُش‬ww‫يفرة بإمكانه‬ww‫نف عىل ش‬ww‫ذا الص‬ww‫ يحتوي ه‬.‫ في التمرين التالي‬Profiler ‫ستحتاج إىل الصنف‬

.‫ وتَعرِض النتائج‬،‫ وتقيس زمن التشغيل لكلٍّ منها‬، ٍ‫تابعً ا ما عىل مشاكلَ ذات أحجامٍ متفاوتة‬

ArrayList ‫نفين‬ww‫لٍّ من الص‬ww‫رَّف في ك‬w ‫المع‬


ُ add ‫ابع‬ww‫نيف أداء الت‬ww‫ لتص‬Profiler ‫نف‬ww‫تخدِم الص‬ww‫ستَس‬
.‫ اللذين تُو ِّفرهما لغة جافا‬LinkedList‫و‬

ِّ ُ‫ت‬
:‫وضح الشيفرة التالية طريقة استخدام ذلك الصنف‬

public static void profileArrayListAddEnd() {


Timeable timeable = new Timeable() {
List<String> list;

public void setup(int n) {


list = new ArrayList<String>();
}

public void timeMe(int n) {


for (int i=0; i<n; i++) {
list.add("a string");
}
}
};

String title = "ArrayList add end";


Profiler profiler = new Profiler(title, timeable);

int startN = 4000;


int endMillis = 1000;
XYSeries series = profiler.timingLoop(startN, endMillis);
profiler.plotResults(series);
}

‫ الذي يُضيف عنصرًا جديدًا في نهاية قائمةٍ من‬add ‫التابع‬


ِ ُ
ُ‫السابق الزمنَ الذي يستغرقه تشغيل‬ ُ‫يقيس التابع‬

.‫ سنشرح الشيفرة أواًل ثم نَعرِض النتائج‬.ArrayList ‫النوع‬

46
‫هياكل البيانات للمبرمجين‬ ‫القائمة المترابطة ‪LinkedList‬‬

‫لكي يتمكَّن الصنف ‪ Profiler‬من أداء عمله‪ ،‬سنحتاج أواًل إىل إنش‪ww‬اء ك‪ٍ w‬‬
‫‪w‬ائن من الن‪ww‬وع ‪ .Timeable‬يُ‪ww‬و ِّفر‬

‫هذا الكائنُ التابعين ‪ setup‬و‪ ،timeMe‬حيث يُن ِّفذ التابع ‪ setup‬كل ما ينبغي فعله قبل ب‪w‬دء تش‪w‬غيل الم‪w‬ؤقت‪،‬‬

‫أم‪ w‬ا الت‪ww‬ابع ‪ timeMe‬ف ُين ِّفذ العملي‪ww‬ة ال‪ww‬تي نح‪ww‬اول قي‪ww‬اس أدائه‪ww‬ا‪ .‬في ه‪ww‬ذا‬ ‫ً‬
‫قائمة فارغة‪ّ ،‬‬ ‫نشئ‬
‫وفي هذا المثال س ُي ِ‬

‫المثال‪ ،‬سنجعله يُضيف عددًا من العناصر مقداره ‪ n‬إىل القائمة‪.‬‬

‫‪w‬نف مجه‪ww‬ول االس‪ww‬م ‪،anonymous‬‬


‫لقد عرَّفنا الشيفرة المسؤولة عن إنشاء المتغ‪ww‬ير ‪ timeable‬ض‪ww‬من ص‪ٍ w‬‬
‫نش‪w‬ئ نس‪ً w‬‬
‫‪w‬خة من الص‪ww‬نف الجدي‪ww‬د في نفس‬ ‫حيث يُعرِّف ذل‪ww‬ك الص‪ww‬نف تنفي‪ً w‬ذا جدي‪w‬دًا للواجه‪ww‬ة ‪ ،Timeable‬ويُ ِ‬
‫ٌ‬
‫فكرة عن األصناف مجهول‪ww‬ة االس‪ww‬م‪ ،‬فس‪ww‬نحيلك إىل المقال‪ww‬ة م‪ww‬ا هي األص‪ww‬ناف مجهول‪ww‬ة‬ ‫الوقت‪ .‬إذا لم يكن لديك‬

‫االسم؟ (باللغة اإلنجليزية) والفصل األصناف المتداخلة ‪ Nested Classes‬في جافا‪.‬‬

‫حال لست في حاجةٍ إىل معرفة الكث‪ww‬ير عنه‪ww‬ا لح‪ww‬ل التم‪ww‬رين الت‪ww‬الي‪ ،‬ويُمكِن‪ww‬ك نس‪ww‬خ ش‪ww‬يفرة‬
‫ٍ‬ ‫ولكنك عىل كل‬

‫المثال وتعديلها‪.‬‬

‫واآلن سننتقل إىل الخطوة التالية‪ ،‬وهي إنشاء كائن من الص‪ww‬نف ‪ Profiler‬م‪ww‬ع تمري‪ww‬ر مع‪ww‬املين ل‪ww‬ه هم‪ww‬ا‪:‬‬

‫العنوان ‪ title‬وكائن من النوع ‪.Timeable‬‬

‫يحتوي الصنف ‪ Profiler‬عىل التابع ‪ .timingLoop‬يس‪ww‬تخدم ذل‪ww‬ك الت‪ww‬ابعُ الك‪ww‬ائنَ ‪ُ Timeable‬‬


‫المخ‪w‬زَّن‬

‫‪w‬رات م‪ww‬ع قيم مختلف‪ww‬ةٍ لحجم‬ ‫مث‪ww‬ل متغ ِّير ِ ن ُ ْ‬


‫س‪w‬خَةٍ ‪ ،instance variable‬حيث يَس‪ww‬تدعِ ي تابع‪ww‬ه ‪ timeMe‬ع‪ww‬دة م‪ٍ w‬‬

‫المشكلة ‪ n‬في كل مرة‪ .‬ويَستق ِبل التابع ‪ timingLoop‬المعاملين التاليين‪:‬‬

‫‪ :startN‬هي قيمة ‪ n‬التي تبدأ منها الحلقة‪.‬‬ ‫•‬

‫‪ :endMillis‬عب‪ww‬ارة عن قيم‪ww‬ة قص‪ww‬وى بوح‪ww‬دة الميلي ثاني‪ww‬ة‪ .‬ي‪ww‬زداد زمن التش‪ww‬غيل عن‪ww‬دما يُزي‪ww‬د الت‪ww‬ابع‬ ‫•‬

‫المحدّدة‪ ،‬فينبغي أن يتوق‪ww‬ف‬ ‫َ‬


‫القيمة القصوى ُ‬ ‫‪ timingLoop‬حجم المشكلة‪ ،‬وعندما يتجاوز ذلك الزمنُ‬

‫التابع ‪.timingLoop‬‬

‫تضطر إىل ضبط قيم تلك المعامالت عند إجراء تلك التجارب‪ ،‬فإذا كانت قيم‪w‬ة ‪ startN‬ض‪ً w‬‬
‫‪w‬ئيلة للغاي‪w‬ة‪،‬‬ ‫ّ‬ ‫قد‬
‫فلن يكون زمن التشغيل طوياًل بما يكفي لقياس‪ww‬ه ّ‬
‫بدق‪ww‬ة‪ ،‬وإذا ك‪ww‬انت قيم‪ww‬ة ‪ endMillis‬ص‪ww‬غير ًة للغاي‪ww‬ة‪ ،‬فق‪ww‬د ال‬

‫بيانات كافيةٍ الستنباط العالقة بين حجم المشكلة وزمن التشغيل‪.‬‬


‫ٍ‬ ‫تحصل عىل‬

‫ستج د تلك الشيفرة ‪-‬التي ستحتاج إليها في التمرين التالي‪ -‬في الملف ‪ ،ProfileListAdd.java‬والتي‬
‫ِ‬
‫حصلنا عند تشغيلها عىل الخرج التالي‪:‬‬

‫‪3‬‬
‫‪0‬‬
‫‪1‬‬
‫‪2‬‬
‫‪3‬‬

‫‪47‬‬
‫هياكل البيانات للمبرمجين‬ ‫القائمة المترابطة ‪LinkedList‬‬

‫‪6‬‬
‫‪18‬‬
‫‪30‬‬
‫‪88‬‬
‫‪185‬‬
‫‪242‬‬
‫‪544‬‬
‫‪1325‬‬

‫يُمثِل العمود األول حجم المشكلة ‪ ،n‬أما العمود الثاني ف ُيمثِل زمن التشغيل بوحدة الميلي ثانية‪ .‬كما تالحظ‪،‬‬

‫تماما وليس له‪w‬ا م‪w‬دلول حقيقي‪ ،‬ولعلّ‪w‬ه ك‪w‬ان من األفض‪ww‬ل ض‪ww‬بط قيم‪ww‬ة‬
‫ً‬ ‫ً‬
‫دقيقة‬ ‫فالقياسات القليلة األوىل ليست‬

‫‪ startN‬إىل ‪.64000‬‬

‫ً‬
‫متتالية تحتوي عىل القياس‪ww‬ات‪.‬‬ ‫يُعيد التابعُ ‪ timingLoop‬النتائجَ بهيئة كائن من النوع ‪ ،XYSeries‬ويُمثِل‬

‫رسم شكاًل بيان ًيا ‪-‬مش‪ww‬ابها للموج‪ww‬ود في الص‪ww‬ورة التالي‪ww‬ة‪ -‬وسنش‪ww‬رحه‬


‫إذا مرَّرتها إىل التابع ‪ ،plotResults‬فإنه يَ ِ‬

‫طبعً ا في القسم التالي‪.‬‬

‫‪ 4.4‬تفسري النتائج‬
‫َّ‬
‫نتوقع أن يس‪ww‬تغرق الت‪ww‬ابع ‪ add‬زمنً‪ww‬ا ثاب ًت‪ww‬ا عن‪ww‬دما‬ ‫بنا ًء عىل فهمنا لكيفية عمل الص‪ww‬نف ‪ ،ArrayList‬فإنن‪ww‬ا‬

‫ي إلض‪ww‬افة ع‪w‬د ٍد من العناص‪ww‬ر مق‪ww‬داره ‪n‬‬ ‫ّ‬


‫نضيف عناص‪w‬رَ إىل نهاي‪ww‬ة القائم‪ww‬ة‪ ،‬وبالت‪ww‬الي ينبغي أن يك‪ww‬ون ال‪w‬زمنُ الكل ُّ‬
‫زمنًا خط ًيا‪.‬‬

‫‪48‬‬
‫هياكل البيانات للمبرمجين‬ ‫القائمة المترابطة ‪LinkedList‬‬

‫لكي نختبر صحة تلك النظرية‪ ،‬سنَعرِض تأثير زيادة حجم المش‪w‬كلة عىل زمن التش‪w‬غيل الكلّي‪ .‬من المف‪w‬ترض‬

‫خط مستقيمٍ عىل األقل ألحج‪ww‬ام المش‪ww‬كلة ‪ problem size‬الكب‪ww‬يرة بالق‪ww‬در الك‪ww‬افي لقي‪ww‬اس زمن‬
‫أن نحصل عىل ٍّ‬
‫ُ‬
‫كتابة دالّة هذا الخط المستقيم رياض ًّيا عىل النحو التالي‪:‬‬ ‫التشغيل ‪ runtime‬بدقة‪ .‬ويُمكِننا‬

‫‪runtime = a + b*n‬‬

‫حيث يشير ‪ a‬إىل إزاحةِ الخط وهو قيمة ثابتة و ‪ b‬إىل ميل الخط‪.‬‬

‫الكلي لتنفيذ ع‪ww‬د ٍد من اإلض‪ww‬افات بمق‪ww‬دار ‪ n‬تربيع ًي‪ww‬ا‪.‬‬


‫ُّ‬ ‫خط ًّيا‪ ،‬فسيكون الزمن‬
‫في المقابل‪ ،‬إذا كان التابع ‪ّ add‬‬

‫‪w‬ع مك‪ww‬افٍئ‬ ‫َّ‬


‫وعن‪ww‬دها إذا نظرن‪ww‬ا إىل ت‪ww‬أثير زي‪ww‬ادة حجم المش‪ww‬كلة عىل زمن التش‪ww‬غيل‪ ،‬فس‪ww‬نتوقع الحص‪ww‬ول عىل قط‪ٍ w‬‬
‫‪ ،parabola‬والذي يُمكِن كتابة معادلته رياض ًّيا عىل النحو التالي‪:‬‬

‫‪runtime = a + b*n + c*n^2‬‬

‫ً‬
‫دقيقة فسنتمكَّن بس‪w‬هولةٍ من التمي‪w‬يز بين الخ‪w‬ط المس‪w‬تقيم والقط‪w‬ع‬ ‫إذا كانت القياسات التي حصلنا عليها‬

‫تماما‪ ،‬فقد يكون تمييز ذلك صعبًا إىل ح ٍّد ما‪ ،‬وعندئ ‪ٍ w‬ذ يُ ّ‬
‫فض ‪w‬لُ اس‪ww‬تخدام مقي‪ww‬اس‬ ‫ً‬ ‫ً‬
‫دقيقة‬ ‫أما إذا لم تكن‬
‫المكافئ‪ّ ،‬‬
‫لوغاريتمي‪-‬لوغاريتمي ‪ log-log‬لعرض تأثير حجم المشكلة عىل زمن التشغيل‪.‬‬

‫السبب‪ ،‬لنفترض أن زمن التشغيل يتناسب مع ‪ ،nk‬ولكننا ال نعلم قيمة األس ‪ .k‬يُمكِننا كتاب‪ww‬ة تل‪ww‬ك‬
‫َ‬ ‫ولِنَعر ِ َف‬

‫العالقة عىل النحو التالي‪:‬‬

‫‪runtime = a + b*n + … + c*n^k‬‬

‫األس األكبر ه‪ww‬و األهم من بين القيم الكب‪ww‬يرة لحجم المش‪ww‬كلة‪،‬‬


‫ِّ‬ ‫كما ترى في المعادلة السابقة‪ ،‬فإنّ األساس ذا‬

‫وبالتالي يُمكِن تقريب ًّيا إهمال باقي الحدود وكتابة العالقة عىل النحو التالي‪:‬‬

‫‪runtime ≈ c * n^k‬‬

‫حيث نعني بالرمز ≈ "يساوي تقريبًا"‪ ،‬فإذا حسبنا اللوغاريتم لطرفي المعادلة‪ ،‬فستصبح مثل اآلتي‪:‬‬

‫)‪log(runtime) ≈ log(c) + k*log(n‬‬

‫تعني المعادلة السابقة أنه لو رسمنا زمن التشغيل مقابل حجم المش‪ww‬كلة ‪ n‬باس‪ww‬تخدام مقي‪ww‬اس لوغ‪ww‬اريتمي‪-‬‬

‫بثابت ‪-‬يمثل اإلزاحة‪ -‬يساوي )‪ log(c‬وبمي‪w‬ل يس‪w‬اوي ‪ .k‬ال يهمن‪ww‬ا الث‪ww‬ابت هن‪ww‬ا‬
‫ٍ‬ ‫مستقيما‬
‫ً‬ ‫خطا‬
‫لوغاريتمي‪ ،‬فسنرى ً‬
‫وزبدة الكالم أنه إذا كانت قيم‪ww‬ة ‪ k‬تس‪ww‬اوي ‪ ،1‬فالخوارزمي‪ُ w‬‬
‫‪w‬ة‬ ‫ُ‬ ‫وإنما يهمنا الميل ‪ ،k‬فهو الذي يشير إىل ترتيب النمو‪.‬‬

‫خط ّية؛ أما إذا كانت تساوي ‪ ،2‬فالخوارزمية تربيع ّية‪.‬‬

‫‪49‬‬
‫هياكل البيانات للمبرمجين‬ ‫القائمة المترابطة ‪LinkedList‬‬

‫‪w‬ل تقريب ًي‪ww‬ا‪ ،‬في حين ل‪ww‬و اس‪ww‬تدعينا الت‪ww‬ابع‬ ‫‪w‬اني الس‪ww‬ابق‪ ،‬يُمكِنن‪ww‬ا أن نُق‪w‬دِّر قيم‪ww‬ة َ‬
‫الم ْي‪ِ w‬‬ ‫تأملنا في الرس‪ww‬م البي‪ّ w‬‬
‫إذا ّ‬
‫‪ ،plotResults‬فسيحسب قيمة الميل بتطبيق طريقة المربعات الدنيا ‪ least squares fit‬عىل القياس‪ww‬ات‪،‬‬

‫ثم يَطبَعه‪ .‬و قد كانت قيمة الميل التي حصل عليها التابع‪:‬‬

‫‪Estimated slope = 1.06194352346708‬‬

‫أي تقريبًا يساوي ‪ .1‬إ ًذا فالزمنُ الكلي إلجراء عدد مقداره ‪ n‬من اإلض‪w‬افات ه‪w‬و زمن خطي‪ ،‬وزمن ك‪ww‬ل إض‪w‬افة‬

‫منها ثابت كما توقعنا‪.‬‬

‫خط ّية‪ .‬إذا كان‬


‫مستقيما في رسمة مشابهة للرسمة السابقة‪ ،‬فهذا ال يَعنِي بالضرورة أن الخوارزم ّية ّ‬
‫ً‬ ‫خطا‬
‫إذا رأيت ً‬
‫‪k‬‬
‫مستقيما ميله يساوي ‪ .k‬فإذا كان الميل‬
‫ً‬ ‫خطا‬
‫زمن التشغيل متناسبًا مع ‪ n‬ألي أس ‪ ،k‬فمن المتوقع أن نرى ً‬
‫حتمل أن تكون‬
‫المرجَّح أن تكون الخوارزمية خطية؛ أما إذا كان أقرب الثنين‪ ،‬ف ُي َ‬
‫أقرب للواحد الصحيح‪ ،‬فمن ُ‬
‫تربيعية‪.‬‬

‫‪ 4.5‬تمرين ‪4‬‬
‫ستجد ملفات الشيفرة المطلوبة لهذا التمرين في مستودع الكتاب‪.‬‬

‫‪ :Profiler.java .1‬يحتوي عىل تنفيذ الصنف ‪ Profiler‬الذي شرحناه فيما سبق‪ .‬ستَس‪ww‬تخدِم ذل‪ww‬ك‬

‫الصنف‪ ،‬ولن تحتاج لفهم طريقة عمله‪ ،‬ومع ذلك يُمكِنك االطالع عىل شيفرته إن أردت‪.‬‬

‫‪ :ProfileListAdd.java .2‬يحتوي عىل شيفرة تشغيل التمرين‪ ،‬بم‪ww‬ا في ذل‪ww‬ك المث‪ww‬ال العل‪ww‬وي ال‪ww‬ذي‬

‫شخَّصنا خالله التابع ‪ .ArrayList.add‬ستُعدِّل هذا الملف لتُجرِي التشخيص عىل القليل من التواب‪ww‬ع‬

‫األخرى‪.‬‬

‫ستجد ملف البناء ‪ build.xml‬في المجلد ‪ً code‬‬


‫أيضا‪.‬‬

‫ن ِّفذ األمر ‪ ant ProfileListAdd‬لكي تُش ‪ِّ w‬غل المل‪ww‬ف ‪ .ProfileListAdd.java‬ينبغي أن تحص‪ww‬ل‬

‫تض‪ww‬طر إىل ض‪ww‬بط قيم‪ww‬ة المع‪ww‬املين ‪startN‬‬


‫ّ‬ ‫المرفق‪ww‬ة في األعىل‪ ،‬ولكن‪ww‬ك ق‪ww‬د‬
‫عىل نت‪ww‬ائج مش‪ww‬ابهة للص‪ww‬ورة ُ‬
‫المق‪w‬دَّر قريبً‪ww‬ا من ‪ ،1‬مم‪ww‬ا يَع‪ww‬ني أن تنفي‪ww‬ذ ع‪ww‬دد ‪ n‬من عملي‪ww‬ات اإلض‪ww‬افة‬
‫و‪ .endMillis‬ينبغي أن يكون الميل ُ‬
‫ً‬
‫فارغ‪ ww‬ا اس‪ww‬مه‬ ‫يس‪ww‬تغرق زمنً‪ww‬ا متناس‪ww‬بًا م‪ww‬ع ‪ n‬مرفوع‪ww‬ة لألس ‪ ،1‬أي ينتمي إىل المجموع‪ww‬ة )‪ .O(n‬س‪ww‬تجد تابعً‪ ww‬ا‬

‫‪ profileArrayListAddBeginning‬في الملف ‪ .ProfileListAdd.java‬امأله بشيفر ٍة تفحص الت‪ww‬ابع‬

‫دائم‪ w‬ا إىل بداي‪ww‬ة القائم‪ww‬ة‪ .‬إذا نَس‪ww‬خَت ش‪ww‬يفرة الت‪ww‬ابع‬


‫ً‬ ‫‪ ArrayList.add‬ج‪ww‬اعاًل إي‪ww‬اه يُض‪ِ ww‬يف العنص‪ww‬ر الجدي‪ww‬د‬

‫‪ ،profileArrayListAddEnd‬فستحتاج فق‪ww‬ط إىل إج‪ww‬راء القلي‪ww‬ل من التع‪ww‬ديالت‪ .‬في األخ‪ww‬ير‪ ،‬أض‪ww‬ف س‪ww‬ط ًرا‬

‫ضمن التابع ‪ main‬الستدعاء تلك الدالة‪.‬‬

‫وفس‪w‬ر النت‪ww‬ائج‪ .‬بن‪ww‬ا ًء عىل فهمن‪ww‬ا لطريق‪ww‬ة عم‪ww‬ل الص‪ww‬نف‬


‫ِّ‬ ‫ن ِّفذ األم‪ww‬ر ‪ ant ProfileListAdd‬م‪ً w‬‬
‫‪w‬رة أخ‪ww‬رى‬
‫َّ‬
‫نتوقع أن تَستغرِق عملية اإلضافة الواحدة زمنً‪w‬ا خط ًي‪w‬ا‪ ،‬وبالت‪ww‬الي س‪ww‬يكون ال‪w‬زمن الكلي ال‪w‬ذي‬ ‫‪ ،ArrayList‬فإننا‬

‫‪50‬‬
‫هياكل البيانات للمبرمجين‬ ‫القائمة المترابطة ‪LinkedList‬‬

‫المق‪w‬دَّر للخ‪ww‬ط بمقي‪ww‬اس‬


‫يَس‪ww‬تغرِقه ع‪ww‬دد مق‪ww‬داره ‪ n‬من اإلض‪ww‬افات تربيع ًي‪ww‬ا‪ .‬إن ك‪ww‬ان ذل‪ww‬ك ص‪ww‬حيحًا‪ ،‬ف‪ww‬إن المي‪ww‬ل ُ‬
‫لوغاريتمي‪-‬لوغاريتمي ينبغي أن يكون قريبًا من ‪.2‬‬

‫بع‪wwwwwwww‬د ذل‪wwwwwwww‬ك‪ ،‬وازن ذل‪wwwwwwww‬ك األداء م‪wwwwwwww‬ع أداء الص‪wwwwwwww‬نف ‪ .LinkedList‬امأل متن الت‪wwwwwwww‬ابع‬

‫‪ profileLinkedListAddBeginning‬واس‪ww‬تخدمه لتص‪ww‬نيف الت‪ww‬ابع ‪ LinkedList.add‬بينم‪ww‬ا يُض‪ِ ww‬يف‬

‫عنصرًا جديدًا إىل بداية القائمة‪ .‬ما األداء الذي تتوقعه؟ وهل تتوافق النتائج مع تلك التوقعات؟‬

‫أخي ًرا‪ ،‬امأل متن التابع ‪ ،profileLinkedListAddEnd‬واِستخدَمه لتصنيف الت‪ww‬ابع ‪LinkedList.add‬‬

‫بينما يُض ِيف عنصرًا جديدًا إىل نهاية القائمة‪ .‬ما األداء الذي تتوقعه؟ وهل تتوافق النتائج مع تلك التوقعات؟‬

‫سنَعرِض النتائج ونجيب عىل تلك األسئلة في الفصل التالي‪.‬‬

‫‪51‬‬
‫‪ .5‬القائمة ازدواجية الرتابط‬

‫‪Doubly-Linked List‬‬

‫سنراجع في هذا الفصل نت‪w‬ائج تم‪w‬رين الفص‪ww‬ل الرابع الس‪w‬ابق‪ ،‬ثم س‪w‬نُقدِّم تنفي‪ً w‬ذا آخ‪w‬رَ للواجه‪ww‬ة ‪ ،List‬وه‪ww‬و‬

‫القائمة ازدواجية الترابط ‪.doubly-linked list‬‬

‫‪ 5.1‬نتائج تشخيص األداء‬


‫اِس‪www‬تخدَمنا الص‪www‬نف ‪- Profiler.java‬في التم‪www‬رين المش‪www‬ار إلي‪www‬ه‪ -‬لكي نُطبِّق عملي‪www‬ات الص‪www‬نفين‬

‫‪ ArrayList‬و‪ LinkedList‬عىل أحجام مختلفة من المشكلة‪ ،‬ثم عرضنا زمن التشغيل مقاب‪ww‬ل حجم المش‪ww‬كلة‬

‫بمقياس لوغ‪ww‬اريتمي‪-‬لوغ‪ww‬اريتمي ‪ ،log-log‬وق‪w‬دّرنا مي‪w‬ل المنح‪ww‬ني الن‪w‬اتج‪ .‬يُ ِّ‬


‫وض‪w‬ح ذل‪w‬ك المي‪ww‬ل العالق‪ww‬ة بين زمن‬

‫التشغيل وحجم المشكلة‪.‬‬

‫فعىل سبيل المثال‪ ،‬عندما استخدمنا التابع ‪ add‬إلضافة عناص‪ww‬ر إىل نهاي‪ww‬ة قائم‪ww‬ة من الن‪ww‬وع ‪،ArrayList‬‬

‫وجدنا أن الزمن الكلّي لتنفيذ عدد ‪ n‬من اإلضافات يتناسب مع ‪ ،n‬أي أن المي‪w‬ل ُ‬
‫المق‪w‬دَّر ك‪w‬ان قريبً‪w‬ا من ‪ ،1‬وبن‪w‬ا ًء‬

‫عىل ذلك استنتجنا أن تنفيذ عدد ‪ n‬من اإلضافات ينتمي إىل المجموعة )‪ ،O(n‬وأن تنفي َذ إض‪ww‬افةٍ واح‪ww‬د ٍة يتطلَّب‬

‫زمنً‪ww‬ا ثاب ًت‪ww‬ا في المتوس‪ww‬ط‪ ،‬أي أن‪ww‬ه ينتمي إىل المجموع‪ww‬ة )‪ ،O(1‬وه‪ww‬و نفس م‪ww‬ا توص‪ww‬لنا إلي‪ww‬ه باس‪ww‬تخدام تحلي‪ww‬ل‬

‫الخوارزميات‪.‬‬

‫ك‪ww‬ان المطل‪ww‬وب من ذل‪ww‬ك التم‪ww‬رين ه‪ww‬و إكم‪ww‬ال متن الت‪ww‬ابع ‪ profileArrayListAddBeginning‬ال‪ww‬ذي‬

‫يُشخ ِّص عملية إضافة عناصر جديدة إىل بداية قائمة من النوع ‪ .ArrayList‬وبنا ًء عىل تحليلنا للخوارزمية‪ ،‬فق‪ww‬د‬
‫توقعنا أن يتطلَّب تنفيذ إضافة واحدة زمنًا خط ًيا بس‪ww‬بب تحري‪ww‬ك العناص‪ww‬ر األخ‪ww‬رى إىل اليمين‪ ،‬وعلي‪ww‬ه َّ‬
‫توقعن‪ww‬ا أن‬ ‫ّ‬

‫يتطلَّب تنفيذ عدد ‪ n‬من اإلضافات زمنًا تربيع ًيا‪.‬‬

‫انظر إىل حل التمرين الذي ستجده في مجلد الحل داخل مستودع الكتاب‪:‬‬
‫هياكل البيانات للمبرمجين‬ Doubly-Linked List ‫القائمة ازدواجية الترابط‬

public static void profileArrayListAddBeginning() {


Timeable timeable = new Timeable() {
List<String> list;

public void setup(int n) {


list = new ArrayList<String>();
}

public void timeMe(int n) {


for (int i=0; i<n; i++) {
list.add(0, "a string");
}
}
};
int startN = 4000;
int endMillis = 10000;
runProfiler("ArrayList add beginning", timeable, startN,
endMillis);
}

‫ابع‬ww‫ود في الت‬ww‫د موج‬ww‫ارق الوحي‬ww‫ فالف‬،profileArrayListAddEnd ‫ابع‬ww‫ع الت‬ww‫ا م‬wwً‫يتطابق هذا التابع تقريب‬
َ
‫ا‬ww‫ كم‬،0 ‫رس‬ww‫دة في الفه‬ww‫ لكي يضع العناصر الجدي‬add ‫ثنائية المعامل من التابع‬ ً
‫نسخة‬ ‫ حيث يَستخدِم‬،timeMe

.‫بيانات إضاف ّية‬


ٍ ‫ لكي يحصل عىل نقطة‬endMillis ‫أنه يزيد من قيمة‬

:)‫انظر إىل النتائج (حجم المشكلة عىل اليسار وزمن التشغيل بوحدة الميلي ثانية عىل اليمين‬

14
35
150
604
2518
11555

.problem size ‫ مقابل حجم المشكلة‬runtime ‫رسما بيان ًيا لزمن التشغيل‬
ً ‫تَعرِض الصورة التالية‬

53
‫هياكل البيانات للمبرمجين‬ ‫‪Doubly-Linked List‬‬ ‫القائمة ازدواجية الترابط‬

‫خط ّي‪ww‬ة‪ ،‬وإنم‪ww‬ا يع‪ww‬ني أن‪ww‬ه إذا ك‪ww‬ان زمن‬


‫ال يَعنِي ظهور خ‪ww‬ط مس‪ww‬تقيم في ه‪ww‬ذا الرس‪ww‬م البي‪ww‬اني أن الخوارزمي‪ww‬ة ّ‬
‫َّ‬
‫نتوقع في ه‪ww‬ذا‬ ‫‪w‬تقيما ميل‪ww‬ه يس‪ww‬اوي ‪.k‬‬
‫ً‬ ‫خطا مس‪w‬‬ ‫َّ‬
‫المتوقع أن ن‪w‬رى ًّ‬ ‫التشغيل متناس‪w‬بًا م‪ww‬ع ‪ nk‬ألي أس ‪ ،k‬فإن‪ww‬ه من‬

‫‪w‬ل يس‪ww‬اوي‬
‫خط مستقيمٍ بمي‪ٍ w‬‬
‫‪2‬‬
‫ي لعدد ‪ n‬من اإلضافات متناسبًا مع ‪ ،n‬وأن نحصل عىل ٍّ‬ ‫ّ‬
‫المثال أن يكون الزمنُ الكل ّ‬
‫المقدَّر ‪ 1.992‬تقريبًا‪ ،‬وهو في الحقيقة دقيق جدًا لدرجةٍ تجعلنا ال نرغب في تزوير‬
‫‪ ،2‬وفي الحقيقة يساوي الميل ُ‬
‫بيانات بهذه الجودة‪.‬‬

‫‪ 5.2‬تشخيص توابع الصنف ‪LinkedList‬‬


‫طلب التمرين المشار إليه منك ً‬
‫أيضا تشخيص أداء عملية إضافة عناص َر جدي‪ww‬د ٍة إىل بداي‪ww‬ة قائم‪ww‬ةٍ من الن‪ww‬وع‬ ‫َ‬
‫توقعنا أن يتطلَّب تنفيذ إضافةٍ واحد ٍة زمنًا ثابتًا؛ ألننا ال نض‪ّ w‬‬
‫‪w‬طر إىل‬ ‫‪ .LinkedList‬وبنا ًء عىل تحليلنا للخوارزمية‪ّ ،‬‬

‫‪w‬دة جدي‪ً w‬‬


‫‪w‬دة إىل بداي‪ww‬ة القائم‪ww‬ة‪ ،‬وعلي‪ww‬ه‬ ‫تحريك العناصر الموجودة في هذا النوع من القوائم‪ ،‬وإنما نضيف فق‪ww‬ط عق‪ً w‬‬

‫توقعنا أن يتطلَّب تنفيذ عدد ‪ n‬من اإلضافات زمنًا ّ‬


‫خط ًّيا‪ .‬انظر شيفرة الحل‪:‬‬ ‫َّ‬

‫{ )(‪public static void profileLinkedListAddBeginning‬‬


‫{ )(‪Timeable timeable = new Timeable‬‬
‫;‪List<String> list‬‬

‫{ )‪public void setup(int n‬‬


‫;)(>‪list = new LinkedList<String‬‬
‫}‬

‫{ )‪public void timeMe(int n‬‬


‫{ )‪for (int i=0; i<n; i++‬‬
‫;)"‪list.add(0, "a string‬‬

‫‪54‬‬
‫هياكل البيانات للمبرمجين‬ ‫‪Doubly-Linked List‬‬ ‫القائمة ازدواجية الترابط‬

‫}‬
‫}‬
‫;}‬
‫;‪int startN = 128000‬‬
‫;‪int endMillis = 2000‬‬
‫‪runProfiler("LinkedList add beginning", timeable, startN,‬‬
‫;)‪endMillis‬‬
‫}‬

‫اضطرّرنا إىل إجراء القليل من التع‪ww‬ديالت‪ ،‬فع‪w‬دّلنا الص‪ww‬نف ‪ ArrayList‬إىل الص‪ww‬نف ‪ ،LinkedList‬كم‪ww‬ا‬

‫ضبطنا قيمة المعاملين ‪ startN‬و‪ endMillis‬لكي نحصل عىل قياسات مناسبة‪ ،‬فق‪ww‬د الحظن‪ww‬ا أن القياس‪ww‬ات‬

‫ليست بدقة القياسات السابقة‪ .‬انظر إىل النتائج‪:‬‬

‫‪16‬‬
‫‪19‬‬
‫‪28‬‬
‫‪77‬‬
‫‪330‬‬
‫‪892‬‬
‫‪1047‬‬
‫‪4755‬‬

‫‪55‬‬
‫هياكل البيانات للمبرمجين‬ ‫‪Doubly-Linked List‬‬ ‫القائمة ازدواجية الترابط‬

‫تماما‪ ،‬وميل الخيط ال يساوي ‪ 1‬بالض‪ww‬بط‪ ،‬وق‪ww‬د ق‪w‬دَّرت المربع‪ww‬ات ال‪ww‬دنيا ‪least‬‬
‫ً‬ ‫لم نحصل عىل خط مستقيم‬

‫‪ squares fit‬الميل بحوالي ‪ ،1.23‬ومع ذلك تشير تلك النتائج إىل أن الزمن الكلي لعدد ‪ n‬من اإلض‪ww‬افات ينتمي‬

‫إىل المجموعة )‪ O(n‬عىل األقل‪ ،‬وبالتالي يتطلَّب تنفي ُذ إضافةٍ واحد ٍة زمنًا ثابتًا‪.‬‬

‫‪ 5.3‬اإلضافة إىل نهاية قائمة من الصنف ‪LinkedList‬‬


‫َّ‬
‫نتوقع أن يكون الصنف ‪ LinkedList‬أثن‪ww‬اء‬ ‫ً‬
‫واحدة من العمليات التي‬ ‫تُع ّد إضافة العناصر إىل بداية القائمة‬
‫َّ‬
‫نتوقع‬ ‫تنفيذها أسر ع من الصنف ‪ArrayList‬؛ وفي المقابل‪ ،‬بالنسبة إلضافة العناصر إىل نهاية القائم‪ww‬ة‪ ،‬فإنن‪ww‬ا‬

‫يضطر ت‪w‬ابع اإلض‪w‬افة إىل الم‪w‬رور ع‪w‬بر قائم‪w‬ة العناص‪w‬ر بالكام‪w‬ل لكي‬
‫ّ‬ ‫أن يكون الصنف ‪ LinkedList‬أبطأ‪ ،‬حيث‬
‫َّ‬
‫نتوقع أن يكون ال‪ww‬زمن الكلي لع‪ww‬دد‬ ‫يتمكَّن من إضافة عنصر جديد إىل النهاية‪ ،‬مما يَعنِي أن العملية خطية‪ ،‬وعليه‬

‫‪ n‬من اإلضافات تربيع ًيا‪.‬‬

‫في الواقع هذا ليس صحيحًا‪ ،‬ويمكنك االطالع إىل الشيفرة التالية‪:‬‬

‫{ )(‪public static void profileLinkedListAddEnd‬‬


‫{ )(‪Timeable timeable = new Timeable‬‬
‫;‪List<String> list‬‬

‫{ )‪public void setup(int n‬‬


‫;)(>‪list = new LinkedList<String‬‬
‫}‬

‫{ )‪public void timeMe(int n‬‬


‫{ )‪for (int i=0; i<n; i++‬‬
‫;)"‪list.add("a string‬‬
‫}‬
‫}‬
‫;}‬
‫;‪int startN = 64000‬‬
‫;‪int endMillis = 1000‬‬
‫‪runProfiler("LinkedList add end", timeable, startN,‬‬
‫;)‪endMillis‬‬
‫}‬

‫ها هي النتائج التي حصلنا عليها‪:‬‬

‫‪9‬‬
‫‪9‬‬

‫‪56‬‬
‫هياكل البيانات للمبرمجين‬ ‫‪Doubly-Linked List‬‬ ‫القائمة ازدواجية الترابط‬

‫‪21‬‬
‫‪24‬‬
‫‪78‬‬
‫‪235‬‬
‫‪851‬‬
‫‪950‬‬
‫‪6160‬‬

‫‪w‬اوي ‪،1.19‬‬
‫المق ‪w‬دّر يُس‪ِ w‬‬
‫تماما‪ ،‬والميل ُ‬
‫ً‬ ‫مستقيما‬
‫ً‬ ‫كما ترى هنا فالقياسات غير دقيقة ً‬
‫أيضا‪ ،‬كما أن الخط ليس‬
‫قريب لما حصلنا عليه عند إضافة العناصر إىل بداية القائمة‪ ،‬وليس قريبًا من ‪ 2‬الذي ّ‬
‫توقعنا أن نحصل علي‪ww‬ه‬ ‫ٌ‬ ‫وهو‬

‫بنا ًء عىل تحليلنا للخوارزمية‪ .‬في الواقع‪ ،‬هو أقرب إىل ‪ ،1‬مما قد يشير إىل أن إض‪ww‬افة العناص‪ww‬ر إىل نهاي‪ww‬ة القائم‪ww‬ةِ‬

‫يستغرق زمنًا ثابتًا‪.‬‬

‫‪ 5.4‬القوائم ازدواجية الرتابط ‪Doubly-linked list‬‬

‫الفكرة هي أن الصنف ‪ MyLinkedList‬ال‪ww‬ذي ن َّفذناه يَس‪ww‬تخدِم قائم‪ww‬ة مترابط‪ww‬ة أحادي‪ww‬ة‪ ،‬أي أن ك‪ww‬ل عنص‪ww‬ر ٍ‬
‫رابط واح ٍد إىل العنصر التالي‪ ،‬في حين يحتوي الكائن ‪ MyArrayList‬نفس‪ww‬ه عىل راب‪ww‬ط إىل العق‪ww‬دة‬
‫ٍ‬ ‫يحتوي عىل‬

‫األوىل‪.‬‬

‫في المقابل‪ ،‬إذا اطلعت عىل توثيق الصنف ‪ LinkedList‬باللغة اإلنجليزية الذي تُ‪ww‬و ِّفره جاف‪ww‬ا‪ ،‬فإنن‪ww‬ا نج‪ww‬د م‪ww‬ا‬

‫يلي‪:‬‬

‫الم َّ‬
‫توقع من قائمةٍ‬ ‫تنفيذ قائمة ازدواجية الترابط للواجهتين ‪ List‬و‪ .Deque‬تَ َ‬
‫عمل جميع العمليات بالشكل ُ‬
‫ازدواجية الترابط‪ ،‬أي تؤدي عمليات استرجاع فهرس معين إىل اجتياز أو التنقل في عناصر القائمة من البداية أو‬

‫من النهاية بنا ًء عىل أيهما أقرب لذلك الفهرس‪.‬‬

‫‪57‬‬
‫هياكل البيانات للمبرمجين‬ ‫‪Doubly-Linked List‬‬ ‫القائمة ازدواجية الترابط‬

‫العامة عنها‪ ،‬إذ فيها‪:‬‬


‫ّ‬ ‫يمكنك ‪-‬إذا أردت‪ -‬معرفة المزيد عن القوائم ازدواجية الترابط‪ ،‬وفيما يلي نذكر الفكرة‬

‫ورابط إىل العقدة السابقة‪.‬‬


‫ٍ‬ ‫رابط إىل العقدة التالية‬
‫ٍ‬ ‫تحتوي كل عقد ٍة عىل‬ ‫•‬

‫تحتوي كائنات الصنف ‪ LinkedList‬عىل روابط إىل العنصر األول والعنصر األخير في القائمة‪.‬‬ ‫•‬

‫بنا ًء عىل ما سبق‪ ،‬يُمكِننا أن نبدأ من أي طرف‪ ،‬وأن نجتاز القائمة بأي اتجاه‪ ،‬وعلي‪ww‬ه تتطلَّب إض‪ww‬افة العناص‪ww‬ر‬

‫وحذفها من بداية القائمة أو نهايتها زمنًا ثابتًا‪.‬‬

‫خص ‪w‬ص ‪MyLinkedList‬‬


‫الم َّ‬ ‫الم َّ‬
‫توقع من الص‪ww‬نف ‪ ArrayList‬والص‪ww‬نف ُ‬ ‫يُلخ ِّص الج‪ww‬دول الت‪ww‬الي األداء ُ‬
‫الذي تحتوي عقده عىل رابط واحد والصنف ‪ LinkedList‬الذي تحتوي عقده عىل رابطين‪:‬‬

‫‪LinkedList‬‬ ‫‪MyLinkedList‬‬ ‫‪MyArrayList‬‬


‫‪1‬‬ ‫‪n‬‬ ‫‪1‬‬ ‫‪( add‬بالنهاية)‬

‫‪1‬‬ ‫‪1‬‬ ‫‪n‬‬ ‫‪( add‬بالبداية)‬

‫‪n‬‬ ‫‪n‬‬ ‫‪n‬‬ ‫‪( add‬في العموم)‬

‫‪n‬‬ ‫‪n‬‬ ‫‪1‬‬ ‫‪get / set‬‬

‫‪indexOf /‬‬
‫‪n‬‬ ‫‪n‬‬ ‫‪n‬‬
‫‪lastIndexOf‬‬

‫‪1‬‬ ‫‪1‬‬ ‫‪1‬‬ ‫‪isEmpty / size‬‬

‫‪1‬‬ ‫‪n‬‬ ‫‪1‬‬ ‫‪( remove‬من النهاية)‬

‫‪1‬‬ ‫‪1‬‬ ‫‪n‬‬ ‫‪( remove‬من البداية)‬

‫‪n‬‬ ‫‪n‬‬ ‫‪n‬‬ ‫‪( remove‬في العموم)‬

‫‪ 5.5‬اختيار هيكل البيانات األنسب‬


‫يُع ّد التنفيذ مزدوج الروابط أفضل من التنفيذ ‪ ArrayList‬فيما يتعلَّق بعمليتي اإلضافة والحذف من بداية‬

‫القائمة‪ ،‬ويتمتعان بنفس الكفاءة فيم‪ww‬ا يتعلَّق بعملي‪ww‬تي اإلض‪ww‬افة والح‪ww‬ذف من نهاي‪ww‬ة القائم‪ww‬ة‪ ،‬وبالت‪ww‬الي تقتص‪ww‬ر‬

‫أفضلية الصنف ‪ ArrayList‬عليه بعمليتي ‪ get‬و‪ ،set‬ألنهما تتطلبان زمنًا خط ًيا في القوائم المترابطة حتى لو‬

‫كانت مزدوجة‪.‬‬

‫إذا كان زمن تشغيل التطبيق الخاص بك يعتم‪ww‬د عىل ال‪ww‬زمن ال‪ww‬ذي تتطلَّب‪ww‬ه عمليت‪ww‬ا ‪ get‬و‪ ،set‬فق‪ww‬د يك‪ww‬ون‬

‫التنفيذ ‪ ArrayList‬هو الخيار األفضل؛ أما إذا كان يَعتمِ د عىل عملية إضافة العناصر وحذفها إىل بداي‪ww‬ة القائم‪ww‬ة‬

‫ونهايتها‪ ،‬فلربما التنفيذ ‪ LinkedList‬هو الخيار األفضل‪.‬‬

‫التوصيات مبن ّي ٌة عىل ترتيب النمو ‪ order of growth‬لألحجام الكبيرة من المش‪ww‬كالت‪.‬‬


‫ِ‬ ‫ولكن تذ ّكر أن هذه‬

‫هنالك عوامل أخرى ينبغي أن تأخذها في الحسبان ً‬


‫أيضا‪:‬‬

‫‪58‬‬
‫هياكل البيانات للمبرمجين‬ ‫‪Doubly-Linked List‬‬ ‫القائمة ازدواجية الترابط‬

‫لو لم تكن تلك العمليات تستغرِق جزءًا كبيرًا من زمن تشغيل التطبيق الخاص بك ‪-‬أي لو ك‪ww‬ان التط‪ww‬بيق‬ ‫•‬

‫يقضي غالبية زمن تشغيله في تنفيذ أشياء أخرى‪ ،-‬فإن اختيارك لتنفي‪ww‬ذ الواجه‪ww‬ة ‪ List‬غ‪ww‬ير مهم لتل‪ww‬ك‬
‫ِ‬

‫الدرجة‪.‬‬

‫َّ‬
‫تتوقع‪ww‬ه‪ ،‬فبالنس‪ww‬بة‬ ‫ً‬
‫كبيرة بدرجة كافية‪ ،‬فلربما لن تحصل عىل األداء الذي‬ ‫إذا لم تكن القوائم التي تُعالجها‬ ‫•‬

‫للمشكالت الصغيرة‪ ،‬قد تكون الخوارزمية التربيعية أسر ع من الخوارزمية الخطية‪ ،‬وقد تكون الخوارزمي‪ww‬ة‬

‫هم كثيرًا‪.‬‬
‫الخطية أسر ع من الخوارزمية ذات الزمن الثابت‪ ،‬كما أن االختالف بينها في العموم ال يُ ّ‬

‫أيض ‪w‬ا‪ ،‬إذ تتطلَّب‬


‫ال تنسى عامل المساحة‪ .‬ركزَّنا حتى اآلن عىل زمن التشغيل‪ ،‬ولكن عامل المساحة مهم ً‬ ‫•‬
‫ً‬
‫مختلفة من الذاكرة‪ ،‬وتُخزَّن العناصر في قائمةٍ من الص‪ww‬نف ‪ArrayList‬‬ ‫مساحات‬
‫ٍ‬ ‫التنفيذات المختلفة‬

‫إىل جانب بعضها البعض ض‪ww‬من قطع‪ww‬ة واح‪ww‬دة من ال‪ww‬ذاكرة‪ ،‬وبالت‪ww‬الي ال تُب‪w‬دَّد مس‪ww‬احة ال‪ww‬ذاكرة‪ ،‬كم‪ww‬ا أن‬

‫الحاسوب عاد ًة ما يكون أسر ع عندما يتعامل م‪ww‬ع أج‪ww‬زاء متص‪ww‬لة من ال‪ww‬ذاكرة‪ .‬في المقاب‪ww‬ل‪ ،‬يتطلَّب ك‪ww‬ل‬

‫رابط أو رابطين‪.‬‬
‫ٍ‬ ‫عنصر في القوائم المترابطة عقد ًة مك َّو ً‬
‫نة من‬

‫تحتل تلك الروابط حيزًا من الذاكرة ‪ -‬أحيانًا ما يكون أكبرَ من الحيز ِ الذي تحتله البيان‪ww‬ات نفس‪ww‬ها‪ ،-‬كم‪ww‬ا تك‪ww‬ون‬
‫ً‬
‫كفاءة في تعامله معها‪.‬‬ ‫ً‬
‫مبعثرة ضمن أجزا ٍء مختلفةٍ من الذاكرة‪ ،‬مما يَجعَ ل الحاسوب أقلّ‬ ‫تلك العق ُد‬

‫خالصة القول هي أن تحلي‪ww‬ل الخوارزمي‪ww‬ات يُ‪ww‬و ِّفر بعض اإلرش‪ww‬ادات ال‪ww‬تي ق‪ww‬د تس‪ww‬اعدك عىل اختي‪ww‬ار هياك‪ww‬ل‬

‫البيانات األنسب‪ ،‬ولكن بشروط‪:‬‬

‫‪ .1‬زمن تشغيل التطبيق مهم‪.‬‬

‫‪ .2‬زمن تشغيل التطبيق يعتمد عىل اختيارك لهيكل البيانات‪.‬‬

‫‪ .3‬حجم المشكلة كبير ٌ بالقدر الكافي بحيث يتمكن ترتيب النمو من توقع هيكل البيانات األنسب‪.‬‬

‫في الحقيقة‪ ،‬يُمكِنك أن تتمتع بحيا ٍة مهن ّيةٍ طويلةٍ أثن‪ww‬اء عمل‪ww‬ك كمهن‪ww‬دس برمجي‪ww‬ات دون أن تتع ‪w‬رَّض له‪ww‬ذا‬

‫الموقف عىل اإلطالق‪.‬‬

‫‪59‬‬
‫‪ .6‬التنقل في الشجرة ‪Tree Traversal‬‬

‫َصف مك ِّونات‪ww‬ه‬ ‫ً‬


‫سريعة عن تطبيق محرك البحث الذي ننوي بناءه‪ ،‬حيث سن ِ‬ ‫ً‬
‫مقدمة‬ ‫سنتناول في هذا الفصل‬

‫ونشرح ُأوالها‪ ،‬وهي عبارة عن زاحف ويب ‪ crawler‬يُ ِّ‬


‫حمل ص‪ww‬فحات موق‪ww‬ع ويكيبي‪ww‬ديا ويُحلِّله‪ww‬ا‪ .‬س‪ww‬نتناول ً‬
‫أيض‪w‬ا‬

‫تنفي ًذا تعاوديً‪ww‬ا ‪ recursive‬ألس‪ww‬لوب البحث ب‪ww‬العمق أواًل ‪ depth-first‬وك‪ww‬ذلك تنفي ‪ً w‬ذا تكراريً‪ww‬ا ُ‬
‫للمك ‪w‬دِّس ‪stack‬‬

‫(الداخل آخرًا‪ ،‬يخرج أواًل ‪ )LIFO‬باستخدام ‪.Deque‬‬

‫‪ 6.1‬محركات البحث‬
‫مجموعة من كلم‪ww‬ات البحث‪ ،‬وتعي‪ww‬د قائم‪ً w‬‬
‫‪w‬ة بص‪ww‬فحات‬ ‫ً‬ ‫تستقبل محركات البحث ‪-‬مثل محرك جوجل وبينغ‪-‬‬

‫اإلنترنت المرتبطة بتلك الكلمات (سنناقش ما تعنيه كلم‪w‬ة مرتبط‪w‬ة الح ًق‪w‬ا)‪ .‬يُمكِن‪w‬ك ق‪w‬راءة المزي‪w‬د عن محرك‪w‬ات‬

‫البحث (باللغة اإلنجليزية)‪ ،‬ولكننا سنشرح هنا كل ما قد تحتاج إليه‪.‬‬

‫مكونات أساسيةٍ نستعرضها فيما يلي‪:‬‬


‫ٍ‬ ‫يتك ّون أي محرك بحث من عدة‬

‫الزحف ‪ :crawling‬برن‪ww‬امج بإمكان‪ww‬ه تحمي‪ww‬ل ص‪ww‬فحة إن‪ww‬ترنت وتحليله‪ww‬ا واس‪ww‬تخراج النص وأي رواب‪ww‬ط إىل‬ ‫•‬

‫صفحات أخرى‪.‬‬

‫الفهرسة ‪ :indexing‬هيكل بيانات ‪ data structure‬بإمكانه البحث عن كلمةٍ والعث‪ww‬ور عىل الص‪ww‬فحات‬ ‫•‬

‫التي تحتوي عىل تلك الكلمة‪.‬‬

‫ً‬
‫صلة بكلمات البحث‪.‬‬ ‫المفهرِس واختيار الصفحات األكثر‬
‫االسترجاع ‪ :retrieval‬طريقة لتجميع نتائج ُ‬ ‫•‬

‫سنبدأ بالزاحف‪ ،‬والذي تتلخص مهمته في اكتشاف مجموعة من صفحات الويب وتحميلها‪ ،‬في حين تهدف‬

‫محركات البحث مثل جوجل وبينغ إىل العثور عىل جميع صفحات اإلنترنت‪ ،‬لكن المعتاد ً‬
‫أيضا أن يك‪ww‬ون الزاح‪ww‬ف‬

‫مقتص ًرا عىل نطاق أصغر‪ .‬وفي حالتنا هذه‪ ،‬سنقتصر عىل صفحات موقع ويكيبيديا فقط‪.‬‬
‫هياكل البيانات للمبرمجين‬ ‫التنقل في الشجرة ‪Tree Traversal‬‬

‫رابط ضمن الصفحة‪ ،‬وينتقل إىل‬


‫ٍ‬ ‫ً‬
‫صفحة من موقع ويكيبيديا‪ ،‬ويبحث عن أول‬ ‫في البداية‪ ،‬سنبني زاح ًفا يقرأ‬

‫الصفحة التي يشير إليها الرابط‪ ،‬ثم يكرر األمر‪ .‬سنَستخدِم ذلك الزاحف الختبار صحة فرض ّية فرض ‪ّ w‬ية الطري‪ww‬ق إىل‬

‫تنص عىل ما يلي‪:‬‬


‫الفلسفة‪ ،‬الموجودة في صفحات ويكيبيديا والتي ّ‬

‫بأحرف صغير ٍة في أي مقالةٍ في موقع ويكيبيديا‪ ،‬وكرَّرت ذلك عىل المقاالت‬


‫ٍ‬ ‫مكتوب‬
‫ٍ‬ ‫رابط‬
‫ٍ‬ ‫إذا نقرت عىل أول‬

‫التالية‪ ،‬فسينتهي بك المطاف إىل مقالة الفلسفة في موقع ويكيبيديا‪.‬‬

‫سيسمح لنا اختبار تلك الفرض ّية ببناء القطع األساسية للزاحف بدون الحاجة إىل الزحف عبر اإلنترنت بأكمل‪ww‬ه‬

‫أو حتى عبر كل صفحات موقع ويكيبيديا‪ ،‬كما أن هذا التمرين ممت ٌع نو ًعا ما‪.‬‬

‫سترجع فسنبني كاًّل منهما في فصل مستقلّ الح ًقا‪.‬‬


‫ِ‬ ‫والم‬
‫ُ‬ ‫المفهرس‬
‫أما ُ‬
‫ّ‬

‫‪ 6.2‬تحليل مستند ‪HTML‬‬


‫ً‬
‫مكتوبة بلغة ترم‪ww‬يز النص الف‪ww‬ائق ‪HyperText Markup‬‬ ‫عندما تُ ِّ‬
‫حمل صفحة إنترنت‪ ،‬فإن محتوياتها تكون‬

‫ختصر عاد ًة إىل ‪ .HTML‬عىل سبيل المثال‪ ،‬انظر إىل مستند ‪ HTML‬التالي‪:‬‬
‫‪ ،Language‬التي تُ َ‬

‫>‪<!DOCTYPE html‬‬
‫>‪<html‬‬
‫>‪<head‬‬
‫>‪<title>This is a title</title‬‬
‫>‪</head‬‬
‫>‪<body‬‬
‫>‪<p>Hello world!</p‬‬
‫>‪</body‬‬
‫>‪</html‬‬

‫الفعلي المعروض في الصفحة‪ ،‬أما بقية العناصر‬


‫َّ‬ ‫النص‬
‫َّ‬ ‫تُم ِّثل العبارات "‪ "This is a title‬و"!‪"Hello world‬‬

‫فهي عبارة عن وسوم ‪ tags‬تشير إىل الكيفية التي ستُعرَض بها تلك النصوص‪.‬‬

‫حمل الزاحف صفحة إن‪w‬ترنت‪ ،‬يُحلِّل محتوياته‪w‬ا المكتوب‪w‬ة بلغ‪w‬ة ‪ HTML‬ليتمكَّن من اس‪w‬تخراج النص‬
‫بعد أن يُ ِّ‬
‫وإيجاد الروابط‪ .‬سنَستخدِم مكتبة ‪ jsoup‬مفتوحة المصدر من لغة جافا إلجراء ذلك‪ ،‬حيث تستطيع تلك المكتب‪ww‬ة‬

‫تحميل صفحات ‪ HTML‬وتحليلها‪.‬‬

‫ينتج عن تحليل مستندات ‪ HTML‬شجرة نموذج كائن المستند ‪ Document Object Model‬التي تُختصرُ‬
‫إىل ‪ ،DOM‬حيث تحتوي تلك الشجرة عىل ما يتضمنه المس‪ww‬تند من عناص‪ww‬ر بم‪ww‬ا في ذل‪ww‬ك النص‪ww‬وص والوس‪ww‬وم‪،‬‬

‫مترابطا ‪ linked‬يتألف من عقد ‪ nodes‬تُم ِّثلُ كاًل من النصوص والوسوم والعناصر األخرى‪.‬‬
‫ً‬ ‫بيانات‬
‫ٍ‬ ‫تم ِّثل هيكل‬

‫‪61‬‬
‫هياكل البيانات للمبرمجين‬ ‫التنقل في الشجرة ‪Tree Traversal‬‬

‫وض‪w‬ح في األعىل مثاًل ‪ ،‬العق‪ww‬دة‬ ‫تُحدِّد بنية المستند العالقات بين العقد‪ .‬يُع ّد الوسم <‪- >html‬في المث‪ww‬ال ُ‬
‫الم َّ‬

‫األوىل التي يُطلَق عليها اسم الجذر ‪ ،root‬وتحتوي تلك العقدة عىل روابط تش‪ww‬ير إىل العق‪ww‬د ال‪ww‬تي تتض‪ww‬منها وفي‬

‫حالتنا هما العقدتان <‪ >head‬و<‪ ،>body‬وتُع ّد ك ٌّ‬


‫ل منهما ابنًا للعقدة الجذر‪.‬‬

‫تملك العقدة <‪ >head‬ابنًا واحدًا ه‪w‬و العق‪w‬دة <‪ ،>title‬وبالمث‪ww‬ل‪ ،‬تمل‪ww‬ك العق‪w‬دة <‪ >body‬ابنً‪w‬ا واح‪w‬دًا ه‪w‬و‬

‫العقدة <‪( >p‬اختصار لكلمة ‪ .)paragraph‬تُ ِّ‬


‫وضح الصورة التالية تلك الشجرة بيان ًيا‪.‬‬

‫تحتوي كلّ عقدة عىل روابط إىل عقد األبن‪w‬اء‪ ،‬كم‪w‬ا تحت‪w‬وي عىل راب‪w‬ط إىل عق‪w‬دة األب الخاص‪w‬ة به‪w‬ا‪ ،‬وبالت‪w‬الي‬

‫يُمكِننا أن نبدأ من أي عقدة في الشجرة‪ ،‬ثم نتن ّقل إىل أعاله‪ww‬ا أو أس‪ww‬فلها‪ .‬ع‪ww‬اد ًة م‪ww‬ا تك‪ww‬ون أش‪ww‬جار ‪ُ DOM‬‬
‫الممثِل‪ww‬ة‬

‫للصفحات الحقيقية أعق َد بكثير ٍ من هذا المثال‪.‬‬

‫تُ‪ww‬و ِّفر غالبي‪ww‬ة متص‪ww‬فحات اإلن‪ww‬ترنت أدوات للتح ّق‪ww‬ق من نم‪ww‬وذج ‪ DOM‬الخ‪ww‬اص بالص‪ww‬فحة المعروض‪ww‬ة‪ .‬ففي‬

‫متصفح كروم مثاًل ‪ ،‬يُمكِنك النقر بزر الفأرة األيمن عىل أي مكان من الصفحة‪ ،‬واختيار "‪ "Inspect‬من القائمة؛ أما‬

‫في متصفح فايرفوكس‪ ،‬ف ُيمكِنك ً‬


‫أيضا النقر بزر الفأرة األيمن عىل أي مك‪ww‬ان واختي‪ww‬ار "‪ "Inspect Element‬من‬

‫القائمة‪ .‬يُمكِنك القراءة عن أداة ‪ Web Inspector‬التي يُو ِّفرها متصفح سفاري أو كروم ‪.Chrome‬‬

‫‪62‬‬
‫هياكل البيانات للمبرمجين‬ ‫التنقل في الشجرة ‪Tree Traversal‬‬

‫تعرض الصورة السابقة لقط‪ww‬ة شاش‪ww‬ة لنم‪ww‬وذج ‪ DOM‬الخ‪ww‬اص بمقال‪ww‬ة ويكيبي‪ww‬ديا عن لغ‪ww‬ة جافا‪ ،‬حيث يُم ِّثلُ‬

‫العنصر المظلل أول فقرة في النص الرئيسي من المقالة‪ .‬الحِ ظ أن الفقرة تقع داخل عنص‪ww‬ر <‪ >div‬ال‪ww‬ذي يمل‪ww‬ك‬

‫السمة ‪ ،"id="mw-content-text‬والتي سنَستخ ِدمها للعثور عىل النص الرئيسي في أي مقالةٍ ن ُ ِّ‬
‫حملها‪.‬‬

‫‪ 6.3‬استخدام مكتبة ‪jsoup‬‬


‫تُسهِّل مكتبة ‪ jsoup‬من تحميل صفحات اإلنترنت وتحليله‪ww‬ا‪ ،‬وك‪ww‬ذلك التن ُق‪ww‬ل ع‪ww‬بر ش‪ww‬جرة ‪ .DOM‬انظ‪ww‬ر إىل‬

‫المثال التالي‪:‬‬

‫= ‪String url‬‬
‫;")‪"http://en.wikipedia.org/wiki/Java_(programming_language‬‬

‫ّ‬
‫وحلله ‪//‬‬ ‫ِّ‬
‫حمل المستند‬
‫;)‪Connection conn = Jsoup.connect(url‬‬
‫;)(‪Document doc = conn.get‬‬

‫اختر المحتوى النصي واسترجع الفقرات ‪//‬‬


‫;)"‪Element content = doc.getElementById("mw-content-text‬‬
‫;)"‪Elements paragraphs = content.select("p‬‬

‫نش‪w‬ئ اتص‪ww‬ااًل م‪ww‬ع خ‪ww‬ادم‬


‫يَستق ِبل التابع ‪ُ Jsoup.connect‬محدِّد موارد موحدًا ‪ URL‬من النوع ‪ ،String‬ويُ ِ‬

‫حمل التابع ‪ get‬مستند ‪ HTML‬ويُحلِّله‪ ،‬ويعيد كائنًا من النوع ‪ Document‬يُمثِل شجرة ‪.DOM‬‬
‫الويب‪ .‬بعد ذلك يُ ِّ‬

‫‪63‬‬
‫هياكل البيانات للمبرمجين‬ ‫التنقل في الشجرة ‪Tree Traversal‬‬

‫ً‬
‫كثيرة جدًا لدرجة‬ ‫يُو ِّفر الصنف ‪ Document‬توابعً ا للتنقل عبر الشجرة واختيار العقد‪ .‬في الواقع‪ ،‬إنه يُو ِّفر توابع‬

‫تُصيبَك بالحيرة‪ .‬وس َيعرِض المثال التالي طريقتين الختيار العقد‪:‬‬

‫ً‬
‫نصية من النوع ‪ ،String‬ويبحث ضمن الشجرة عن عنصر ٍ‬ ‫ً‬
‫سلسلة‬ ‫‪ :getElementById‬يستق ِبل‬ ‫•‬

‫الممرَّرة‪ .‬يختار التابع في هذا المثال العقدة‬


‫يملك نفس قيمة حقل ‪ُ id‬‬
‫>"‪<div id="mw-content-text" lang="en" dir="ltr" class="mw-content-ltr‬‬

‫تضمن للنص الرئيسي‬


‫ِّ‬ ‫ي مقالةٍ من موقع ويكيبيديا لكي تُم ّيز عنصر <‪ُ >div‬‬
‫الم‬ ‫التي تَظهَ ر في أ ّ‬
‫للصفحة‪ ،‬عن شريط التنقل الجانبي والعناصر األخرى‪ .‬يعيد التابع ‪ getElementById‬كائنًا من النوع‬

‫‪ Element‬يُمثِل عنصر <‪ >div‬ذاك‪ ،‬ويحتوي عىل العناصر الموجودة داخله بهيئة أبنا ٍء وأحفا ٍد وغيرها‪.‬‬

‫سلسلة نص ّي ًة من النوع ‪ ،String‬ويتن ّقل عبر الشجرة‪ ،‬ثم يُعيد جميع العناصر التي‬
‫ً‬ ‫‪ :select‬يستقبل‬ ‫•‬

‫يتوافق الوسم ‪ tag‬الخاص بها مع تلك السلسلة النصية‪ .‬يعيد التابع في هذا المثال جميع وسوم‬

‫الفقرات الموجودة في الكائن ‪ .content‬تكون القيمة المعادة عبارة عن كائن من النوع ‪.Elements‬‬

‫فض ‪w‬ل أن تلقي نظ‪ً w‬‬


‫‪w‬رة عىل توثي‪ww‬ق ك‪ww‬لٍّ من األص‪ww‬ناف الم‪ww‬ذكورة لكي تتع‪ww‬رف عىل‬ ‫قب‪ww‬ل أن تُكمِ ‪ w‬ل الق‪ww‬راءة‪ ،‬يُ َّ‬

‫األهم‪.‬‬
‫ّ‬ ‫إمكانيات كلٍّ منها‪ .‬تجدر اإلشارة إىل أنّ األصناف ‪ Element‬و‪ Elements‬و‪ Node‬هي األصناف‬

‫ٌ‬
‫فرعية ‪ subclasses‬كث‪ww‬ير ٌة مث‪ww‬ل ‪Element‬‬ ‫ٌ‬
‫أصناف‬ ‫يُمثِل الصنف ‪ Node‬عقد ًة في شجرة ‪ .DOM‬وتمتد منه‬
‫ً‬
‫تجميع‪ww‬ة من الن‪ww‬وع ‪ Collection‬ال‪ww‬تي‬ ‫د الص‪ww‬نف ‪Elements‬‬
‫و‪ TextNode‬و‪ DataNode‬و‪ .Comment‬يُع‪ّ ww‬‬

‫كائنات من النوع ‪.Element‬‬


‫ٍ‬ ‫تحتوي عىل‬

‫تحتوي الصورة السابقة عىل مخطط ‪ UML‬يُ ّ‬


‫وضح العالقة بين تلك األصناف‪ .‬يشير الخط ذو ال‪ww‬رأس األج‪ww‬وف‬

‫صنف آخر‪ ،‬إذ يمتد الصنف ‪ Elements‬مثاًل ‪ ،‬من الصنف ‪ .ArrayList‬وس‪ww‬نعود‬


‫ٍ‬ ‫إىل أن هناك صن ًفا يمتد من‬

‫الح ًقا للحديث عن مخططات ‪.UML‬‬

‫‪64‬‬
‫هياكل البيانات للمبرمجين‬ ‫التنقل في الشجرة ‪Tree Traversal‬‬

‫‪ 6.4‬التنقل في شجرة ‪DOM‬‬


‫سمح لك الصنف ‪- WikiNodeIterable‬الذي كتبه المؤلف‪ -‬ب‪ww‬المرور ع‪ww‬بر عق‪ww‬د ش‪ww‬جرة ‪ .DOM‬انظ‪w‬ر إىل‬
‫يَ َ‬
‫المثال التالي الذي يبين طريقة استخدامه‪:‬‬

‫;)"‪Elements paragraphs = content.select("p‬‬


‫;)‪Element firstPara = paragraphs.get(0‬‬

‫;)‪Iterable<Node> iter = new WikiNodeIterable(firstPara‬‬


‫{ )‪for (Node node: iter‬‬
‫{ )‪if (node instanceof TextNode‬‬
‫;)‪System.out.print(node‬‬
‫}‬
‫}‬

‫يُكمِ ل هذا المثال ما وصلنا إليه في المثال السابق‪ ،‬فهو يختار الفق‪w‬رة األوىل في الك‪w‬ائن ‪ paragraphs‬أواًل ‪،‬‬

‫نش‪wwwwww‬ئ كائنً‪wwwwww‬ا من الن‪wwwwww‬وع ‪ WikiNodeIterable‬ل ُين ِّفذ الواجه‪wwwwww‬ة >‪ .Iterable<Node‬يُج‪wwwwww‬رِي‬


‫ثم يُ ِ‬

‫‪ WikiNodeIterable‬بح ًثا بتقنية العمق أواًل ‪ ،depth-first‬ويُولِّد العقد بنفس ترتيب ظهورها بالصفحة‪.‬‬

‫تَطبَع الشيفر ُة العق‪َ w‬د إذا ك‪w‬انت من الن‪w‬وع ‪ TextNode‬وتتجاهله‪w‬ا إذا ك‪w‬انت من أي ن‪w‬وع آخ‪w‬ر‪ ،‬وال‪w‬تي تُمثِ‪w‬ل‬

‫ي ترم‪ww‬يزات‪ .‬وق‪ww‬د ك‪ww‬ان‬


‫وسوما من الصنف ‪ Element‬في هذا المثال‪ .‬ينتج عن ذلك طباع‪ww‬ة نص الفق‪ww‬رة ب‪ww‬دون أ ّ‬
‫ً‬
‫الخرج في هذا المثال كما يلي‪:‬‬

‫‪Java is a general-purpose computer programming language that is‬‬


‫‪concurrent, class-based, object-oriented,[13] and specifically‬‬
‫… ‪designed‬‬

‫‪ 6.5‬البحث بالعمق أوال ‪Depth-first search‬‬


‫‪w‬واع مختلف‪ww‬ةٍ من التطبيق‪ww‬ات‪ .‬س‪ww‬نبدأ‬ ‫تتو َّفر العديد من الطرائق للتنقل في األشجار‪ ،‬ويتالءم ك‪ٌّ w‬‬
‫ل منه‪ww‬ا م‪ww‬ع أن‪ٍ w‬‬
‫بطريقة البحث بالعمق أواًل ‪ .DFS‬تبدأ تلك الطريقة من ج‪w‬ذر الش‪ww‬جرة‪ ،‬ثم تخت‪ww‬ار االبن األول للج‪w‬ذر‪ .‬إذا ك‪ww‬ان لدي‪ww‬ه‬

‫أبناء‪ ،‬فإنها ستختار االبن األول‪ ،‬وتستمر في ذلك حتى تصل إىل عقد ٍة ليس له‪ww‬ا أبن‪ww‬اء‪ ،‬أين تب‪ww‬دأ ب‪ww‬التراجع عن‪ww‬دها‬

‫والتحرك ألعىل إىل عقدة األب‪ ،‬لتختار منها االبن التالي إن كان موج‪ww‬ودًا‪ ،‬وفي حال‪ww‬ة ع‪ww‬دم وج‪ww‬وده‪ ،‬فإنه‪ww‬ا ت‪ww‬تراجع‬

‫للوراء مجددًا‪ .‬عندما تنتهي من البحث في االبن األخير لعقدة الجذر‪ ،‬فإنها تكون قد انتهت‪.‬‬

‫هناك طريقتان شائعتان لتنفيذ ‪ :DFS‬إما بالتعاود ‪ ،recursion‬أو بالتكرار‪ .‬يُع ّد التنفيذ بالتعاود ه‪ww‬و الطريق‪ww‬ة‬

‫األبسط‪:‬‬

‫‪65‬‬
‫هياكل البيانات للمبرمجين‬ ‫التنقل في الشجرة ‪Tree Traversal‬‬

‫{ )‪private static void recursiveDFS(Node node‬‬


‫{ )‪if (node instanceof TextNode‬‬
‫;)‪System.out.print(node‬‬
‫}‬
‫{ ))(‪for (Node child: node.childNodes‬‬
‫;)‪recursiveDFS(child‬‬
‫}‬
‫}‬

‫الممرَّرة من الن‪ww‬وع‬ ‫ً‬


‫بداية من الجذر‪ .‬إذا كانت العقدة ُ‬ ‫يُستدعَ ى التابع السابق من أجل كل عقد ٍة ضمن الشجرة‬

‫‪ ،TextNode‬ويطبع التابع محتوياتِها‪ ،‬ثم يفحص إذا كان للعقدة أي أبن‪w‬اء‪ .‬ف‪w‬إذا ك‪w‬ان له‪w‬ا أبن‪w‬اء‪ ،‬فإن‪w‬ه س َيس‪w‬تدعِ ي‬

‫‪- recursiveDFS‬أي ذاته‪ -‬لجميع عقد األبناء عىل التوالي‪.‬‬

‫في هذا المثال‪َ ،‬طبَعَ نا محتويات العقد التي تنتمي إىل النوع ‪ TextNode‬قبل أن ننتقل إىل األبناء‪ ،‬وهو م‪ww‬ا‬

‫يُع‪ّ w‬د مث‪ww‬ااًل عىل التنق‪ww‬ل ذي ال‪ww‬ترتيب الس‪ww‬ابق‪ .‬يُمكِن‪ww‬ك الق‪ww‬راءة عن التنقالت ذات ال‪ww‬ترتيب الس‪ww‬ابق ‪pre-order‬‬

‫والترتيب الالحق ‪ post-order‬وفي الترتيب ‪ .in-order‬ال يُشكّل ترتيب التنقل في تطبيقنا هذا أي فارق‪.‬‬

‫نظ ًرا ألن التابع ‪ recursiveDFS‬يَستدعِ ي ذاته تعاوديًا‪ ،‬فقد كان بإمكان‪ww‬ه اس‪ww‬تخدام ُمك ‪w‬دِّس االس‪ww‬تدعاءات‬

‫لالحتفاظ بالعقد األبناء‪ ،‬ومعالجتها بالترتيب المناسب‪ ،‬لكننا بداًل من ذل‪ww‬ك يُمكِنن‪ww‬ا أن نَس‪ww‬تخدِم ُمكد ً‬
‫ِّس ‪w‬ا ص‪ww‬ريحًا‬

‫لالحتفاظ بالعقد‪ ،‬وفي تلك الحالة لن نحتاج إىل التعاود‪ ،‬حيث سنتمكَّن من التنقل في الشجرة عبر حلقة تكراريّة‪.‬‬

‫‪ 6.6‬المكدسات ‪ Stacks‬في جافا‬


‫المك‪w‬دّس‪ .‬س‪ww‬نبدأ‬ ‫قبل أن نشرح التنفيذ التك‪ww‬راري لتقني‪ww‬ة ‪ ،DFS‬س‪ww‬نناقش أواًل هيك‪ww‬ل بيان‪ٍ w‬‬
‫‪w‬ات يُع‪w‬رَف باس‪ww‬م ُ‬

‫للمكدِّس‪ ،‬ثم سنتحدث عن واجهتين ‪ interfaces‬بلغة جافا تُعرِّفان توابع ُ‬


‫المكدِّس‪ ،‬وهم‪ww‬ا ‪Stack‬‬ ‫بالفكرة العامة ُ‬
‫و‪.Deque‬‬

‫بيانات مشابهً ا للقائمة‪ ،‬فهو عبارة عن تجميعة تتذكر ترتيب العناصر‪ .‬ويتمثل الفرق بين‬
‫ٍ‬ ‫المكدِّس هيكل‬
‫يُع ّد ُ‬
‫المكدِّس التوابع التالية‪:‬‬ ‫ً‬
‫عادة ما يُو ِّفر ُ‬ ‫المكدِّس يو ِّفر توابعَ أقل‪ ،‬وأنه‬
‫المكدّس والقائمة في أن ُ‬

‫المكدِّس‪.‬‬
‫‪ :push‬يضيف عنصرًا إىل أعىل ُ‬ ‫•‬

‫المكدِّس ويعيده‪.‬‬
‫‪ :pop‬يح ِذف العنصر الموجود أعىل ُ‬ ‫•‬

‫المكدِّس دون حذفه‪.‬‬


‫‪ :peek‬يعيد العنصر الموجود أعىل ُ‬ ‫•‬

‫المكدِّس فار ًغا‪.‬‬


‫‪ :isEmpty‬يشير إىل ما إذا كان ُ‬ ‫•‬

‫‪66‬‬
‫هياكل البيانات للمبرمجين‬ ‫التنقل في الشجرة ‪Tree Traversal‬‬

‫ّسات باستخدام كلمة‬


‫ِ‬ ‫دائما‪ ،‬يُشار إىل المكد‬
‫ً‬ ‫ّس‬
‫نظرًا ألن التابع ‪ pop‬يسترجع العنصر الموجود في أعىل المكد ِ‬

‫"‪ ،"LIFO‬والتي تُع ّد اختصا ًرا لعبارة "الداخل آخ ًرا‪ ،‬يخرج أواًل "‪ .‬في المقابل‪ ،‬تُع ّد األرتال ‪ queue‬بدياًل للمكد‬
‫ّسات‪،‬‬
‫ِ‬

‫‪w‬ادة باس‪ww‬تخدام كلم‪ww‬ة "‪ "FIFO‬أي "ال‪ww‬داخل أواًل ‪،‬‬


‫ولكنها تُعيد العناصر بنفس ترتيب إضافتها‪ ،‬ولذلك‪ ،‬يُشار إليها ع‪ً w‬‬

‫يخرج أواًل "‪.‬‬

‫ً‬
‫‪w‬افية عن تل‪ww‬ك‬
‫‪w‬ات إض‪w‬‬ ‫ً‬
‫واضحة بالنسبة لك‪ ،‬فهم‪ww‬ا ال ي‪ww‬وفران أي إمكاني‪ٍ w‬‬ ‫المكدِّسات واألرتال‬
‫قد ال تكون أهمية ُ‬
‫الموجودة في القوائم ‪ .lists‬بل يوفران إمكانيات أقل‪ ،‬لذلك قد تتساءل لم ال نكتفي باستخدام الق‪ww‬وائم؟ واإلجاب‪ww‬ة‬

‫هي أن هناك سببان‪:‬‬

‫‪ .1‬إذا ألزمت نفسك بعدد أقل من التوابع‪ ،‬أي بواجهة تطوير تطبيقات ‪ API‬أصغر‪ ،‬فعاد ًة ما تصبح الش‪ww‬يفرة‬
‫مقروء ًة أكثر‪ ،‬كما تقل احتمالية احتوائها عىل أخطاء‪ .‬عىل س‪ww‬بيل المث‪ww‬ال‪ ،‬إذا اس‪ww‬تخدمت قائم‪ً w‬‬
‫‪w‬ة لتمثي‪ww‬ل‬

‫ُمكدِّس‪ ،‬فقد تَحذِف ‪-‬عن طريق الخطأ‪ -‬عنصرًا بترتيب خاطئ‪ .‬في المقابل‪ ،‬إذا استخدمت واجهة تط‪ww‬وير‬

‫للمكدِّس‪ ،‬فسيستحيل أن تق‪ww‬ع في مث‪ww‬ل ه‪w‬ذا الخط‪ww‬أ‪ ،‬وله‪w‬ذا فالطريق‪ww‬ة األفض‪ww‬ل‬


‫خصصة ُ‬
‫الم َّ‬
‫التطبيقات ُ‬
‫لتجنُّب األخطاء هي بأن تجعلها مستحيلة‪.‬‬

‫ً‬
‫صغيرة‪ ،‬فسيكون تنفيذها بكف‪ww‬اء ٍة أس‪ww‬هل‪.‬‬ ‫‪ .2‬إذا كانت واجهة تطوير التطبيقات التي يُو ِّفرها هيكل البيانات‬

‫المك‪w‬دِّس‬ ‫‪w‬ة مترابط‪ً w‬‬


‫‪w‬ة ‪ linked list‬أحادي‪ww‬ة التراب‪ww‬ط لتنفي‪ww‬ذ ُ‬ ‫عىل سبيل المثال‪ ،‬يُمكِنن‪ww‬ا أن نَس‪ww‬تخدِم قائم‪ً w‬‬

‫المك‪w‬دِّس‪ ،‬فعلين‪ww‬ا أن نض‪ww‬يفه إىل بداي‪ww‬ة القائم‪ww‬ة؛ أم‪ww‬ا عن‪ww‬دما نس‪ww‬حب‬


‫بسهولة‪ ،‬وعندما نضع عنصرًا في ُ‬
‫عنص ًرا منها‪ ،‬فعلينا أن نَحذفه من بدايتها‪ .‬ونظ ًرا ألن عمليتي إضافة العناص‪ww‬ر وح‪w‬ذفها من بداي‪ww‬ة الق‪w‬وائم‬

‫المترابطة تستغرق زمنًا ثابتًا‪ ،‬فإننا نكون قد حصلنا عىل تنفي ‪ٍ w‬ذ ذي كف‪ww‬اء ٍة عالي‪ww‬ة‪ .‬في المقاب‪ww‬ل‪ ،‬يَص ‪ُ w‬عب‬

‫تنفيذ واجهات التطوير الكبيرة بكفاءة‪.‬‬

‫ِّس بلغة جافا‪:‬‬


‫لديك ثالثة خيارات لتنفيذ ُمكد ٍ‬

‫‪ .1‬اِستخدِم الصنف ‪ ArrayList‬أو الصنف ‪ .LinkedList‬إذا اِس‪ww‬تخدَمت الص‪ww‬نف ‪ ،ArrayList‬تأ َّكد‬

‫من إجراء عمليتي اإلضافة والحذف من نهاية القائمة ألنهم‪ww‬ا ب‪ww‬ذلك سيس‪ww‬تغرِقان زمنً‪ww‬ا ثاب ًت‪ww‬ا‪ ،‬وانتب‪ww‬ه من‬

‫بترتيب خاطئ‪.‬‬
‫ٍ‬ ‫مكان خاطٍئ أو تحذفها‬
‫ٍ‬ ‫إضافة العناصر في‬

‫قديما من لغة‬
‫ً‬ ‫‪ .2‬تُو ِّفر جافا الصنف ‪ Stack‬الذي يحتوي عىل التوابع األساسية ُ‬
‫للمكدِّسات‪ ،‬ولكنه يُع ّد جزءًا‬
‫جافا‪ ،‬فهو غير متوافق مع إطار عمل جافا للتجميعات ‪ Java Collections Framework‬الذي ُأض‪َ w‬‬
‫‪w‬يف‬

‫الح ًقا‪.‬‬

‫‪ .3‬ربما الخيار األفضل هو استخدام إحدى تنفيذات الواجهة ‪ Deque‬مثل الصنف ‪.ArrayDeque‬‬

‫إن كلمة ‪ Deque‬هي اختصار للتسمية رتل ذو نهايتين ‪ ،double-ended queue‬والتي يُف‪ww‬ت َرض أن تُل َف‪w‬ظ‬

‫‪ ،deck‬ولكنها تُل َفظ أحيانًا ‪ .deek‬تُو ِّفر واجهة ‪ Deque‬بلغة جافا التواب‪ww‬ع ‪ push‬و‪ pop‬و‪ peek‬و‪ ،isEmpty‬ل‪ww‬ذلك‬

‫كمكدِّس‪ ،‬كما أنها تُو ِّفر توابع أخرى ولكننا لن نَستخدِمها حال ًيا‪.‬‬
‫يُمكِنك أن تَستخدِم كائنًا من النوع ‪ُ Deque‬‬

‫‪67‬‬
‫هياكل البيانات للمبرمجين‬ ‫التنقل في الشجرة ‪Tree Traversal‬‬

‫‪ 6.7‬التنفيذ التكراري لتقنية البحث بالعمق أوال‬


‫انظ‪ww‬ر إىل التنفي‪ww‬ذ التك‪ww‬راري ألس‪ww‬لوب "البحث ب‪ww‬العمق أواًل "‪ .‬يَس‪ww‬تخدِم ذل‪ww‬ك التنفي‪ww‬ذ كائنً‪ww‬ا من الن‪ww‬وع‬

‫ِّسا يحتوي عىل كائنات تنتمي إىل النوع ‪:Node‬‬


‫‪ ArrayDeque‬ل ُيمثِل ُمكد ً‬

‫{ )‪private static void iterativeDFS(Node root‬‬


‫;)(>‪Deque<Node> stack = new ArrayDeque<Node‬‬
‫;)‪stack.push(root‬‬

‫{ ))(‪while (!stack.isEmpty‬‬
‫;)(‪Node node = stack.pop‬‬
‫{ )‪if (node instanceof TextNode‬‬
‫;)‪System.out.print(node‬‬
‫}‬

‫;))(‪List<Node> nodes = new ArrayList<Node>(node.childNodes‬‬


‫;)‪Collections.reverse(nodes‬‬

‫{ )‪for (Node child: nodes‬‬


‫;)‪stack.push(child‬‬
‫}‬
‫}‬
‫}‬

‫المكدِّس ونضيف الجذر إليه‪.‬‬


‫ُنشئ ُ‬
‫يُمثِل المعامل ‪ root‬جذر الشجرة التي نريد أن نجتازها‪ ،‬حيث سن ِ‬

‫المك‪w‬دِّس فار ًغ‪ w‬ا‪ .‬يَس‪w‬حَب ك‪ww‬ل تك‪ww‬رار ض‪ww‬من الحلق‪ww‬ة عق‪ً w‬‬
‫‪w‬دة من‬ ‫تستمر الحلق‪ww‬ة ‪ loop‬بالعم‪ww‬ل إىل أن يُص‪ِ w‬بح ُ‬

‫المكدِّس‪ ،‬فإذا كانت العقدة من النوع ‪ ،TextNode‬فإنه يَطبَعُ محتوياتِها ثم يضيف أبناءها إىل ُ‬
‫المك‪ww‬دِّس‪ .‬ينبغي‬ ‫ُ‬
‫معاكس لكي نتمكَّن من معالجته‪ww‬ا ب‪ww‬الترتيب الص‪ww‬حيح‪ ،‬ول‪ww‬ذلك سنَنَس‪ww‬خ‬
‫ٍ‬ ‫بترتيب‬
‫ٍ‬ ‫المكدِّس‬
‫أن نضيف األبناء إىل ُ‬
‫األبناء أواًل إىل قائمة من النوع ‪ ،ArrayList‬ثم نعكس ترتيب العناص‪ww‬ر فيه‪ww‬ا‪ ،‬وفي النهاي‪ww‬ة س‪ww‬نمرّ ع‪ww‬بر القائم‪ww‬ة‬

‫المعكوسة‪.‬‬

‫من السهل كتابة التنفيذ التكراري لتقنيةِ البحث بالعمق أواًل باستخدام كائن من النوع ‪ ،Iterator‬وس‪ww‬ترى‬

‫ذلك في الفصل التالي‪.‬‬

‫في مالحظة أخيرة عن الواجهة ‪ ،Deque‬باإلضافة إىل الصنف ‪ ،ArrayDeque‬تُو ِّفر جاف‪ww‬ا تنفي ‪ً w‬ذا آخ ‪w‬رًا لتل‪ww‬ك‬

‫الواجهة‪ ،‬هو الصنف ‪ LinkedList‬الذي يُن ِّفذ الواجهتين ‪ List‬و‪ ،Deque‬وتعتمد الواجه‪w‬ة ال‪ww‬تي تحص‪w‬ل عليه‪ww‬ا‬

‫‪68‬‬
‫هياكل البيانات للمبرمجين‬ ‫التنقل في الشجرة ‪Tree Traversal‬‬

‫عىل الطريقة التي تَستخدِمه بها‪ .‬عىل سبيل المث‪w‬ال‪ ،‬إذا أس‪w‬ندت كائنً‪w‬ا من الن‪w‬وع ‪ LinkedList‬إىل متغ‪w‬ير ٍ من‬

‫النوع ‪ Deque‬كالتالي‪:‬‬

‫;)(>‪Deqeue<Node> deque = new LinkedList<Node‬‬

‫المعرَّفة بالواجهة ‪ ،Deque‬ال تواب‪ww‬ع الواجه‪ww‬ةِ ‪ .List‬وفي المقاب‪ww‬ل‪ ،‬إذا‬


‫فسيكون في إمكانك استخدام التوابع ُ‬
‫أسندته إىل متغير ٍ من النوع ‪ ،List‬كالتالي‪:‬‬

‫;)(>‪List<Node> deque = new LinkedList<Node‬‬

‫المعرَّفة بالواجهة ‪ List‬ال توابع الواجهة ‪Deque‬؛ أم‪ww‬ا إذا أس‪ww‬ندته عىل‬
‫فسيكون في إمكانك استخدام التوابع ُ‬
‫النحو التالي‪:‬‬

‫;)(>‪LinkedList<Node> deque = new LinkedList<Node‬‬

‫‪w‬ات مختلف‪ww‬ةٍ ‪ ،‬ه‪ww‬و أن‬


‫فسيكون بإمكانك استخدام جميع التوابع‪ ،‬ولكن الذي يحدث عن‪ww‬د دمج تواب‪ww‬ع من واجه‪ٍ w‬‬
‫ً‬
‫عرضة الحتواء األخطاء‪.‬‬ ‫الشيفرة ستصبح أصعب قراء ًة وأكثر‬

‫‪69‬‬
‫‪ .7‬كل الطرق تؤدي إىل روما‬

‫َ‬
‫زاحف إنترنت ‪ crawler‬يختبر صحة فرض ّية "الطريق إىل مقالة الفلس‪ww‬فة" ‪Getting‬‬ ‫سنبني في هذا الفصل‬

‫‪- to Philosophy‬التي تشبه المثل الشهير كل الطرق تؤدي إىل روما‪ -‬في موقع ويكيبيديا التي شرحنا معناه‪ww‬ا‬

‫في الفصل السابق‪.‬‬

‫‪ 7.1‬البداية‬
‫ستجد في مستودع الكتاب ملفات الشيفرة التالية التي ستساعدك عىل بدء العمل‪:‬‬

‫‪ :WikiNodeExample.java .1‬يحتوي عىل شيفرة التنفي‪ww‬ذ التع‪ww‬اودي ‪ recursive‬والتك‪ww‬راري ‪iterative‬‬

‫لتقنية البحث بالعمق أواًل ‪.depth-first search‬‬

‫‪ :WikiNodeIterable.java .2‬يحتوي عىل ص‪w‬نف ممت‪ٍّ w‬د من الن‪w‬وع ‪ Iterable‬بإمكان‪ww‬ه الم‪w‬رور ع‪ww‬بر‬

‫شجرة ‪.DOM‬‬

‫ً‬
‫أداة تَستخدِم مكتبة ‪ jsoup‬لتحمي‪ww‬ل الص‪ww‬فحات من‬ ‫صنف يُعرِّف‬
‫ٍ‬ ‫‪ :WikiFetcher.java .3‬يحتوي عىل‬

‫موقع ويكيبيديا‪ .‬ويضع الصنف حدًّا لسرعة تحميل الصفحات امتثااًل لشروط الخدم‪ww‬ة في الموق‪ww‬ع‪ ،‬ف‪ww‬إذا‬

‫طلبت أكثر من صفحة في الثانية الواحدة‪ ،‬فإنه ينتظر قلياًل قبل أن يُ ِّ‬
‫حمل الصفحة التالية‪.‬‬

‫‪w‬دئي عن الش‪ww‬يفرة ال‪ww‬تي ينبغي أن تكمله‪ww‬ا في ه‪ww‬ذا‬


‫ٍّ‬ ‫‪ :WikiPhilosophy.java .4‬يحتوي عىل تص ‪ّ w‬ورٍ مب‪w‬‬

‫التمرين‪ ،‬وسنناقشها في األسفل‪.‬‬

‫َعمل الشيفرة المبدئية إذا ن َّفذت األمر التالي‪:‬‬ ‫ستجد ً‬


‫أيضا ملف البناء ‪ ،build.xml‬حيث ست َ‬

‫‪ant WikiPhilosophy‬‬
‫هياكل البيانات للمبرمجين‬ ‫كل الطرق تؤدي إىل روما‬

‫‪ 7.2‬الواجهتان ‪ Iterables‬و‪Iterators‬‬
‫تناولنا في الفصل السابق تنفي ًذا تكراريًا ل‪ww‬ه‪ ،‬وذكرن‪ww‬ا وج‪ww‬ه تفض‪ww‬يله عىل التنفي‪ww‬ذ التع‪ww‬اودي من جه‪ww‬ة س‪ww‬هولة‬

‫كائن من النوع ‪ .Iterator‬سنناقش في هذا الفصل طريقة القيام بذلك‪.‬‬


‫ٍ‬ ‫تضمينه في‬

‫يُمكِنك القراءة عن الواجهتين ‪ Iterator‬و‪ Iterable‬إذا لم تكن عىل معرفة بهما‪.‬‬

‫الخ‪wwwww‬ارجي‬
‫ُّ‬ ‫أل‪wwwww‬ق نظ‪wwwww‬ر ًة عىل محتوي‪wwwww‬ات المل‪wwwww‬ف ‪ .WikiNodeIterable.java‬يُن ِّفذ الص‪wwwww‬نف‬
‫ِ‬
‫‪ WikiNodeIterable‬الواجهة >‪ ،Iterable<Node‬ولذا يُمكِننا أن نَس‪ww‬تخدِمه ض‪ww‬من حلق‪ww‬ة تك‪ww‬رار ‪ loop‬عىل‬

‫النحو التالي‪:‬‬

‫‪Node root = ...‬‬


‫;)‪Iterable<Node> iter = new WikiNodeIterable(root‬‬
‫{ )‪for (Node node: iter‬‬
‫;)‪visit(node‬‬
‫}‬

‫يشير ‪ root‬إىل جذر الشجرة التي ننوي اجتيازها أو التنقل فيها‪ ،‬بينما يُمثِل ‪ visit‬التابع ال‪ww‬ذي ن‪ww‬رغب في‬

‫تطبيقه عند مرورنا بعقد ٍة ما‪.‬‬

‫يَت ِبع التنفيذ ‪ WikiNodeIterable‬المعادلة التقليدية‪:‬‬

‫‪ .1‬يَستق ِبل الباني ‪ constructor‬مرجعً ا إىل عقدة الجذر‪.‬‬

‫نشئ التابع ‪ iterator‬كائنًا من النوع ‪ Iterator‬ويعيده‪.‬‬


‫‪ .2‬يُ ِ‬

‫انظر إىل شيفرة الصنف‪:‬‬

‫{ >‪public class WikiNodeIterable implements Iterable<Node‬‬

‫;‪private Node root‬‬

‫{ )‪public WikiNodeIterable(Node root‬‬


‫;‪this.root = root‬‬
‫}‬

‫‪@Override‬‬
‫{ )(‪public Iterator<Node> iterator‬‬
‫;)‪return new WikiNodeIterator(root‬‬

‫‪71‬‬
‫هياكل البيانات للمبرمجين‬ ‫كل الطرق تؤدي إىل روما‬

}
}

:‫ العمل الفعلي‬WikiNodeIterator ‫نجز الصنف الداخلي‬


ِ ُ‫ ي‬،‫في المقابل‬

private class WikiNodeIterator implements Iterator<Node> {

Deque<Node> stack;

public WikiNodeIterator(Node node) {


stack = new ArrayDeque<Node>();
stack.push(root);
}

@Override
public boolean hasNext() {
return !stack.isEmpty();
}

@Override
public Node next() {
if (stack.isEmpty()) {
throw new NoSuchElementException();
}

Node node = stack.pop();


List<Node> nodes = new ArrayList<Node>(node.childNodes());
Collections.reverse(nodes);
for (Node child: nodes) {
stack.push(child);
}
return node;
}
}

َّ ‫ا ُم‬ww‫ ولكنّه‬،‫ير‬ww‫د كب‬ww‫تتطابق الشيفرة السابقة مع التنفيذ التكراري ألسلوب "البحث بالعمق أواًل " إىل ح‬
‫مة‬w ‫قس‬

:‫اآلن عىل ثالثة توابع‬

72
‫هياكل البيانات للمبرمجين‬ ‫كل الطرق تؤدي إىل روما‬

‫المن َّفذ باس‪ww‬تخدام ك‪ww‬ائن من الن‪ww‬وع ‪ ،)ArrayDeque‬ويُض‪ww‬يف إلي‪ww‬ه عق‪ww‬دة‬


‫‪ .1‬يُهيئ الباني المكدس ‪ُ ( stack‬‬
‫الجذر‪.‬‬

‫ً‬
‫فارغا‪.‬‬ ‫‪ :isEmpty .2‬يفحص ما إذا كان المكدس‬

‫‪w‬اكس إىل المك ‪w‬دّس‪ ،‬ثم يعي‪ww‬د‬


‫‪w‬ترتيب مع‪ٍ w‬‬
‫ٍ‬ ‫‪ :next .3‬يَسحَب العقدة التالية من المكدّس‪ ،‬ويضيف أبناءه‪ww‬ا ب‪w‬‬

‫العقدة التي سحبها‪ .‬وفي حال استدعاء التابع ‪ next‬في كائن ‪ Iterator‬فار ٍغ‪ ،‬فإنه يُبلِّغ عن اع‪ww‬تراض‬

‫‪.exception‬‬

‫ً‬
‫فكرة غير جديرة باالهتمام‪.‬‬ ‫ربما تعتقد أن إعادة كتابة تابع جيد فعل ًيا باستخدام صنفين‪ ،‬وأن خمسة توابع تُعَ د‬

‫مكان يُمكِننا في‪ww‬ه‬


‫ٍ‬ ‫ولكننا وقد فعلنا ذلك اآلن‪ ،‬أصبح بإمكاننا أن نَستخدِم الصنف ‪ WikiNodeIterable‬في أي‬

‫استخدام النوع ‪ .Iterable‬يُسهِّل ذل‪ww‬ك من الفص‪ww‬ل بين منط‪ww‬ق التنفي‪ww‬ذ التك‪ww‬راري (البحث ب‪ww‬العمق أواًل ) وبين‬

‫المعالجة التي نريد إجراءها عىل العقد‪.‬‬

‫‪ 7.3‬الصنف ‪WikiFetcher‬‬
‫ً‬
‫كثيرة بس‪w‬رعةٍ فائق‪w‬ةٍ ‪ ،‬مم‪w‬ا ق‪w‬د ي‪w‬ؤدي إىل انته‪w‬اك ش‪w‬روط الخدم‪w‬ة‬ ‫صفحات‬
‫ٍ‬ ‫حمل‬
‫يستطيع زاحف الويب أن يُ ِّ‬
‫حمل منه تلك الصفحات‪ .‬ولكي نتجنَّب ذلك‪ ،‬و َّفرنا الصنف ‪ WikiFetcher‬الذي يقوم بما يلي‪:‬‬
‫للخادم الذي يُ ِّ‬

‫‪ .1‬يُغلِّف الشيفرة التي تناولناها في الفصل السابق‪ ،‬أي تلك التي تُ ِّ‬
‫حمل الص‪ww‬فحات من موق‪ww‬ع ويكيبي‪ww‬ديا‪،‬‬

‫وتُحلِّل ‪ ،HTML‬وتختار المحتوى النصي‪.‬‬

‫نقضي بين طلبات االتصال‪ ،‬فإذا لم يَكن كاف ًيا‪ ،‬فإنه ينتظر حتى تمرّ فتر ٌة معقول‪ww‬ة‪ .‬وق‪ww‬د‬
‫الم ِ‬
‫‪ .2‬يقيس الزمن ُ‬
‫افتراضي‪.‬‬ ‫بشكل‬ ‫ً‬
‫ثانية واحد ًة‬ ‫ضبطنا تلك الفترة لتكون‬
‫ّ‬ ‫ٍ‬

‫انظر فيما يلي إىل تعريف الصنف ‪:WikiFetcher‬‬

‫{ ‪public class WikiFetcher‬‬


‫;‪private long lastRequestTime = -1‬‬
‫;‪private long minInterval = 1000‬‬

‫**‪/‬‬
‫ِّ‬
‫حمل صفحة محدد موارد موحد وحللها *‬

‫أعد قائمة تحتوي على عناصر ُت ِ‬


‫مثل الفقرات *‬
‫*‬
‫‪* @param url‬‬
‫‪* @return‬‬
‫‪* @throws IOException‬‬

‫‪73‬‬
‫هياكل البيانات للمبرمجين‬ ‫كل الطرق تؤدي إىل روما‬

*/
public Elements fetchWikipedia(String url) throws IOException {
sleepIfNeeded();

Connection conn = Jsoup.connect(url);


Document doc = conn.get();
Element content = doc.getElementById("mw-content-text");
Elements paragraphs = content.select("p");
return paragraphs;
}

private void sleepIfNeeded() {


if (lastRequestTime != -1) {
long currentTime = System.currentTimeMillis();
long nextRequestTime = lastRequestTime + minInterval;
if (currentTime < nextRequestTime) {
try {
Thread.sleep(nextRequestTime - currentTime);
} catch (InterruptedException e) {
System.err.println(
"Warning: sleep interrupted in
fetchWikipedia.");
}
}
}
lastRequestTime = System.currentTimeMillis();
}
}

‫ يَستق ِبل‬.‫ ضمن ذلك الصنف‬public ‫المعدِّل‬


ُ ‫المعرَّف باستخدام‬
ُ ‫ هو التابع الوحيد‬fetchWikipedia ‫يُع ّد‬
ً w‫د تجميع‬ww‫ ويعي‬،URL ‫دًا‬wّ‫وارد موح‬ww‫دّد م‬w‫ وتُمثِل ُمح‬String ‫سلسلة نص ّي ًة من النوع‬
‫تي‬ww‫وع ال‬ww‫ة من الن‬w ً ‫هذا التابع‬
ً
‫ة‬ww‫مألوف‬ ‫يفرة‬ww‫ يُفترَض أن تكون تلك الش‬.‫النصي‬ ‫ لكل فقر ٍة ضمن المحتوى‬DOM ‫ تحتوي عىل عنصر‬Elements
ّ
.‫بالنسبة لك‬

‫ر‬ww‫ وينتظ‬،‫طلب‬
ٍ ‫المنقضي منذ آخر‬
َ َ‫ الذي يفحص الزمن‬sleepIfNeeded ‫تقع الشيفرة الجديدة ضمن التابع‬

.‫ والمقدّرة بوحدة الميلي ثانية‬minInterval ‫إذا كان الزمن أقلّ من القيمة الدنيا‬

ِّ ُ‫ وت‬.WikiFetcher ‫هذا هو كل ما يفعله الصنف‬


:‫وضح الشيفرة التالية طريقة استخدامه‬

74
‫هياكل البيانات للمبرمجين‬ ‫كل الطرق تؤدي إىل روما‬

‫;)(‪WikiFetcher wf = new WikiFetcher‬‬

‫{ )‪for (String url: urlList‬‬


‫;)‪Elements paragraphs = wf.fetchWikipedia(url‬‬
‫;)‪processParagraphs(paragraphs‬‬
‫}‬

‫افترضنا في هذا المثال أن ‪ urlList‬عب‪ww‬ارة عن تجميع‪ww‬ة تحت‪ww‬وي عىل سالس‪ww‬لَ نص ‪ّ w‬ية من الن‪ww‬وع ‪String‬‬

‫وأن الت‪wwww‬ابع ‪ processParagraphs‬يُع‪wwww‬الِج بطريق‪wwww‬ةٍ م‪wwww‬ا ك‪wwww‬ائن الص‪wwww‬نف ‪ Elements‬ال‪wwww‬ذي أع‪wwww‬اده‬

‫التابع ‪.fetchWikipedia‬‬

‫مهم‪ w‬ا‪ ،‬حيث ينبغي أن تُ ِ‬


‫نش‪w‬ئ كائنً‪ww‬ا واح‪w‬دًا فق‪ww‬ط من الن‪ww‬وع ‪ WikiFetcher‬وأن‬ ‫ً‬ ‫وضح ه‪ww‬ذا المث‪ww‬ال ش‪ww‬يًئا‬
‫يُ ِّ‬

‫تَستخ ِدمه لمعالجة جميع الطلبات؛ فلو كانت لديك عدة نس‪ww‬خ ‪ instances‬من الص‪ww‬نف ‪ ،WikiFetcher‬فإنه‪ww‬ا‬

‫لن تتمكَّن من فرض الزمن األدنى الالزم بين كل طلب والطلب الذي يليه‪.‬‬

‫نسخ منه‪ .‬يُمكِنك‬


‫ٍ‬ ‫بسيط للغاية‪ ،‬ولكن من السهل إساءة استخدامه بإنشاء عدة‬
‫ٌ‬ ‫تنفيذنا للصنف ‪WikiFetcher‬‬

‫أن تتجنب تلك المشكلة بجعل الصنف ‪ WikiFetcher‬يتبع نمط التصميم المفردة ‪.singleton‬‬

‫‪ 7.4‬تمرين ‪5‬‬
‫بسيطا يُ ِّ‬
‫وض ‪w‬ح طريق‪ww‬ة اس‪ww‬تخدام أج‪ww‬زا ٍء من تل‪ww‬ك‬ ‫ً‬ ‫ستجد في الملف ‪ WikiPhilosophy.java‬تابع ‪main‬‬

‫زاحف يقوم بما يلي‪:‬‬


‫ٍ‬ ‫ُ‬
‫كتابة‬ ‫الشيفرة‪ .‬وبدءًا منه‪ ،‬ستكون وظيفتك هي‬

‫حملها ويُحلِّلها‪.‬‬
‫‪ .1‬يَستق ِبل ُمحدّد موارد موحّدًا ‪ URL‬لصفحةٍ من موقع ويكيبيديا‪ ،‬ويُ ِّ‬

‫صالح‪ .‬وسنشرح المقصود بكلمة "صالح" في األسفل‪.‬‬


‫ٍ‬ ‫رابط‬
‫ٍ‬ ‫‪ .2‬يجتاز شجرة ‪ DOM‬الناتجة ويعثر عىل أول‬

‫‪w‬ط من قب‪ww‬ل‪ ،‬فعندئ‪ٍ w‬ذ ينبغي أن ينتهي البرن‪ww‬امج‬


‫روابط أو كنا قد زرن‪ww‬ا أ ّول راب‪ٍ w‬‬
‫َ‬ ‫تحتو الصفحة عىل أية‬
‫ِ‬ ‫‪ .3‬إذا لم‬

‫مشيرًا إىل فشله‪.‬‬

‫‪ .4‬إذا كان ُم حدّد الموارد الموحد يشير إىل مقالة ويكيبيديا عن الفلسفة‪ ،‬فينبغي أن ينتهي البرن‪ww‬امج مش‪ww‬ي ًرا‬

‫إىل نجاحه‪.‬‬

‫‪ .5‬وفيما عدا ذلك‪ ،‬يعود إىل الخطوة رقم ‪.1‬‬

‫ً‬
‫قائمة من النوع ‪ List‬تحتوي عىل جمي‪w‬ع ُمح‪w‬دّدات الم‪ww‬وارد ال‪w‬تي زاره‪w‬ا‪ ،‬ويَع‪ww‬رِض‬ ‫نشئ البرنامج‬
‫ينبغي أن يُ ِ‬

‫النتائج عند انتهائه‪ ،‬سوا ٌء أكانت النتيجة الفشل أم النجاح‪.‬‬

‫‪75‬‬
‫هياكل البيانات للمبرمجين‬ ‫كل الطرق تؤدي إىل روما‬

‫واآلن‪ ،‬ما الذي نعنيه برابط "صالح"؟ في الحقيقة ل‪ww‬دينا بعض الخي‪ww‬ارات‪ ،‬إذ تَس‪ww‬تخدِم النس‪ww‬خ المختلف‪ww‬ة من‬

‫ً‬
‫بعضا منها هنا‪:‬‬ ‫ً‬
‫مختلفة نَستعرِض‬ ‫نظرية "الوصول إىل مقالة ويكيبيديا عن الفلسفة" قواع َد‬

‫‪ .1‬ينبغي أن يك‪w‬ون الراب‪w‬ط ض‪w‬من المحت‪w‬وى النص‪w‬ي للص‪w‬فحة وليس في ش‪w‬ريط التنق‪w‬ل الج‪w‬انبي أو خ‪w‬ارجَ‬

‫الصندوق‪.‬‬

‫مائل أو بين أقواس‪.‬‬


‫ٍ‬ ‫بخط‬
‫ٍّ‬ ‫الرابط مكتوبًا‬
‫ُ‬ ‫‪ .2‬ال ينبغي أن يكون‬

‫‪ .3‬ينبغي أن تتجاهل الروابط الخارج ّية والروابط التي تشير إىل الصفحة الحالية والروابط الحمراء‪.‬‬

‫بحرف كبير‪.‬‬
‫ٍ‬ ‫‪ .4‬ينبغي أن تتجاهل الرابط إذا كان بادًئا‬

‫ليس من الضروري أن تتقيد بكل تلك القواعد‪ ،‬ولكن يُمكِنك عىل األقل معالجة األق‪ww‬واس والخط‪ww‬وط المائل‪ww‬ة‬

‫والروابط التي تشير إىل الصفحة الحالية‪.‬‬

‫إذا كنت تظن أن لديك المعلوم‪ww‬ات الكافي‪ww‬ة لتب‪ww‬دأ‪ ،‬فاب‪w‬دأ اآلن‪ ،‬ولكن ال ب‪ww‬أس قب‪ww‬ل ذل‪ww‬ك بق‪ww‬راءة التلميح‪ww‬ات‬

‫التالية‪:‬‬

‫‪ .1‬ستحتاج إىل معالجة نوعين من العقد بينما تجتاز الش‪ww‬جرة‪ ،‬هم‪ww‬ا الص‪ww‬نفان ‪ TextNode‬و‪ .Element‬إذا‬

‫تضطر إىل تحويل نوعه ‪ typecast‬لكي تتمكَّن من استرجاع‬


‫ّ‬ ‫قابلت كائنًا من النوع ‪ ،Element‬فلربما قد‬

‫الوسم وغيره من المعلومات‪.‬‬

‫بخ‪ww‬ط‬
‫ٍ‬ ‫‪ .2‬عندما تقابل كائنًا من النوع ‪ Element‬يحتوي عىل رابط‪ ،‬فعندها يُمكِنك اختبار ما إذا كان مكتوب ً‪ww‬ا‬

‫مائل باتباع روابط عقد األب أعىل الشجرة‪ ،‬فإذا وجدت بينها الوسم <‪ >i‬أو الوسم <‪ ،>em‬فهذا يَع‪ww‬ني أن‬
‫ٍ‬
‫بخط مائل‪.‬‬
‫ٍّ‬ ‫مكتوب‬
‫ٌ‬ ‫الرابط‬

‫‪w‬طر إىل فحص النص أثن‪ww‬اء التنق‪ww‬ل في الش‪ww‬جرة‬


‫‪ .3‬لكي تفحص ما إذا كان الرابط مكتوبًا بين أقواس‪ ،‬ستض‪ّ w‬‬
‫لكي تتعقب أق‪ww‬واس الفتح والغل‪ww‬ق (س‪ww‬يكون مثال ًي‪ww‬ا ل‪ww‬و اس‪ww‬تطاع الح‪ww‬ل الخ‪ww‬اص ب‪ww‬ك معالج‪ww‬ة األق‪ww‬واس‬

‫المتداخلة (مثل تلك))‪.‬‬

‫‪ .4‬إذا بدأت من مقالة ويكيبيديا عن جافا‪ ،‬فينبغي أن تصل إىل مقال‪ww‬ة الفلس‪ww‬فة بع‪ww‬د اتب‪ww‬اع ‪ 7‬رواب‪ww‬ط ل‪ww‬و لم‬

‫يحدث تغيير في صفحات ويكيبيديا منذ لحظة بدئنا بتشغيل الشيفرة‪.‬‬

‫اآلن وقد حصلت عىل كل المساعدة الممكنة‪ ،‬يُمكِنك أن تبدأ في العمل‪.‬‬

‫‪76‬‬
‫‪ .8‬المفهرس ‪Indexer‬‬

‫انتهينا من بناء زاحف اإلنترنت ‪ crawler‬في الفصل الس‪ww‬ابع الس‪ww‬ابق‪ ،‬وس‪ww‬ننتقل اآلن إىل الج‪ww‬زء الت‪ww‬الي من‬

‫‪w‬ات ‪data‬‬
‫تط‪ww‬بيق مح‪ww‬رك البحث‪ ،‬وه‪ww‬و الفه‪ww‬رس‪ .‬يُع‪ّ w‬د الفه‪ww‬رس ‪-‬في س‪ww‬ياق البحث ع‪ww‬بر اإلن‪ww‬ترنت‪ -‬هيك‪ww‬ل بيان‪ٍ w‬‬

‫‪ structure‬يُس ِّهل من العثور عىل الصفحات التي تحتوي عىل كلمة معينة‪ ،‬كما يساعدنا عىل معرفة ع‪ww‬دد م‪ww‬رات‬

‫ظهور الكلمة في كل صفحة‪ ،‬مما يُمكِّننا من تحديد الصفحات األكثر صلة‪.‬‬

‫المس‪ww‬تخدِم كلم‪ww‬تي البحث ‪ Java‬و‪ ،programming‬فإنن‪ww‬ا نبحث عن كلتيهم‪ww‬ا‬


‫عىل سبيل المثال‪ ،‬إذا أدخل ُ‬
‫‪w‬االت عن جزي‪ww‬رة‬
‫ٍ‬ ‫ستتضمن الص‪ww‬فحات الناتج‪ww‬ة عن البحث عن كلم‪ww‬ة ‪ Java‬مق‪w‬‬
‫َّ‬ ‫صفحات لكل كلمة‪.‬‬
‫ٍ‬ ‫ونسترجع عدة‬

‫ستتضمن الصفحات الناتجة عن البحث‬


‫َّ‬ ‫‪ ،Java‬وعن االسم المستعار للقهوة‪ ،‬وعن لغة البرمجة جافا‪ .‬في المقابل‪،‬‬

‫استخدامات أخرى للكلمة‪.‬‬


‫ٍ‬ ‫مقاالت عن لغات البرمجة المختلفة‪ ،‬وعن‬
‫ٍ‬ ‫عن كلمة ‪programming‬‬

‫باختيارنا لكلمات بحث تبحث عن الصفحات التي تحتوي عىل الكلمتين‪ ،‬سنتطلّع الستبعاد المق‪ww‬االت ال‪ww‬تي‬

‫ليس لها عالقة بكلمات البحث‪ ،‬وفي التركيز عىل الصفحات التي تتحدث عن البرمجة بلغة جافا‪.‬‬

‫بيانات يُم ِّثله‪.‬‬


‫ٍ‬ ‫واآلن وقد فهمنا ما يعنيه الفهرس والعمليات التي يُنف ّذها‪ ،‬يُمكِننا أن نُصمم هيكل‬

‫‪ 8.1‬اختيار هيكل البيانات‬


‫تتلخص العملي‪ww‬ة الرئيس‪ww‬ية للفه‪ww‬رس في إج‪ww‬راء البحث‪ ،‬فنحن نحت‪ww‬اج إىل إمكاني‪ww‬ة البحث عن كلم‪ww‬ةٍ مع ّين‪ww‬ةٍ‬

‫تتضمنها‪ .‬ربما يكون استخدام تجميعة من الص‪ww‬فحات ه‪ww‬و األس‪ww‬لوب األبس‪ww‬ط‬


‫َّ‬ ‫والعثور عىل جميع الصفحات التي‬

‫لتحقيق ذلك‪ ،‬فبتو ّفر كلمة بحث معينة‪ ،‬يُمكِننا المرور عبر محتويات الصفحات‪ ،‬وأن نخت‪ww‬ار من بينه‪ww‬ا تل‪ww‬ك ال‪ww‬تي‬

‫تحتوي عىل كلمة البحث‪ ،‬ولكن زمن التشغيل في تلك الطريقة سيتناسب مع عدد الكلمات الموجودة في جميع‬
‫ً‬
‫بطيئة للغاية‪.‬‬ ‫الصفحات‪ ،‬مما يَعنِي أن العملية ستكون‬
‫هياكل البيانات للمبرمجين‬ ‫المفهرس ‪Indexer‬‬

‫والطريقة البديلة عن تجميعة الصفحات ‪ collection‬هي ‪:‬الخريطة ‪ ،map‬والتي هي عبارة عن هيكل بيان‪ww‬ات‬

‫يتكون من مجموعة من أزواج‪ ،‬حيث يتألف كل منها من مفتاح وقيمة ‪.key-value‬‬

‫ُنش‪w‬ئ مثاًل الخريط‪ww‬ة‬ ‫ً‬


‫سريعة للبحث عن مفت‪ww‬اح معين والعث‪ww‬ور عىل قيمت‪ww‬ه المقابل‪ww‬ة‪ .‬سن ِ‬ ‫ً‬
‫طريقة‬ ‫تُو ِّفر الخريطة‬

‫‪ ،TermCounter‬بحيث تربُط كل كلمة بحث بعدد مرات ظهور تلك الكلمة في ك‪w‬ل ص‪w‬فحة‪ ،‬وس‪w‬تُمثِل المف‪w‬اتيح‬

‫كلمات البحث‪ ،‬بينما ستُمثِل القيم عدد مرات الظهور (أو تكرار الظهور)‪.‬‬

‫ي خريط‪ww‬ة‪ ،‬ومن أهمها‬


‫المف‪w‬ت َرض توافره‪w‬ا في أ ّ‬ ‫تُو ِّفر جاف‪w‬ا الواجه‪ww‬ة ‪ Map‬ال‪ww‬تي تُ ِّ‬
‫خص‪w‬ص التواب‪ww‬ع ‪ُ methods‬‬
‫ما يلي‪:‬‬

‫)‪ :get(key‬يبحث هذا التابع عن مفتاح معين ويعيد قيمته المقابلة‪.‬‬ ‫•‬

‫)‪ :put(key, value‬يضيف هذا التابع زوجًا جديدًا من أزواج مفتاح‪/‬قيمة إىل خريطة من النوع ‪،Map‬‬ ‫•‬

‫أو يستبدل القيمة المرتبطة بالمفتاح في حالة وجوده بالفعل‪.‬‬

‫تنفيذات للواجهة ‪ ،Map‬ومن بينها التنفي‪ww‬ذان ‪ HashMap‬و‪ TreeMap‬الل‪ww‬ذان سنناقش‪ww‬هما في‬


‫ٍ‬ ‫تُو ِّفر جافا عدة‬

‫فصول قادمة ونُحلّل أداء ُكلٍّ منهما‪.‬‬

‫باإلضافة إىل الخريطة ‪ TermCounter‬التي تربط كلمات البحث بعدد مرات ظهورها‪ ،‬سنُعرِّف ً‬
‫أيضا الصنف‬

‫‪ Index‬الذي يربط كل كلمة بحث بتجميعة الصفحات التي تَظهرَ فيها الكلمة‪ .‬يقودن‪ww‬ا ذل‪ww‬ك إىل الس‪ww‬ؤال الت‪ww‬الي‪:‬‬

‫كيف نُمثِل تجميعة الصفحات؟ سنتوصل إىل اإلجابة المناسبة إذا فكرنا في العملي‪w‬ات ال‪w‬تي نن‪w‬وي تنفي‪w‬ذها عىل‬

‫تلك التجميعة‪.‬‬

‫سنحتاج في هذا المثال إىل دمج مجموعتين أو أكثر‪ ،‬وإىل العثور عىل الصفحات التي تظه‪ww‬ر الكلم‪ww‬ات فيه‪ww‬ا‬

‫جميعً ا‪ .‬ويُمكِن النظر إىل ذلك وكأن‪ww‬ه عملي‪ww‬ة تق‪ww‬اطع مجموع‪ww‬تين ‪ .sets‬يتم َث‪ww‬ل تق‪ww‬اطع أي مجموع‪ww‬تين بمجموع‪ww‬ة‬

‫العناصر الموجودة في كلتيهما‪.‬‬

‫ً‬
‫قادرة عىل تنفيذها‪ ،‬ولكنه‪ww‬ا ال‬ ‫العمليات التي يُفترَض ألي مجموعة أن تكون‬
‫ِ‬ ‫تُ ِّ‬
‫خصص الواجهة ‪ Set‬بلغة جافا‬

‫تقاطع مجموعتين‪ ،‬وإن كانت تُو ِّفر تواب‪w‬عَ يُمكِن باس‪ww‬تخدامها تنفي‪ww‬ذ عملي‪ww‬ة التق‪ww‬اطع وغيره‪ww‬ا بكف‪ww‬اءة‪.‬‬
‫ِ‬ ‫تُو ِّفر عملية‬

‫وفيما يلي التوابع األساسية للواجهة ‪:Set‬‬

‫)‪ :add(element‬يض‪ww‬يف ه‪ww‬ذا الت‪ww‬ابع عنص‪w‬رًا إىل مجموع‪ww‬ة‪ .‬وإذا ك‪ww‬ان العنص‪ww‬ر موج‪ww‬ودًا فعل ًي‪ww‬ا ض‪ww‬من‬ ‫•‬

‫المجموعة‪ ،‬فإنه ال يفعل شيًئا‪.‬‬

‫الممرَّر موجودًا في المجموعة‪.‬‬


‫)‪ :contains(element‬يَفحَص هذا التابع ما إذا كان العنصر ُ‬ ‫•‬

‫تُو ِّفر جافا عدة تنفيذات للواجهة ‪ ،Set‬ومن بينها الصنفان ‪ HashSet‬و‪.TreeSet‬‬

‫اآلن وق‪ww‬د ص‪ww‬ممنا هياك‪ww‬ل البيان‪ww‬ات من أعىل ألس‪ww‬فل‪ ،‬فإنن‪ww‬ا س‪ww‬نُن ِّفذها من ال‪ww‬داخل إىل الخ‪ww‬ارج ب‪ww‬دءًا من‬

‫الصنف ‪.TermCounter‬‬

‫‪78‬‬
‫هياكل البيانات للمبرمجين‬ ‫المفهرس ‪Indexer‬‬

‫‪ 8.2‬الصنف ‪TermCounter‬‬
‫ربطا بين كلمات البحث م‪ww‬ع ع‪ww‬دد م‪ww‬رات ح‪ww‬دوثها في الص‪ww‬فحات‪ ،‬وتَع‪ww‬رِض‬
‫يُمثِل الصنف ‪ً TermCounter‬‬
‫الشيفرة التالية الجزء األول من تعريف الصنف‪:‬‬

‫{ ‪public class TermCounter‬‬

‫;‪private Map<String, Integer> map‬‬


‫;‪private String label‬‬

‫{ )‪public TermCounter(String label‬‬


‫;‪this.label = label‬‬
‫;)(>‪this.map = new HashMap<String, Integer‬‬
‫}‬
‫}‬

‫يربط متغير النسخة ‪ map‬الكلمات بعدد مرات حدوثها‪ ،‬بينما يُحدّد المتغ‪ww‬ير ‪ label‬المس‪ww‬تند ال‪ww‬ذي يحت‪ww‬وي‬

‫عىل تلك الكلمات‪ ،‬وسنَستخ ِدمه لتخزين محددات الموارد الموحدة ‪.URLs‬‬

‫يُع ّد الصنف ‪ HashMap‬أكثر تنفيذات الواجهة ‪ Map‬شيوعً ا‪ ،‬وسنَستخ ِدمه لتنفيذ عملية الربط‪ ،‬كما س‪ww‬نتناول‬

‫طريقة عمله ونفهم سبب شيوع استخدامه في الفصول القادمة‪.‬‬

‫يُو ِّفر الصنف ‪ TermCounter‬التابعين ‪ put‬و‪ُ get‬‬


‫المعرَّفين عىل النحو التالي‪:‬‬

‫{ )‪public void put(String term, int count‬‬


‫;)‪map.put(term, count‬‬
‫}‬

‫{ )‪public Integer get(String term‬‬


‫;)‪Integer count = map.get(term‬‬
‫;‪return count == null ? 0 : count‬‬
‫}‬

‫عمل التابع ‪ put‬بمثابة تابع ُمغلِّف‪ ،‬فعندما تستدعيه‪ ،‬س َيستدعِ ي بدوره الت‪ww‬ابع ‪ُ put‬‬
‫المع ‪w‬رَّف في الخريط‪ww‬ة‬ ‫يَ َ‬
‫المخزَّنة داخله‪.‬‬
‫ُ‬

‫حقيقي‪ ،‬فعن‪w‬دما تَس‪w‬تدعِ يه س َيس‪ww‬تدعِ ي الت‪ww‬ابع ‪ُ get‬‬


‫المع‪w‬رَّف في‬ ‫ّ‬ ‫من الجهة األخرى‪ ،‬يقوم التابع ‪ get‬بعم‪w‬ل‬

‫الخريط‪wwww‬ة‪ ،‬ثم يَفحَص النتيج‪wwww‬ة‪ ،‬ف‪wwww‬إذا لم تكن الكلم‪wwww‬ة موج‪wwww‬ود ًة في الخريط‪wwww‬ة من قب‪wwww‬ل‪ ،‬ف‪wwww‬إن الت‪wwww‬ابع‬

‫‪ TermCounter.get‬يعيد القيمة ‪.0‬‬

‫‪79‬‬
‫هياكل البيانات للمبرمجين‬ Indexer ‫المفهرس‬

‫تق ِبل‬ww‫ ويَس‬،‫ بسهولة‬incrementTermCount ‫ بتلك الطريقة عىل تعريف التابع‬get ‫يُساعدنا تعريف التابع‬

.1 ‫الخاص بها بمقدار‬


َّ ً
‫كلمة ويزيد العدّاد‬ ‫ذلك التابع‬

public void incrementTermCount(String term) {


put(term, get(term) + 1);
}

ً
‫ ثم نَستخدِم التابع‬،1 ‫ ونزيد العداد بمقدار‬،0 ‫ القيمة‬get ‫ فسيعيد‬،‫موجودة ضمن الخريطة‬ ‫إذا لم تكن الكلمة‬
ً
‫موجودة في الخريطة‬ ‫ إذا كانت الكلمة‬،‫ في المقابل‬.‫ جديد إىل الخريطة‬key-value ‫قيمة‬/‫ إلضافة زوج مفتاح‬put

.‫ ثم نُخزِّنها بحيث تَستبدِل القيمة القديمة‬،1 ‫ ونزيدها بمقدار‬،‫ فإننا نسترجع قيمة العداد القديم‬، ‫فعاًل‬

:‫ توابع أخرى للمساعدة عىل فهرسة صفحات اإلنترنت‬TermCounter ‫يُعرِّف الصنف‬

public void processElements(Elements paragraphs) {


for (Node node: paragraphs) {
processTree(node);
}
}

public void processTree(Node root) {


for (Node node: new WikiNodeIterable(root)) {
if (node instanceof TextNode) {
processText(((TextNode) node).text());
}
}
}

public void processText(String text) {


String[] array = text.replaceAll("\\pP", " ").
toLowerCase().
split("\\s+");

for (int i=0; i<array.length; i++) {


String term = array[i];
incrementTermCount(term);
}
}

80
‫هياكل البيانات للمبرمجين‬ ‫المفهرس ‪Indexer‬‬

‫‪ :processElements‬يَستق ِبل هذا التابع كائنًا من الن‪ww‬وع ‪ Elements‬ال‪ww‬ذي ه‪ww‬و تجميع‪ww‬ة من كائن‪ww‬ات‬ ‫•‬

‫كائن منها التابع ‪.processTree‬‬


‫ٍ‬ ‫‪ .Element‬يمرّ التابع عبر التجميعة ويَستدعِ ي لكل‬

‫ً‬
‫عقدة تُم ِّثل عقدة ج‪ww‬ذر ش‪ww‬جرة ‪ ،DOM‬ويَم‪w‬رّ الت‪ww‬ابع ع‪ww‬بر الش‪ww‬جرة‪ ،‬ليع‪ww‬ثر عىل‬ ‫‪ :processTree‬يَستق ِبل‬ ‫•‬

‫العقد التي تحتوي عىل نص‪ ،‬ثم يَستخر ِج منها النص ويُمرِّره إىل التابع ‪.processText‬‬

‫سلسلة نص‪ً w‬‬


‫‪w‬ية من الن‪w‬وع ‪ String‬تحت‪ww‬وي عىل كلم‪w‬ات وفراغ‪ww‬ات وعالم‪ww‬ات‬ ‫ً‬ ‫‪ :processText‬يَستق ِبل‬ ‫•‬

‫ترقيم وغيرها‪ .‬يَح ِذف التابع عالمات الترقيم باستبدالها بفراغ‪ww‬ات‪ ،‬ويُح ‪ِّ w‬ول األح‪ww‬رف المتبقي‪ww‬ة إىل حالته‪ww‬ا‬

‫قس‪www‬م النص إىل كلم‪www‬ات‪ .‬يَم‪www‬رّ الت‪www‬ابع ع‪www‬بر تل‪www‬ك الكلم‪www‬ات‪ ،‬ويَس‪www‬تدِعي الت‪www‬ابع‬
‫الص‪www‬غرى‪ ،‬ثم يُ ِّ‬
‫‪ incrementTermCount‬لكُ‪w‬لٍّ منه‪w‬ا‪ ،‬ويَس‪ww‬تق ِبل التابع‪ww‬ان ‪ replaceAll‬و‪ split‬تعب‪w‬يرات نمطي‪ww‬ة‬

‫‪ regular expression‬مثل معامالت‪.‬‬

‫وأخيرًا‪ ،‬انظر إىل المثال التالي الذي يُ ِّ‬


‫وضح طريقة استخدام الصنف ‪:TermCounter‬‬

‫= ‪String url‬‬
‫;")‪"http://en.wikipedia.org/wiki/Java_(programming_language‬‬
‫;)(‪WikiFetcher wf = new WikiFetcher‬‬
‫;)‪Elements paragraphs = wf.fetchWikipedia(url‬‬

‫;)‪TermCounter counter = new TermCounter(url‬‬


‫;)‪counter.processElements(paragraphs‬‬
‫;)(‪counter.printCounts‬‬

‫يَستخدِم هذا المثال كائنًا من النوع ‪ WikiFetcher‬لتحميل ص‪ww‬فحةٍ من موق‪ww‬ع ويكيبي‪ww‬ديا‪ ،‬ثم يُحلّ‪ww‬ل النص‬

‫نشئ كائنًا من النوع ‪ TermCounter‬ويَستخدِمه لع ّد الكلمات الموجودة في الصفحة‪.‬‬


‫الرئيسي الموجو َد بها‪ ،‬ويُ ِ‬
‫َّ‬

‫يُمكِنك تشغيل الشيفرة في القسم التالي‪ ،‬واختبار فهمك لها بإكمال متن التابع غير المكتمل‪.‬‬

‫‪ 8.3‬تمرين ‪6‬‬
‫ستجد ملفات شيفرة التمرين في مستودع الكتاب‪:‬‬

‫‪ :TermCounter.java‬يحتوي عىل شيفرة القسم السابق‪.‬‬ ‫•‬

‫‪ :TermCounterTest.java‬يحتوي عىل شيفرة اختبار الملف ‪.TermCounter.java‬‬ ‫•‬

‫‪ :Index.java‬يحتوي عىل تعريف الصنف الخاص بالجزء التالي من التمرين‪.‬‬ ‫•‬

‫‪ :WikiFetcher.java‬يحتوي عىل الصنف ال‪w‬ذي اس‪ww‬تخدمناه في التم‪w‬رين الس‪ww‬ابق لتحمي‪ww‬ل ص‪ww‬فحة‬ ‫•‬

‫إنترنت وتحليلها‪.‬‬

‫‪81‬‬
‫هياكل البيانات للمبرمجين‬ ‫المفهرس ‪Indexer‬‬

‫‪ :WikiNodeIterable.java‬يحتوي عىل الصنف الذي استخدمناه للتنقل في عقد شجرة ‪.DOM‬‬ ‫•‬

‫َجد ً‬
‫أيضا ملف البناء ‪.build.xml‬‬ ‫ست ِ‬

‫ن ِّفذ األمر ‪ ant build‬لتصريف ملفات الشيفرة‪ ،‬ثم ن ِّفذ األمر ‪ ant TermCounter‬لكي تُش ِّغل ش‪ww‬يفرة‬

‫‪w‬رج مش‪ww‬ابهٍ‬
‫تحص‪w‬ل عىل خ‪ٍ w‬‬
‫ُ‬ ‫القسم السابق‪ .‬تَطبَع تلك الشيفرة قائمة بالكلمات وع‪ww‬دد م‪ww‬رات ظهوره‪ww‬ا‪ ،‬وينبغي أن‬

‫لما يلي‪:‬‬

‫‪genericservlet, 2‬‬
‫‪configurations, 1‬‬
‫‪claimed, 1‬‬
‫‪servletresponse, 2‬‬
‫‪occur, 2‬‬
‫‪Total of all counts = -1‬‬

‫ي‬ ‫ّ‬ ‫قد تجد ترتيب ظهور الكلمات مختل ًفا عندما تُش ِّغل الشيفرة‪ ،‬وينبغي أن يَطبَع السطر األخير المجم‪ww‬و َ‬
‫ع الكل َّ‬
‫لعدد مرات ظهور جميع الكلمات‪ ،‬ولكنه يعيد القيمة ‪ 1-‬في هذا المثال ألن التابع ‪ size‬غير مكتم‪ww‬ل‪ .‬أكم‪ww‬ل متن‬

‫هذا التابع‪ ،‬ثم ن ِّفذ األمر ‪ ant TermCounter‬مر ًة أخرى‪ ،‬حيث ينبغي أن تحصل عىل القيمة ‪.4798‬‬

‫بشكل صحيح‪.‬‬
‫ٍ‬ ‫ن ِّفذ األمر ‪ ant TermCounterTest‬لكي تتأ َّكد من أنك قد أكملت جزء التمرين ذاك‬

‫لكائن من النوع ‪ ،Index‬وسيكون عليك إكمال متن التابع‬


‫ٍ‬ ‫بالنسبة للجزء الثاني من التمرين‪ ،‬فسنُو ِّفر تنفي ًذا‬

‫غير ِ المكتمل‪ .‬انظر إىل تعريف الصنف‪:‬‬

‫{ ‪public class Index‬‬

‫= ‪private Map<String, Set<TermCounter>> index‬‬


‫;)(>>‪new HashMap<String, Set<TermCounter‬‬

‫{ )‪public void add(String term, TermCounter tc‬‬


‫;)‪Set<TermCounter> set = get(term‬‬

‫ً‬
‫جديدة إذا كنت ترى الكلمة للمرة الأولى ‪//‬‬ ‫ً‬
‫مجموعة‬ ‫أنشئ‬
‫{ )‪if (set == null‬‬
‫;)(>‪set = new HashSet<TermCounter‬‬
‫;)‪index.put(term, set‬‬
‫}‬
‫إذا كنت قد رأيت الكلمة من قبل‪ِّ ،‬‬
‫عدل المجموعة الموجودة ‪//‬‬

‫‪82‬‬
‫هياكل البيانات للمبرمجين‬ ‫المفهرس ‪Indexer‬‬

‫;)‪set.add(tc‬‬
‫}‬

‫{ )‪public Set<TermCounter> get(String term‬‬


‫;)‪return index.get(term‬‬
‫}‬

‫كائن‪ww‬ات تنتمي إىل الن‪ww‬وع‬


‫ٍ‬ ‫بحث بمجموع‪ww‬ةِ‬
‫ٍ‬ ‫ً‬
‫خريط‪ww‬ة ‪ map‬ترب‪ww‬ط ك‪ww‬ل كلم‪ww‬ة‬ ‫يُمثِ‪ww‬ل متغ‪ww‬ير النس‪ww‬خة ‪index‬‬
‫ً‬
‫صفحة ظهرت فيها تلك الكلمة‪.‬‬ ‫كائن منها‬ ‫‪ ،TermCounter‬ويُم ِّثل كل‬
‫ٍ‬

‫يضيف التابع ‪ add‬كائنً‪w‬ا جدي‪w‬دًا من الن‪w‬وع ‪ TermCounter‬إىل المجموع‪w‬ة الخاص‪w‬ة بكلم‪w‬ةٍ معين‪w‬ة‪ .‬وعن‪w‬دما‬
‫نش‪w‬ئ له‪ww‬ا مجموع‪ً w‬‬
‫‪w‬ة جدي‪ww‬دة‪ ،‬أم‪ww‬ا إذا كن‪ww‬ا ق‪ww‬د قابلن‪ww‬ا الكلم‪ww‬ة من قب‪ww‬ل‪،‬‬ ‫ً‬
‫كلمة ألول مرة‪ ،‬سيكون علين‪ww‬ا أن ن ُ ِ‬ ‫نُفهرس‬

‫فسنضيف فقط عنصرًا جديدًا إىل مجموع‪ww‬ة تل‪ww‬ك الكلم‪ww‬ة‪ ،‬أي يُع‪w‬دِّل الت‪ww‬ابع ‪ set.add‬عن‪ww‬دما تك‪ww‬ون المجموع‪ww‬ة‬
‫ً‬
‫موجودة بالفعل داخل ‪ index‬وال يُعدِّل ‪ index‬ذاته‪ ،‬حيث إننا سنض‪ww‬طر إىل تع‪ww‬ديل ‪ index‬فق‪ww‬ط عن‪ww‬د إض‪ww‬افة‬

‫كلمةٍ جديدة‪.‬‬

‫بحث‪ ،‬ويعيد مجموعة كائنات الصنف ‪ TermCounter‬المقابلة للكلمة‪.‬‬


‫ٍ‬ ‫وأخي ًرا‪ ،‬يستقبل التابع ‪ get‬كلمة‬

‫يُعَ ّد هيكل البيانات هذا ُمعقدًا بعض الشيء‪ .‬والختصاره‪ ،‬يمكن الق‪ww‬ول أن ك‪ww‬ائن الن‪ww‬وع ‪ Index‬يحت‪ww‬وي عىل‬

‫‪w‬ات تنتمي إىل الن‪ww‬وع‬


‫بحث بمجموع‪ww‬ةٍ من الن‪ww‬وع ‪ ،Set‬المك َّون‪ww‬ةٍ من كائن‪ٍ w‬‬
‫ٍ‬ ‫خريطةٍ من الن‪ww‬وع ‪ Map‬ترب‪ww‬ط ك‪ww‬ل كلم‪ww‬ة‬

‫كلمات البحث بعدد مرات ظهور تلك الكلمات‪.‬‬


‫ِ‬ ‫ً‬
‫خريطة تربط‬ ‫كائن منها‬ ‫‪ ،TermCounter‬حيث يُمثِل كلّ‬
‫ٍ‬

‫رسما توضيح ًّيا لتلك الكائنات‪ ،‬حيث يحتوي كائن الصنف ‪ Index‬عىل متغير نسخة‬
‫ً‬ ‫تَعرِض الصورة السابقة‬

‫اسمه ‪ index‬يشير إىل كائن الصنف ‪ ،Map‬الذي يحتوي ‪-‬في هذا المثال‪ -‬عىل سلسلةٍ نص ّيةٍ واحد ٍة ‪ Java‬مرتبطةٍ‬

‫‪83‬‬
‫هياكل البيانات للمبرمجين‬ ‫المفهرس ‪Indexer‬‬

‫بمجموعةٍ من النوع ‪ Set‬تحتوي عىل ك‪ww‬ائنين من الن‪ww‬وع ‪TermCounter‬؛ بحيث يك‪ww‬ون واح‪w‬دًا لك‪ww‬ل ص‪ww‬فحة ق‪ww‬د‬

‫ظهرت فيها كلمة ‪.Java‬‬

‫كائن من النوع ‪ TermCounter‬عىل متغيرَ النسخة ‪ label‬الذي يُمثِل ُمح ‪w‬دّد الم‪ww‬وارد الموح‪ww‬د‬
‫ٍ‬ ‫يتضمن كلّ‬
‫َّ‬
‫يتضمن المتغي َر ‪ map‬الذي يحتوي عىل الكلمات الموجودة في الصفحة‪ ،‬وعدد مرات‬
‫َّ‬ ‫‪ URL‬الخاص بالصفحة‪ ،‬كما‬

‫حدوث كلّ كلمةٍ منها‪.‬‬

‫يُ ِّ‬
‫وضح التابع ‪ printIndex‬طريقة قراءة هيكل البيانات ذاك‪:‬‬

‫{ )(‪public void printIndex‬‬


‫ّ‬
‫مر عبر كلمات البحث ‪//‬‬
‫{ ))(‪for (String term: keySet‬‬
‫;)‪System.out.println(term‬‬

‫لكل كلمة‪ ،‬اطبع الصفحات التي ظهرت فيها الكلمة وعدد مرات ظهورها ‪//‬‬
‫;)‪Set<TermCounter> tcs = get(term‬‬
‫{ )‪for (TermCounter tc: tcs‬‬
‫;)‪Integer count = tc.get(term‬‬
‫"(‪System.out.println‬‬ ‫‪" + tc.getLabel() + " " +‬‬
‫;)‪count‬‬
‫}‬
‫}‬
‫}‬

‫تم‪ww‬رّ حلق‪ww‬ة التك‪ww‬رار الخارجي‪ww‬ة ع‪ww‬بر كلم‪ww‬ات البحث‪ ،‬بينم‪ww‬ا تم‪ww‬رّ حلق‪ww‬ة التك‪ww‬رار الداخلي‪ww‬ة ع‪ww‬بر كائن‪ww‬ات‬

‫الصنف ‪.TermCounter‬‬

‫ن ِّفذ األم‪ww‬ر ‪ ant build‬لكي تتأ َّكد من تص‪ww‬ريف ملف‪ww‬ات الش‪ww‬يفرة‪ ،‬ثم ن ِّفذ األم‪ww‬ر ‪ .ant Index‬س ‪ُ w‬ي ِّ‬
‫حمل‬

‫خرج عند تشغيله ألنن‪ww‬ا تركن‪ww‬ا أح‪ww‬د‬


‫ٍ‬ ‫صفحتين من موقع ويكيبيديا ويُفهرسهما‪ ،‬ثم يَطبَع النتائج‪ ،‬ولكنك لن ترى أي‬

‫التوابع فار ًغا‪.‬‬

‫دورك اآلن هو إكمال التابع ‪ indexPage‬الذي يَستق ِبل ُمحدّد موارد موحّدًا ‪( URL‬عبارة عن سلسلةٍ نصيةٍ )‪،‬‬

‫وكائنًا من النوع ‪ ،Elements‬ويُحدِّث الفهرس‪ .‬تُ ِّ‬


‫وضح التعليقات ما ينبغي أن تفعله‪:‬‬

‫{ )‪public void indexPage(String url, Elements paragraphs‬‬


‫ّ‬
‫وعد الكلمات بكل فقرة ‪//‬‬ ‫أنشئ كائنًا من النوع ‪TermCounter‬‬
‫لكل كلمة في كائن النوع ‪ ،TermCounter‬أضفه إلى ‪// index‬‬
‫}‬

‫‪84‬‬
‫هياكل البيانات للمبرمجين‬ Indexer ‫المفهرس‬

:‫ فستحصل عىل الخرج التالي‬،‫سليما‬


ً ‫ إذا كان كل شيء‬،‫ وبعد االنتهاء‬. ant Index ‫ن ِّفذ األمر‬

...
configurations
http://en.wikipedia.org/wiki/Programming_language 1
http://en.wikipedia.org/wiki/Java_(programming_language) 1
claimed
http://en.wikipedia.org/wiki/Java_(programming_language) 1
servletresponse
http://en.wikipedia.org/wiki/Java_(programming_language) 2
occur
http://en.wikipedia.org/wiki/Java_(programming_language) 2

.‫ضع في الحسبان أنه عند إجرائك للبحث قد يختلف ترتيب ظهور كلمات البحث‬

.‫ لكي تتأ َّكد من اكتمال هذا الجزء من التمرين عىل النحو المطلوب‬ant TestIndex ‫ ن ِّفذ األمر‬،‫وأخيرًا‬

85
‫‪ .9‬الواجهة ‪Map‬‬

‫ً‬
‫مختلفة للواجهة ‪ ،Map‬حيث يعتم ُد أحدها عىل الجدول ‪،hash table‬‬ ‫تنفيذات‬
‫ٍ‬ ‫سنتناول في التمارين التالية‬
‫والذي يُع ّد واحدًا من أفض‪ww‬ل هياك‪ww‬ل البيان‪ww‬ات الموج‪ww‬ودة‪ ،‬في حين يتش‪ww‬ابه تنفي‪ٌ w‬‬
‫ذ آخ‪w‬رُ م‪ww‬ع الص‪ww‬نف ‪،TreeMap‬‬

‫ويُمكِّننا من المرور عبر العناصر بحسب ترتيبها‪ ،‬غير أنّه ال يتمتع بكفاءة الجداول‪.‬‬

‫ستكون لديك الفرصة لتنفيذ هياكل البيانات تلك وتحليل أدائه‪ww‬ا‪ ،‬وس‪ww‬نبدأ أواًل بتنفي‪ww‬ذ بس‪ww‬يط للواجه‪ww‬ة ‪Map‬‬

‫باستخدام قائمة من النوع ‪ List‬تتك ّون من أزواج مفاتيح‪/‬قيم ‪ ،key-value‬ثم سننتقل إىل شرح الجداول‪.‬‬

‫‪ 9.1‬تنفيذ الصنف ‪MyLinearMap‬‬


‫ً‬
‫مبدئي‪ww‬ة‪ ،‬ومهمت‪ww‬ك إكم‪ww‬ال التواب‪ww‬ع غ‪ww‬ير المكتمل‪ww‬ة‪ .‬انظ‪ww‬ر إىل بداي‪ww‬ة تعريف‬ ‫س‪ww‬نُو ِّفر كالمعت‪ww‬اد ش‪ww‬يفر ًة‬

‫الصنف ‪:MyLinearMap‬‬

‫{ >‪public class MyLinearMap<K, V> implements Map<K, V‬‬

‫;)(>‪private List<Entry> entries = new ArrayList<Entry‬‬

‫يَستخدِم هذا الصنف معاملي نوع ‪ ،type parameters‬حيث يش‪ww‬ير المعام‪ww‬ل األول ‪ K‬إىل ن‪ww‬وع المف‪ww‬اتيح‪،‬‬

‫بينما يشير المعامل الثاني ‪ V‬إىل نوع القيم‪ .‬ونظرًا ألن الصنف ‪ MyLinearMap‬يُن ِّفذ الواجهة ‪ ،Map‬فإن علي‪ww‬ه أن‬

‫يُو ِّفر التوابع الموجودة في تلك الواجهة‪.‬‬

‫تحتوي كائنات النوع ‪ MyLinearMap‬عىل متغير نسخةٍ ‪ instance variable‬وحي ٍد ‪ ،entries‬وهو عب‪ww‬ارة‬

‫عن قائمة من النوع ‪ ArrayList‬مك ّونة من كائنات تنتمي إىل النوع ‪ ،Entry‬حيث يحتوي كل ك‪ww‬ائن من الن‪ww‬وع‬

‫‪ Entry‬عىل زوج مفتاح‪-‬قيمة‪ .‬انظر فيما يلي إىل تعريف الصنف‪:‬‬


‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪Map‬‬

‫{ >‪public class Entry implements Map.Entry<K, V‬‬


‫;‪private K key‬‬
‫;‪private V value‬‬

‫{ )‪public Entry(K key, V value‬‬


‫;‪this.key = key‬‬
‫;‪this.value = value‬‬
‫}‬

‫‪@Override‬‬
‫{ )(‪public K getKey‬‬
‫;‪return key‬‬
‫}‬
‫‪@Override‬‬
‫{ )(‪public V getValue‬‬
‫;‪return value‬‬
‫}‬
‫}‬

‫حاو لزوج مفتاح‪/‬قيم‪ww‬ة‪ ،‬ويق‪ww‬ع تعري‪ww‬ف ذل‪ww‬ك الص‪ww‬نف‬


‫ال تتعدى كائنات الصنف ‪ Entry‬كونها أكثر من مجرد ٍ‬
‫ضمن الصنف ‪ ،MyLinearList‬ويَستخدِم نفس معامالت النوع ‪ K‬و‪.V‬‬

‫هذا هو كل ما ينبغي أن تعرفه لحل التمرين‪ ،‬ولذا سننتقل إليه اآلن‪.‬‬

‫‪ 9.2‬تمرين ‪7‬‬
‫ستجد ملفات شيفرة التمرين في مستودع الكتاب‪:‬‬

‫‪ :MyLinearMap.java‬يحتوي هذا الصنف عىل الشيفرة المبدئية للجزء األول من التمرين‪.‬‬ ‫•‬

‫‪ :MyLinearMapTest.java‬يحتوي عىل اختبارات الواحدة ‪ unit tests‬للصنف ‪.MyLinearMap‬‬ ‫•‬

‫ستجد ً‬
‫أيضا ملف البناء ‪ build.xml‬في المستودع‪.‬‬

‫ن ِّفذ األمر ‪ ant build‬لكي تُصرِّف ملفات الشيفرة‪ ،‬ثم ن ِّفذ األمر ‪ .ant MyLinearMapTest‬س‪w‬تجد أن‬

‫بعض االختبارات لم تنجح؛ والسبب هو أنه ما يزال عليك القيام ببعض العمل‪.‬‬

‫أكمل متن التابع المساعد ‪ findEntry‬أواًل ‪ .‬ال يُع ّد هذا التابع جزءًا من الواجهة ‪ ،Map‬ولكن بمجرد أن تكمله‬

‫الم‪ْ w‬دخَالت‪،‬‬
‫معين ض‪ww‬من ُ‬
‫ٍ‬ ‫مفتاح‬
‫ٍ‬ ‫بشكل صحيح‪ ،‬ستتمكَّن من استخدامه ضمن توابعَ كثيرة‪ .‬يبحث هذا التابع عن‬
‫ٍ‬

‫‪87‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪Map‬‬

‫الم ْدخَل الذي يحتوي عىل ذلك المفتاح‪ ،‬أو القيمة الفارغة ‪ null‬إذا لم يكن موجودًا‪ ،‬كما يوازن الت‪ww‬ابع‬
‫ثم يعيد إما ُ‬
‫‪- equals‬الذي وفرناه لك‪ -‬بين مفتاحين‪ ،‬ويعالج القيم الفارغة ‪ null‬بشكل مناسب‪.‬‬

‫ن ِّفذ األمر ‪ ant MyLinearMapTest‬م‪ً w‬‬


‫‪w‬رة أخ‪ww‬رى‪ .‬ح‪ww‬تى ل‪ww‬و كنت ق‪ww‬د أكملت الت‪ww‬ابع ‪ findEntry‬بش‪ww‬كل‬

‫مكتمل بعد‪ ،‬لهذا أكمل التابع ‪ .put‬ينبغي أن تقرأ توثيق الت‪ww‬ابع‬


‫ٍ‬ ‫صحيح‪ ،‬فلن تنجح االختبارات ألن التابع ‪ put‬غير‬

‫‪ Map.put‬باللغة اإلنجليزية أواًل لكي تَعرِف ما ينبغي أن تفعله‪ .‬ويُمكِن‪ww‬ك الب‪ww‬دء بكتاب‪ww‬ة نس‪ww‬خةٍ بس‪w‬يطةٍ من الت‪ww‬ابع‬

‫المدخالت الموجودة‪ .‬سيساعدك ذلك عىل اختبار الحالة البس‪ww‬يطة من‬


‫ِ‬ ‫خاًل جديدًا وال تُعدِّل‬
‫دوما ُم ْد َ‬
‫ً‬ ‫‪ ،put‬تضيف‬

‫التابع‪ ،‬أما إذا كانت لديك الثقة الكافية‪ ،‬فبإمكانك كتابة التابع كاماًل من البداية‪.‬‬

‫ينبغي أن ينجح االختبار ‪ containsKey‬بعدما تنتهي من كتابة التابع ‪ .put‬اقرأ توثي‪w‬ق الت‪w‬ابع ‪ Map.get‬ثم‬

‫ن ِّفذه‪ ،‬وش ِّغل االختبارات مر ًة أخرى‪ .‬وأخي ًرا‪ ،‬اقرأ توثيق التابع ‪ ،Map.remove‬ثم ن ِّفذه‪.‬‬

‫بوصولك إىل هذه النقطة‪ ،‬يُفترَض أن تكون جميع االختبارات قد نجحت‪.‬‬

‫‪ 9.3‬تحليل الصنف ‪MyLinearMap‬‬


‫س‪ww‬نُقدِّم حاًل للتم‪ww‬رين ال‪ww‬وارد في األعىل‪ ،‬ثم س‪ww‬نُحلِّل أداء التواب‪ww‬ع األساس‪ww‬ية‪ .‬انظ‪ww‬ر إىل تعري‪ww‬ف الت‪ww‬ابعين‬

‫‪ findEntry‬و‪:equals‬‬

‫{ )‪private Entry findEntry(Object target‬‬


‫{ )‪for (Entry entry: entries‬‬
‫{ )))(‪if (equals(target, entry.getKey‬‬
‫;‪return entry‬‬
‫}‬
‫}‬
‫;‪return null‬‬
‫}‬

‫{ )‪private boolean equals(Object target, Object obj‬‬


‫{ )‪if (target == null‬‬
‫;‪return obj == null‬‬
‫}‬
‫;)‪return target.equals(obj‬‬
‫}‬

‫قد يعتمد زمن تشغيل التابع ‪ equals‬عىل حجم ‪ target‬والمفاتيح‪ ،‬ولكنه ال يعتمد في العم‪ww‬وم عىل ع‪ww‬دد‬

‫الم ْدخَالت ‪ ،n‬وعليه‪ ،‬يَستغرِق التابع ‪ equals‬زمنًا ثابتًا‪.‬‬


‫ُ‬

‫‪88‬‬
‫هياكل البيانات للمبرمجين‬ Map ‫الواجهة‬

‫ذا ليس‬ww‫ ولكن ه‬،‫ة‬ww‫ ربما يحالفنا الحظ ونجد المفتاح الذي نبحث عنه في البداي‬،findEntry ‫بالنسبة للتابع‬

ُ ‫ يتناسب عدد‬،‫ ففي العموم‬،‫مضمونًا‬


findEntry ‫ابع‬ww‫تغرِق الت‬ww‫ يَس‬،‫ وعليه‬،n ‫الم ْدخَالت التي سنبحث فيها مع‬

.‫زمنًا خط ًيا‬

‫ بما في ذلك‬،findEntry ‫ عىل التابع‬MyLinearMap ‫المعرَّفة في الصنف‬


ُ ‫تعتمد معظم التوابع األساسية‬
:‫ انظر تعريف تلك التوابع‬.remove‫ و‬get‫ و‬put ‫التوابع‬

public V put(K key, V value) {


Entry entry = findEntry(key);
if (entry == null) {
entries.add(new Entry(key, value));
return null;
} else {
V oldValue = entry.getValue();
entry.setValue(value);
return oldValue;
}
}
public V get(Object key) {
Entry entry = findEntry(key);
if (entry == null) {
return null;
}
return entry.getValue();
}
public V remove(Object key) {
Entry entry = findEntry(key);
if (entry == null) {
return null;
} else {
V value = entry.getValue();
entries.remove(entry);
return value;
}
}

89
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪Map‬‬

‫بعدما يَستدعِ ي التابعُ ‪ put‬التابعَ ‪ ،findEntry‬فإن ك‪ww‬ل ش‪ww‬ي ٍء آخ‪w‬رَ ض‪ww‬منَه يس‪ww‬تغرق زمنً‪ww‬ا ثاب ًت‪ww‬ا‪ .‬الحِ‪ w‬ظ أن‬
‫‪ entries‬هي عب‪ٌ w‬‬
‫‪w‬ارة عن قائم‪ww‬ةٍ من الن‪ww‬وع ‪ ،ArrayList‬وأن إض‪ww‬افة عنص‪ww‬ر إىل نهاي‪ww‬ة قائم‪ww‬ةٍ من ذل‪ww‬ك الن‪ww‬وع‬

‫َل‬
‫نضطر إىل إضافة ُم ْدخ ٍ‬
‫ّ‬ ‫تستغرق زمنًا ثابتًا في المتوسط؛ فإذا كان المفتاح موجودًا بالفعل في الخريطة‪ ،‬فإننا لن‬

‫‪w‬طر الس‪ww‬تدعاء الت‪ww‬ابعين ‪ entry.getValue‬و‪ ،entry.setValue‬وكالهم‪ww‬ا‬


‫جديدٍ‪ ،‬ولكنن‪ww‬ا في المقاب‪ww‬ل سنض‪ّ w‬‬
‫يستغرق زمنًا ثابتًا‪ .‬بنا ًء عىل ما سبق‪ ،‬يُع ّد التابع ‪ put‬خط ًيا‪ ،‬ويَستغرِق التابع ‪ get‬زمنًا خط ًيا لنفس السبب‪.‬‬

‫‪w‬طر الت‪ww‬ابع ‪ entries.remove‬إىل ح‪ww‬ذف العنص‪ww‬ر من بداي‪ww‬ة أو‬


‫يُع ّد التابع ‪ remove‬أعقد نو ًعا ما؛ فقد يض‪ّ w‬‬
‫وسط قائمةٍ من النوع ‪ ،ArrayList‬وهو ما يستغرِق زمنًا خط ًيا‪ .‬والواقع أنّه ليس هناك مش‪ww‬كلة في ذل‪ww‬ك‪ ،‬فم‪ww‬ا‬

‫عملية خط ّي ًة ً‬
‫أيضا‪.‬‬ ‫ً‬ ‫تزال محصلة عمليتين خطيتين‬

‫نستخلص مما سبق أن جميع التوابع األساسية ضمن ذلك الصنف خطية‪ ،‬ولهذا السبب أطلقن‪ww‬ا علي‪ww‬ه اس‪ww‬م‬

‫‪.MyLinearMap‬‬

‫الم ْدخَالت صغي ًرا‪ ،‬ولكن ما يزال بإمكاننا أن ن ُ ِّ‬


‫حس‪ww‬نه‪ .‬في الحقيق‪ww‬ة‪،‬‬ ‫قد يكون هذا التنفيذ مناسبًا إذا كان عدد ُ‬
‫يُمكِننا أن نُن ِّفذ جميع توابع الواجهة ‪ ،Map‬بحيث تَستغرِق زمنًا ثابتًا‪ .‬قد يبدو ذلك مس‪ww‬تحياًل عن‪ww‬دما تس‪ww‬معه ألول‬

‫‪w‬ابت‪ ،‬وذل‪ww‬ك بغض النظ‪ww‬ر عن حجم‬


‫زمن ث‪ٍ w‬‬
‫ٍ‬ ‫مرة‪ ،‬فهو أشبه بأن نقول أن بإمكاننا العثور عىل إبر ٍة في كومة ٍّ‬
‫قش في‬

‫كومة القش‪.‬‬

‫سنشرح كيف لذلك أن يكون ممكنًا في خطوتين‪:‬‬

‫‪w‬وائم قص‪ww‬يرةٍ‪،‬‬
‫سنقسمها عىل عدة ق‪َ w‬‬
‫ِّ‬ ‫‪ .1‬بداًل من أن نُخزِّن ُ‬
‫الم ْدخَالت في قائمةٍ واحد ٍة كبير ٍة من النوع ‪،List‬‬

‫وسنَستخدِم شيفرة تعمية ‪ - hash code‬وسنشرح معناها في الفصل التالي‪ -‬لكل مفتاح؛ وذلك لتحديد‬

‫القائمة التي سنَستخدِمها‪.‬‬

‫قوائم قصير ٍة أسر َ‬


‫ع من اِستخدَام قائمةٍ واحد ٍة كب‪ww‬يرةٍ‪ ،‬ولكن‪ww‬ه م‪ww‬ع ذل‪ww‬ك ال يُغ ِّير ت‪ww‬رتيب‬ ‫َ‬ ‫‪ .2‬يُعَ د استخدام عدة‬

‫النمو ‪- order of growth‬كما سنناقش الح ًقا‪ ،-‬فما تزال العمليات األساسية خط ّي ًة‪ ،‬ولكن هنالك خدعة‬

‫ستُمكِّننا من تجاوز ذلك‪ ،‬فإذا زِدنا عدد الق‪ww‬وائم بحيث نُق ّي‪ww‬د ع‪ww‬دد ُ‬
‫الم‪ْ w‬دخَالت الموج‪ww‬ودة في ك‪ww‬ل قائم‪ww‬ة‪،‬‬

‫ثابت‪ .‬سنناقش تفاصيل ذلك في تمرين الفصل التالي‪ ،‬ولكن قب‪ww‬ل أن‬
‫ٍ‬ ‫زمن‬
‫ٍ‬ ‫فسنحصل عىل خريطةٍ ذات‬

‫نفعل ذلك سنشرح ما تعنيه التعمية ‪.hashing‬‬

‫سنتناول حل هذا التمرين ونُحلِّل أداء التوابع األساسية للواجهة ‪ Map‬في الفصل التالي‪ ،‬وسنُقدِّم ً‬
‫أيضا تنفي ًذا‬

‫أكثر كفاءة‪.‬‬

‫‪90‬‬
‫‪ .10‬التعمية ‪Hashing‬‬

‫بش‪www‬كل أفض‪www‬لَ من‬


‫ٍ‬ ‫الص‪www‬نف ‪ MyBetterMap‬ال‪www‬ذي يُن ِّفذ الواجه‪www‬ة ‪Map‬‬
‫َ‬ ‫س‪www‬نُعرِّف في ه‪www‬ذا الفص‪www‬ل‬

‫‪ ،MyLinearMap‬كما س‪ww‬نتناول تقني‪ww‬ة التعمي‪ww‬ة ‪ hashing‬ال‪ww‬تي س‪ww‬اعدتنا عىل تنفي‪ww‬ذ الص‪ww‬نف ‪MyBetterMap‬‬

‫بتلك الكفاءة‪.‬‬

‫‪ 10.1‬التعمية ‪Hashing‬‬
‫به‪ww‬دف تحس‪ww‬ين أداء الص‪ww‬نف ‪ ،MyLinearMap‬س ‪w‬نُعرِّف ص‪ww‬ن ًفا جدي ‪w‬دًا ه‪ww‬و ‪ MyBetterMap‬يحت‪ww‬وي عىل‬

‫المرفق‪ww‬ة لكي‬
‫قسم الصنف الجديد المفاتيح عىل الخرائ‪ww‬ط ُ‬
‫كائنات تنتمي إىل الصنف ‪ .MyLinearMap‬يُ ِّ‬
‫ٍ‬ ‫تجميعة‬

‫يُقلِّل عدد ُ‬
‫الم ْدخَالت الموجودة في كل واحد ٍة منها‪ ،‬وبذلك يتمكَّن من زيادة س‪ww‬رعة الت‪ww‬ابع ‪ findEntry‬والتواب‪ww‬ع‬

‫التي تعتمد عليه‪.‬‬

‫انظر إىل تعريف الصنف‪:‬‬

‫{ >‪public class MyBetterMap<K, V> implements Map<K, V‬‬

‫;‪protected List<MyLinearMap<K, V>> maps‬‬

‫{ )‪public MyBetterMap(int k‬‬


‫;)‪makeMaps(k‬‬
‫}‬

‫{ )‪protected void makeMaps(int k‬‬


‫;)‪maps = new ArrayList<MyLinearMap<K, V>>(k‬‬
‫هياكل البيانات للمبرمجين‬ ‫التعمية ‪Hashing‬‬

‫{ )‪for (int i=0; i<k; i++‬‬


‫;))(>‪maps.add(new MyLinearMap<K, V‬‬
‫}‬
‫}‬
‫}‬

‫‪w‬ات تنتمي إىل الص‪ww‬نف ‪ ،MyLinearMap‬حيث يَس‪ww‬تق ِبل الب‪ww‬اني‬


‫يُمثِل متغ‪ww‬ير النس‪ww‬خة ‪ maps‬تجميع‪ww‬ة كائن‪ٍ w‬‬

‫نش ‪w‬ئ الت‪ww‬ابع ‪makeMaps‬‬


‫المستخدَمة مبدئ ًيا عىل األق‪ww‬ل‪ ،‬ثم يُ ِ‬
‫‪ constructor‬المعاملَ ‪ k‬الذي يُحدِّد عدد الخرائط ُ‬
‫تلك الخرائط ويُخزِّنها في قائمةٍ من النوع ‪.ArrayList‬‬

‫واآلن‪ ،‬س‪ww‬نحتاج إىل طريق‪ww‬ة تُمكِّنن‪ww‬ا من فحص مفت‪ww‬اح معين‪ ،‬وتقري‪ww‬ر الخريط‪ww‬ة ال‪ww‬تي ينبغي أن نَس‪ww‬تخدِمها‪.‬‬

‫وعندما نَستدعِ ي التابع ‪ put‬مع مفتاح جديد‪ ،‬سنختار إحدى الخرائط؛ أما عن‪ww‬دما نَس‪ww‬تدعِ ي الت‪ww‬ابع ‪ get‬م‪ww‬ع نفس‬

‫المفتاح‪ ،‬فعلينا أن نتذ ّكر الخريطة التي وضعنا فيها المفتاح‪.‬‬

‫يُمكِننا إجراء ذلك باختيار إحدى الخرائط الفرعية عشوائ ًيا وتع ّقب المكان الذي وضعنا فيه ك‪ww‬ل مفت‪ww‬اح‪ ،‬ولكن‬
‫كيف سنفعل ذلك؟ يُمكِننا مثاًل أن نَس‪ww‬تخدِم خريط‪ً w‬‬
‫‪w‬ة من الن‪ww‬وع ‪ Map‬للبحث عن المفت‪ww‬اح والعث‪ww‬ور عىل الخريط‪ww‬ة‬

‫المستخدَمة‪ ،‬ولكن الهدف األساسي من ه‪ww‬ذا التم‪ww‬رين ه‪ww‬و كتاب‪ww‬ة تنفي‪ٍ w‬ذ ذي كف‪ww‬اء ٍة عالي‪ww‬ةٍ للواجه‪ww‬ة ‪،Map‬‬
‫الفرعية ُ‬
‫وعليه‪ ،‬ال يُمكِننا أن نفترض وجود ذلك التنفيذ فعل ًيا‪.‬‬

‫دالة تعمي‪ً w‬‬


‫‪w‬ة ‪ hash function‬تَس‪ww‬تق ِبل كائنً‪ww‬ا من الن‪ww‬وع ‪ ،Object‬وتعي‪ww‬د‬ ‫ً‬ ‫بداًل من ذلك‪ ،‬يُمكِننا أن نَستخدِم‬

‫عددًا صحيحًا يُعرَف باسم شيفرة التعمية ‪ .hash code‬األهم من ذلك ه‪ww‬و أنن‪ww‬ا عن‪ww‬دما نقاب‪ww‬ل نفس الك‪ww‬ائن م‪ww‬ر ًة‬

‫دائم‪ w‬ا‪ .‬بتل‪ww‬ك الطريق‪ww‬ة‪ ،‬إذا اس‪ww‬تخدمنا ش‪ww‬يفرة التعمي‪ww‬ة لتخ‪ww‬زين‬


‫ً‬ ‫أخرى‪ ،‬فال ب ُ ّد أن تعيد الدالة نفس شيفرة التعمية‬

‫معين‪ ،‬فإننا سنحصل عىل نفس شيفرة التعمية إذا أردنا استرجاعه‪.‬‬
‫ٍ‬ ‫مفتاح‬
‫ٍ‬

‫حسب هذا التابع شيفرة التعمية‬


‫كائن من النوع ‪ Object‬بلغة جافا تابعً ا اسمه ‪ ،hashCode‬حيث يَ ِ‬
‫ٍ‬ ‫يُو ِّفر أ ّ‬
‫ي‬

‫الخاصة بالكائن‪ .‬يختلف تنفيذ هذا التابع باختالف نوع الكائن‪ ،‬وسنرى مثااًل عىل ذلك الح ًقا‪.‬‬

‫معين‪:‬‬
‫ٍ‬ ‫لمفتاح‬
‫ٍ‬ ‫الخريطة الفرع ّي َة المناسبة‬
‫َ‬ ‫يختار التابعُ المساع ُد التالي‬

‫{ )‪protected MyLinearMap<K, V> chooseMap(Object key‬‬


‫;‪int index = 0‬‬
‫{ )‪if (key != null‬‬
‫;)(‪index = Math.abs(key.hashCode()) % maps.size‬‬
‫}‬
‫;)‪return maps.get(index‬‬
‫}‬

‫‪92‬‬
‫هياكل البيانات للمبرمجين‬ ‫التعمية ‪Hashing‬‬

‫إذا كان ‪ key‬يساوي ‪ ،null‬فإننا سنختار الخريطة الفرعي‪ww‬ة الموج‪ww‬ودة في الفه‪ww‬رس ‪ 0‬عش‪ww‬وائ ًيا‪ .‬وفيم‪ww‬ا ع‪ww‬دا‬

‫صحيح‪ ،‬ثم نُطبِّق علي‪ww‬ه الت‪ww‬ابع ‪ Math.abs‬لكي نتأ ّك‪ww‬د‬


‫ٍ‬ ‫ذلك‪ ،‬سنَستخدِم التابع ‪ hashCode‬لكي نحصل عىل عد ٍد‬

‫من أنه ال يحتوي عىل قيمة سالبة‪ ،‬ثم نَستخدِم عام‪ww‬ل ب‪ww‬اقي القس‪ww‬مة ‪ \%‬لكي نحص‪ww‬ل عىل قيم‪ww‬ةٍ واقع‪ww‬ةٍ بين ‪ 0‬و‬

‫دائم‪ w‬ا‪ .‬وفي‬


‫ً‬ ‫فهرسا صالحًا لالستخدام م‪ww‬ع التجميع‪ww‬ة ‪maps‬‬
‫ً‬ ‫نضمن أن يكون ‪index‬‬
‫َ‬ ‫‪ ،maps.size()-1‬وبذلك‬

‫األخير‪ ،‬سيعيد ‪ chooseMap‬مرجًعا ‪ reference‬إىل الخريطة المختارة‪.‬‬

‫معين‪ ،‬يُفترَض أن‬


‫ٍ‬ ‫مفتاح‬
‫ٍ‬ ‫الحِ ظ أننا استدعينا ‪ chooseMap‬بالتابعين ‪ put‬و‪ ،get‬وبالتالي عندما نبحث عن‬

‫حتما‪ ،‬ألن‪ww‬ه‬
‫ً‬ ‫نحصل عىل نفس الخريطة التي حصلنا عليها عندما أضفنا ذلك المفتاح‪ .‬نقول هنا أنه يُفترَض وليس‬

‫من المحتمل أال يحدث‪ ،‬وهو ما سنشرح أسبابه الح ًقا‪.‬‬

‫انظر إىل تعريف التابعين ‪ put‬و ‪:get‬‬

‫{ )‪public V put(K key, V value‬‬


‫;)‪MyLinearMap<K, V> map = chooseMap(key‬‬
‫;)‪return map.put(key, value‬‬
‫}‬

‫{ )‪public V get(Object key‬‬


‫;)‪MyLinearMap<K, V> map = chooseMap(key‬‬
‫;)‪return map.get(key‬‬
‫}‬

‫ٌ‬
‫بسيطة للغاية‪ ،‬حيث يَستدعِ ي التابع‪ww‬ان الت‪ww‬ابعَ ‪ chooseMap‬للعث‪ww‬ور عىل الخريط‪ww‬ة‬ ‫ربما الحظت أن الشيفرة‬

‫الفرعية الصحيحة‪ ،‬ثم يَستدعِ يان تابعً‪ w‬ا في تل‪ww‬ك الخريط‪ww‬ة الفرعي‪ww‬ة‪ ،‬وه‪ww‬ذا ك‪ww‬لّ م‪ww‬ا في األم‪ww‬ر‪ .‬واآلن‪ ،‬لنفحص أداء‬

‫التابعين‪.‬‬

‫قس ًما عىل عدد مقداره ‪ k‬من الخرائط الفرعي‪ww‬ة‪ ،‬فسيص‪ww‬بح ل‪ww‬دينا‬
‫الم ْدخَالت ُم ّ‬
‫إذا كان لدينا عدد مقداره ‪ n‬من ُ‬
‫الم ْدخَالت في ك‪ww‬ل خريط‪ww‬ة‪ .‬وعن‪ww‬دما نبحث عن مفت‪ww‬اح معين‪ ،‬سنض‪ww‬طرّ إىل حس‪ww‬اب‬
‫في المتوسط عدد ‪ n/k‬من ُ‬
‫شيفرة تعميته‪ ،‬والتي تستغرق بعض الوقت‪ ،‬ثم سنبحث في الخريطة الفرعية المقابلة‪.‬‬

‫الم‪ْ w‬دخَالت في الص‪ww‬نف ‪ MyBetterMap‬أق‪ww‬لّ بمق‪w‬دار ‪ k‬م‪ww‬رة من حجمه‪ww‬ا في الص‪ww‬نف‬


‫لما كان حجم ق‪ww‬وائم ُ‬
‫ّ‬
‫الم َّ‬
‫توقع أن يكون البحث أسر ع بمقدار ‪ k‬مرة‪ ،‬ومع ذلك‪ ،‬ما يزال زمن التش‪ww‬غيل متناس ‪w‬بًا‬ ‫‪ ،MyLinearMap‬فمن ُ‬
‫مع ‪ ،n‬وبالتالي ما يزال الصنف ‪ MyBetterMap‬خط ًّيا‪ .‬سنعالج تلك المشكلة في التمرين التالي‪.‬‬

‫‪93‬‬
‫هياكل البيانات للمبرمجين‬ ‫التعمية ‪Hashing‬‬

‫‪ 10.2‬كيف تعمل التعمية؟‬


‫إذا طبَّقنا دالة تعميةٍ عىل نفس الكائن‪ ،‬فال ب ُ ّد لها أن تنتج نفس شيفرة التعمية في كل مرّةٍ‪ ،‬وه‪ww‬و أم‪w‬ر ٌ س‪ww‬هلٌ‬

‫قابل للتعديل ‪immutable‬؛ أما إذا كان قاباًل للتعديل‪ ،‬فاألمر يحتاج إىل بعض التفكير‪.‬‬
‫ٍ‬ ‫نوعً ا ما إذا كان الكائن غيرَ‬

‫كمثال عىل الكائنات غ‪w‬ير القابل‪w‬ة للتع‪w‬ديل‪ ،‬س‪w‬نُعرِّف الص‪w‬نف ‪ ،SillyString‬حيث يُغلِّف ذل‪w‬ك الص‪w‬نف‬

‫سلسلة نص ّي ًة من النوع ‪:String‬‬


‫ً‬

‫{ ‪public class SillyString‬‬


‫;‪private final String innerString‬‬

‫{ )‪public SillyString(String innerString‬‬


‫;‪this.innerString = innerString‬‬
‫}‬

‫{ )(‪public String toString‬‬


‫;‪return innerString‬‬
‫}‬

‫في الواقع‪ ،‬هذا الصنف ليس ذا فائد ٍة كبيرةٍ‪ ،‬ولهذا السبب سميناه ‪ ،SillyString‬ولكنه مع ذلك يُ ِّ‬
‫وض‪ww‬ح‬

‫الخاصة به‪:‬‬
‫ّ‬ ‫لصنف أن يُعرِّف دالة التعمية‬
‫ٍ‬ ‫كيف يُمكِن‬

‫‪@Override‬‬
‫{ )‪public boolean equals(Object other‬‬
‫;))(‪return this.toString().equals(other.toString‬‬
‫}‬

‫‪@Override‬‬
‫{ )(‪public int hashCode‬‬
‫;‪int total = 0‬‬
‫{ )‪for (int i=0; i<innerString.length(); i++‬‬
‫;)‪total += innerString.charAt(i‬‬
‫}‬
‫;‪return total‬‬
‫}‬

‫َ‬
‫تعريف التابعين ‪ equals‬و‪ ،hashCode‬وهذا األمر ضروري ألننا ل‪ww‬و أردن‪ww‬ا ل‪ww‬ه‬ ‫أعاد الصنف ‪SillyString‬‬

‫عمل بشكل مناسب‪ ،‬فال ب ُ ّد أن يكون التابع ‪ equals‬متواف ًقا مع الت‪ww‬ابع ‪ .hashCode‬يَعنِي ه‪w‬ذا أن‪ww‬ه ل‪w‬و ك‪ww‬ان‬
‫أن يَ َ‬

‫‪94‬‬
‫هياكل البيانات للمبرمجين‬ ‫التعمية ‪Hashing‬‬

‫لدينا كائنان متساويين ‪-‬يُعيد الت‪w‬ابع ‪ equals‬القيم‪w‬ة ‪ true‬عن‪w‬د تطبيق‪ww‬ه عليهم‪w‬ا‪ ،-‬فال ب ُ‪ّ w‬د أن تك‪w‬ون لهم‪w‬ا نفس‬

‫شيفرة التعمية‪ ،‬ولكن هذا صحيحٌ من اتجا ٍه واح ٍد فقط‪ ،‬فمن المحتم‪ww‬ل أن يمل‪ww‬ك كائن‪ww‬ان نفس ش‪ww‬يفرة التعمي‪ww‬ة‪،‬‬

‫ومع ذلك ال يكونان متساويين‪.‬‬

‫يَستدعِ ي ‪ equals‬التابعَ ‪ toString‬الذي يعي‪ww‬د قيم‪َ w‬‬


‫‪w‬ة متغ‪ww‬ير النس‪ww‬خة ‪ ،innerString‬ول‪ww‬ذلك يتس‪ww‬اوى‬

‫المعرَّف فيهما‪.‬‬
‫كائنان من النوع ‪ SillyString‬إذا تساوى متغير النسخة ‪ُ innerString‬‬

‫حس‪w‬ب حاص‪ww‬ل مجموعه‪ww‬ا‪.‬‬


‫يمرّ الت‪ww‬ابع ‪ hashCode‬ع‪ww‬بر مح‪ww‬ارف السلس‪ww‬لة النص‪ww‬ية ‪-‬من الن‪ww‬وع ‪ -String‬ويَ ِ‬

‫‪w‬حيح باس‪ww‬تخدام رقم‬


‫صحيح من الن‪ww‬وع ‪ ،int‬س‪w‬تُح ِّول جاف‪ww‬ا المح‪w‬رف إىل ع‪ww‬د ٍد ص‪ٍ w‬‬
‫ٍ‬ ‫وعندما نضيف محر ًفا إىل عد ٍد‬

‫مكنك ق‪ww‬راءة المزي‪ww‬د عن أرق‪ww‬ام مح‪ww‬ارف اليونيك‪ww‬ود (باللغ‪ww‬ة‬


‫ِ‬ ‫الخاص به‪ .‬يُ‬
‫ّ‬ ‫محرف يونيكود ‪Unicode code point‬‬

‫ي لفهم هذا المثال‪.‬‬


‫اإلنجليزية) إذا أردت‪ ،‬ولكنه غير ضرور ٍّ‬

‫الشرط التالي‪:‬‬
‫َ‬ ‫تُح ِّقق دال ّ ُة التعمية السابقة‬

‫إذا احتوى كائنان من النوع ‪ SillyString‬عىل سالسلَ نص ّيةٍ متساوية‪ ،‬فإنهما يحصالن عىل نفس شيفرة‬

‫التعمية‪.‬‬

‫تَ َ‬
‫عمل الشيفرة السابقة بشكل صحيح‪ ،‬ولكنها ليست بالكفاءة المطلوبة؛ فهي تعي‪ww‬د ش‪ww‬يفرة التعمي‪ww‬ة نفس‪ww‬ها‬

‫لعدد كبير من السالسل النصية المختلفة؛ فمثاًل لو تك َّونت سلسلتان من نفس األحرف مهما كان ترتيبها‪ ،‬فإنهم‪ww‬ا‬

‫ستحصالن عىل نفس شيفرة التعمية‪ ،‬ب‪ww‬ل ح‪ww‬تى ل‪ww‬و لم تتكون‪ww‬ا من نفس األح‪ww‬رف‪ ،‬فق‪ww‬د يك‪ww‬ون حاص‪ww‬ل المجم‪ww‬وع‬

‫متساويًا مثل ‪ ac‬و‪.bb‬‬

‫كائنات كثير ٌة عىل نفس شيفرة التعمية‪ ،‬فإنها ستُخزَّن في نفس الخريط‪ww‬ة الفرعي‪ww‬ة‪ ،‬وإذا احت‪ww‬وت‬
‫ٌ‬ ‫إذا حصلت‬

‫خرائط فرع ّي ٌة معين ٌّة عىل ُمدخ ٍ‬


‫َالت أكثرَ من غيرها‪ ،‬فإن السرعة التي نُح ّققها باستخدام عدد مقداره ‪ k‬من الخرائ‪ww‬ط‬
‫ً‬
‫منتظمة‪ ،‬أي ال ب ُ ّد أن تك‪ww‬ون احتمالي‪ww‬ة الحص‪ww‬ول عىل‬ ‫تكون أقلّ بكثير ٍ من ‪ ،k‬ولذلك ينبغي أن تكون دوال التعمية‬

‫أي قيمةٍ ضمن النطاق المسموح به متساوية‪ .‬يُمكنك قراءة المزيد عن التصميم الج ّيد لدوال التعمية لو أردت‪.‬‬

‫‪ 10.3‬التعمية والقابلية للتغيري ‪mutation‬‬


‫قابل للتعديل‪ ،‬وكذلك الصنف ‪SillyString‬؛ وذلك ألننا صرحنا عنه باستخدام‬
‫ٍ‬ ‫يُعَ د الصنف ‪ String‬غير‬

‫نش‪ww‬ئ كائنً‪ww‬ا من الن‪ww‬وع ‪ ،SillyString‬فإن‪ww‬ك ال تس‪ww‬تطيع أن تُع‪ww‬دِّل متغ‪ww‬ير النس‪ww‬خة‬


‫‪ .final‬بمج‪ww‬رد أن تُ ِ‬

‫المعرَّف فيه لتجعله يشير إىل سلسلةٍ نص ّيةٍ مختلفةٍ من النوع ‪ ،String‬كم‪ww‬ا أن‪ww‬ك ال تس‪ww‬تطيع‬
‫‪ُ innerString‬‬
‫دائما‪.‬‬
‫ً‬ ‫أن تُعدِّل السلسلة النص ّي َة التي يشير إليها‪ ،‬وبالتالي ستكون للكائن نفس شيفرة التعمية‬

‫ولكن‪ ،‬ماذا يحدث لو كان الكائن قاباًل للتعديل؟ انظر إىل تعريف الص‪ww‬نف ‪ SillyArray‬المط‪َ w‬‬
‫‪w‬ابق للص‪ww‬نف‬

‫محارف بداًل من الصنف ‪:String‬‬


‫َ‬ ‫َ‬
‫مصفوفة‬ ‫‪ SillyString‬باستثناء أنه يَستخدِم‬

‫‪95‬‬
‫هياكل البيانات للمبرمجين‬ Hashing ‫التعمية‬

public class SillyArray {


private final char[] array;

public SillyArray(char[] array) {


this.array = array;
}

public String toString() {


return Arrays.toString(array);
}

@Override
public boolean equals(Object other) {
return this.toString().equals(other.toString());
}

@Override
public int hashCode() {
int total = 0;
for (int i=0; i<array.length; i++) {
total += array[i];
}
System.out.println(total);
return total;
}

َ َ‫ الذي ي‬setChar ‫ التابع‬SillyArray ‫يُو ِّفر الصنف‬


:‫سمح بتعديل المحارف الموجودة في المصفوفة‬

public void setChar(int i, char c) {


this.array[i] = c;
}

:‫ ثم أضفناه إىل خريطةٍ كالتالي‬،SillyArray ‫ لنفترض أننا أنشأنا كائنًا من النوع‬،‫واآلن‬

SillyArray array1 = new SillyArray("Word1".toCharArray());


map.put(array1, 1);

96
‫هياكل البيانات للمبرمجين‬ ‫التعمية ‪Hashing‬‬

‫ش‪wwww‬يفرة التعمي‪wwww‬ة لتل‪wwww‬ك المص‪wwww‬فوفة هي ‪ .461‬واآلن إذا ع‪wwww‬دَّلنا محتوي‪wwww‬ات المص‪wwww‬فوفة‪ ،‬وحاولنا‬

‫أن نسترجعها كالتالي‪:‬‬

‫;)'‪array1.setChar(0, 'C‬‬
‫;)‪Integer value = map.get(array1‬‬

‫ستكون شيفرة التعمية بعد التعديل ق‪ww‬د أص‪ww‬بحت ‪ .441‬بحص‪ww‬ولنا عىل ش‪ww‬يفرة تعمي‪ww‬ةٍ مختلف‪ww‬ةٍ ‪ ،‬فإنن‪ww‬ا غالبً‪ww‬ا‬

‫سنبحث في الخريطة الفرع ّية الخاطئة‪ ،‬وبالتالي لن نع‪ww‬ثر عىل المفت‪ww‬اح عىل ال‪ww‬رغم من أن‪ww‬ه موج‪ww‬ود في الخريط‪ww‬ة‪،‬‬

‫سيء‪.‬‬
‫ّ‬ ‫وهذا أمر ٌ‬

‫ال يُعَ‪ ww‬د اس‪ww‬تخدام الكائن‪ww‬ات القابل‪ww‬ة للتع‪ww‬ديل مفاتيحً‪ww‬ا لهياك‪ww‬ل البيان‪ww‬ات المبني‪ww‬ة عىل التعمي‪ww‬ة ‪-‬مث‪ww‬ل‬

‫‪ MyBetterMap‬و‪ -HashMap‬حاًّل آمنًا‪ ،‬فإذا كنت متأ ّكدًا من أن قيم المف‪ww‬اتيح لن تتع ‪w‬دّل بينم‪ww‬ا هي ُمس‪ww‬تخدَمة‬

‫تعديل يُجرَى عليها لن يؤثر عىل شيفرة التعمية؛ فلربم‪ww‬ا يك‪ww‬ون اس‪ww‬تخدامها مناس ‪w‬بًا‪ ،‬ولكن‬
‫ٍ‬ ‫في الخريطة‪ ،‬أو أن أي‬

‫دائما أن تتجنَّب ذلك‪.‬‬


‫ً‬ ‫من األفضل‬

‫‪ 10.4‬تمرين ‪8‬‬
‫ُنهي في هذا التمرين تنفيذ الصنف ‪ ،MyBetterMap‬حيث ستجد ملفات شيفرة التمرين في مس‪ww‬تودع‬
‫ست ِ‬
‫الكتاب‪:‬‬

‫‪ :MyLinearMap.java‬يحتوي عىل حل تمرين الفصل السابق الواجهة ‪ Map‬الذي س‪ww‬نبني علي‪ww‬ه ه‪ww‬ذا‬ ‫•‬

‫التمرين‪.‬‬

‫‪ :MyBetterMap.java‬يحتوي عىل شيفرة من نفس الفصل مع إضافة بعض التوابع التي يُف‪ww‬ترَض أن‬ ‫•‬

‫تُكملها‪.‬‬

‫لجدول ينمو عند الضرورة‪.‬‬


‫ٍ‬ ‫مبدئي ‪-‬عليك إكماله‪-‬‬
‫ٍّ‬ ‫‪ :MyHashMap.java‬يحتوي عىل تص ّورٍ‬ ‫•‬

‫اختبارات وحد ٍة للصنف ‪.MyLinearMap‬‬


‫ِ‬ ‫‪ :MyLinearMapTest.java‬يحتوي عىل‬ ‫•‬

‫‪ :MyBetterMapTest.java‬يحتوي عىل اختبارات وحد ٍة للصنف ‪.MyBetterMap‬‬ ‫•‬

‫‪ :MyHashMapTest.java‬يحتوي عىل اختبارات وحد ٍة للصنف ‪.MyHashMap‬‬ ‫•‬

‫‪ :Profiler.java‬يحتوي عىل شيفر ٍة لقياس األداء ورسم تأثير حجم المشكلة عىل زمن التشغيل‪.‬‬ ‫•‬

‫‪ :ProfileMapPut.java‬يحتوي عىل شيفرة تقيس أداء التابع ‪.Map.put‬‬ ‫•‬

‫‪ ant‬لكي تُص‪wwwww‬رِّف ملف‪wwwww‬ات الش‪wwwww‬يفرة‪ ،‬ثم األمر‬ ‫كالع‪wwwww‬ادة‪ ،‬علي‪wwwww‬ك أن تُن ِّفذ األم‪wwwww‬ر ‪build‬‬

‫‪ .ant MyBetterMapTest‬ستفشل العديد من االختبارات؛ وذلك ألنه ما يزال عليك إكمال بعض التوابع‪.‬‬

‫‪97‬‬
‫هياكل البيانات للمبرمجين‬ ‫التعمية ‪Hashing‬‬

‫راج‪wwww‬ع تنفي‪wwww‬ذ الت‪wwww‬ابعين ‪ put‬و‪ get‬من ذات الفص‪wwww‬ول المش‪wwww‬ار إليه‪wwww‬ا في األعىل‪ ،‬ثم أكم‪wwww‬ل متن‬

‫الت‪wwwww‬ابع ‪ .containsKey‬س‪wwwww‬يكون علي‪wwwww‬ك اس‪wwwww‬تخدام الت‪wwwww‬ابع ‪ ،chooseMap‬وبع‪wwwww‬دما تنتهي ن ِّفذ األمر‬


‫‪ً ant MyBetterMapTest‬‬
‫مرة أخرى‪ ،‬وتأ ّكد من نجاح ‪.testContainsKey‬‬

‫أكم‪wwwwww‬ل متن الت‪wwwwww‬ابع ‪ ،containsValue‬وال تَس‪wwwwww‬تخدِم ل‪wwwwww‬ذلك الت‪wwwwww‬ابع ‪ .chooseMap‬ن ِّفذ األمر‬
‫‪ً ant MyBetterMapTest‬‬
‫مرة أخرى‪ ،‬وتأ ّكد من نجاح ‪ .testContainsValue‬الحِ ظ أن العثور عىل القيم‪ww‬ةِ‬

‫يتطلَّب عماًل أكبر من العثور عىل المفتاح‪.‬‬

‫يُعَ د تنفيذ الت‪ww‬ابع ‪ containsKey‬تنفي‪ً w‬ذا خط ًّيا مث‪ww‬ل الت‪ww‬ابعين ‪ put‬و‪get‬؛ ألن‪ww‬ه علي‪ww‬ه أن يبحث في إح‪w‬دى‬

‫الخرائط الفرعية‪ .‬سنشرح في الفصل التالي كيف يُمكِننا تحسين هذا التنفيذ أكثر‪.‬‬

‫‪98‬‬
‫‪ .11‬الواجهة ‪HashMap‬‬

‫َّ‬
‫وتوقعنا أن يك‪ww‬ون ذل‪ww‬ك التنفي‪ww‬ذ‬ ‫كتبنا تنفي ًذا للواجهة ‪ Map‬باستخدام التعمية ‪ hashing‬في الفصل السابق‪،‬‬

‫خط ًّيا‪.‬‬
‫أسر ع ألن القوائم التي يبحث فيها أقصر‪ ،‬ولكن ما يزال ترتيب نمو ‪ order of growth‬ذلك التنفيذ ّ‬

‫الم ْدخَالت وعدد مقداره ‪ k‬من الخرائط الفرعية ‪ ،sub-maps‬فإن حجم تل‪ww‬ك‬
‫إذا كان هناك عدد مقداره ‪ n‬من ُ‬
‫ساوي ‪ n/k‬في المتوسط‪ ،‬أي ما يزال متناسبًا مع ‪ ،n‬ولكننا لو زدنا ‪ k‬مع ‪ ،n‬سنتمكَّن من الح ِّ‬
‫د من حجم‬ ‫الخرائط يُ ِ‬
‫‪ .n/k‬لنفترض عىل سبيل المثال أننا سنضاعف قيم‪ww‬ة ‪ k‬في ك‪ww‬لّ م‪w‬رّ ٍة تتج‪w‬اوز فيه‪ww‬ا ‪ n‬قيم‪َ w‬‬
‫‪w‬ة ‪ .k‬في تل‪w‬ك الحال‪w‬ة‪،‬‬
‫الم ْدخَالت في كلّ خريطةٍ أقلّ من ‪ 1‬في المتوسط‪ ،‬وأقلّ من ‪ 10‬عىل األغلب بش‪ww‬رط أن تُ‪ww‬وزِّ ع دال ّ‪ُ w‬‬
‫‪w‬ة‬ ‫سيكون عدد ُ‬
‫بشكل معقول‪.‬‬
‫ٍ‬ ‫التعميةِ المفاتيحَ‬

‫الم ْدخَالت في كلّ خريطةٍ فرع ّيةٍ ثابتًا‪ ،‬سنتمكَّن من البحث فيه‪ww‬ا ب‪ٍ w‬‬
‫‪w‬زمن ث‪ww‬ابت‪ .‬عالوة عىل ذل‪ww‬ك‪،‬‬ ‫إذا كان عدد ُ‬
‫يَستغرِق حساب دالة التعمية في العم‪w‬وم زمنً‪w‬ا ثابتً‪w‬ا (ق‪w‬د يعتم‪w‬د عىل حجم المفت‪w‬اح‪ ،‬ولكن‪w‬ه ال يعتم‪w‬د عىل ع‪w‬دد‬

‫المفاتيح)‪ .‬بنا ًء عىل ما سبق‪ ،‬ستَستغرِق توابع ‪ Map‬األساسية أي ‪ put‬و ‪ get‬زمنًا ثابتًا‪.‬‬

‫سنفحص تفاصيل ذلك في التمرين التالي‪.‬‬

‫‪ 11.1‬تمرين ‪9‬‬
‫و َّفرنا التصور المبدئي لجدول تعمية ‪ hash table‬ينمو عند الضرورة في الملف ‪ .MyHashMap.java‬انظر‬

‫إىل بداية تعريفه‪:‬‬

‫‪public class MyHashMap<K, V> extends MyBetterMap<K, V> implements‬‬


‫{ >‪Map<K, V‬‬

‫الم ْد َخلات المسموح بها في كل خريطة فرعية قبل إعادة حساب شيفرات التعمية ‪//‬‬
‫متوسط عدد ُ‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪HashMap‬‬

‫;‪private static final double FACTOR = 1.0‬‬

‫‪@Override‬‬
‫{ )‪public V put(K key, V value‬‬
‫;)‪V oldValue = super.put(key, value‬‬

‫تأ ّكد مما إذا كان عدد العناصر في الخريطة الفرعية قد تجاوز الحد الأقصى ‪//‬‬
‫{ )‪if (size() > maps.size() * FACTOR‬‬
‫;)(‪rehash‬‬
‫}‬
‫;‪return oldValue‬‬
‫}‬
‫}‬

‫المعرَّف‪ww‬ة في‪ww‬ه‪ .‬يعي‪ww‬د‬


‫يمت ّد الصنف ‪ MyHashMap‬من الصنف ‪ ،MyBetterMap‬وبالتالي‪ ،‬فإنه يَ‪ww‬رِث التواب‪ww‬ع ُ‬
‫تعري‪ww‬ف الت‪ww‬ابع ‪ ،put‬ف َيس‪ww‬تدعِ ي أواًل الت‪ww‬ابع ‪ put‬في الص‪ww‬نف األعىل ‪- superclass‬أي‬
‫َ‬ ‫الص‪ww‬نف ‪MyHashMap‬‬

‫المعرَّفة في الص‪ww‬نف ‪ ،MyBetterMap‬ثم يَفحَص م‪ww‬ا إذا ك‪ww‬ان علي‪ww‬ه أن يُع ّي‪ww‬د حس‪ww‬اب ش‪ww‬يفرة‬
‫يَستدعِ ي النسخة ُ‬
‫الم ْدخَالت الكلية ‪ n‬بينما يعيد التابع ‪ maps.size‬عدد الخرائط ‪.k‬‬
‫التعمية‪ .‬يعيد التابع ‪ size‬عدد ُ‬

‫يُحدّد الثابت ‪- FACTOR‬الذي يُطلَق علي‪ww‬ه اس‪ww‬م عام‪ww‬ل الحمول‪ww‬ة ‪ -load factor‬الع‪ww‬دد األقص‪ww‬ى ُ‬
‫للم‪ْ w‬دخَالت‬

‫وسط ًّيا في كل خريطة فرعية‪ .‬إذا تح َّقق الشرط ‪ ،n > k * FACTOR‬فهذا يَعنِي أن الشرط ‪n/k > FACTOR‬‬

‫الم ْدخَالت في كلّ خريطةٍ فرع ّيةٍ قد تج‪ww‬اوز الح‪ww‬د األقص‪ww‬ى‪ ،‬ول‪ww‬ذلك‪ ،‬يك‪ww‬ون علين‪ww‬ا‬ ‫ُمتح ِّقق ً‬
‫أيضا‪ ،‬مما يَعنِي أن عدد ُ‬
‫استدعاء التابع ‪.rehash‬‬

‫‪ .ant‬ستفش‪ww‬ل‬ ‫ن ِّفذ األم‪w‬ر ‪ ant build‬لتص‪ww‬ريف ملف‪ww‬ات الش‪ww‬يفرة‪ ،‬ثم ن ِّفذ األم‪w‬ر ‪MyHashMapTest‬‬

‫اعتراض ‪ ،exception‬ودورك هو أن تكمل متن هذا التابع‪.‬‬


‫ٍ‬ ‫االختبارات ألن تنفيذ التابع ‪ rehash‬يُبلِّغ عن‬

‫الم ْدخَالت الموجودة في الجدول‪ .‬بعد ذلك‪ ،‬عليه أن يض‪w‬بُط حجم‬ ‫إ ًذا أكمل متن التابع ‪ rehash‬بحيث يُ ِّ‬
‫جمع ُ‬
‫الم‪ْ w‬دخَالت إلي‪ww‬ه م‪ww‬ر ًة أخ‪ww‬رى‪ .‬و َّفرن‪ww‬ا ت‪ww‬ابعين مس‪ww‬اعدين هم‪ww‬ا ‪MyBetterMap.makeMaps‬‬
‫الج‪ww‬دول‪ ،‬ويض‪ww‬يف ُ‬
‫و ‪ .MyLinearMap.getEntries‬ينبغي أن يُضاعِ ف حلك عدد الخرائط ‪ k‬في كل مرة يُستد َعى فيها التابع‪.‬‬

‫‪ 11.2‬تحليل الصنف ‪MyHashMap‬‬


‫ً‬
‫متناسبة م‪ww‬ع ‪ ،n‬ف‪ww‬إن‬ ‫الم ْدخَالت في أكبر ِ خريطةٍ فرع ّيةٍ متناسبًا مع ‪ ،n/k‬وكانت الزيادة بقيمة ‪k‬‬
‫إذا كان عدد ُ‬
‫العديد من التوابع األساسية في الصنف ‪ MyBetterMap‬تُص ِبح ثابتة الزمن‪:‬‬

‫‪100‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪HashMap‬‬

‫{ )‪public boolean containsKey(Object target‬‬


‫;)‪MyLinearMap<K, V> map = chooseMap(target‬‬
‫;)‪return map.containsKey(target‬‬
‫}‬

‫{ )‪public V get(Object key‬‬


‫;)‪MyLinearMap<K, V> map = chooseMap(key‬‬
‫;)‪return map.get(key‬‬
‫}‬

‫{ )‪public V remove(Object key‬‬


‫;)‪MyLinearMap<K, V> map = chooseMap(key‬‬
‫;)‪return map.remove(key‬‬
‫}‬

‫َ‬
‫شيفرة التعمية للمفتاح‪ ،‬وهو ما يَستغِ رق زمنًا ثابتًا‪ ،‬ثم يَس‪w‬تدعِ ي تابعً‪ w‬ا عىل خريط‪w‬ةٍ فرع ّي‪w‬ةٍ ‪،‬‬ ‫تابع‬
‫ٍ‬ ‫حسب كلّ‬
‫يَ ِ‬
‫وهو ما يَستغِ رق ً‬
‫أيضا زمنًا ثابتًا‪.‬‬

‫ربما األمورُ جيد ٌة حتى اآلن‪ ،‬ولكن ما يزال من الصعب تحليل أداء التابع األساسي اآلخر ‪ ،put‬فه‪ww‬و يَس‪ww‬تغرِق‬

‫زمنًا ثابتًا إذا لم يضطرّ الستدعاء التابع ‪ ،rehash‬ويَستغرِق زمنًا خط ًيا إذا اضطرّ لذلك‪ .‬بتلك الطريقة‪ ،‬يك‪ww‬ون ه‪w‬ذا‬

‫التابع مشابهً ا للتابع ‪ ArrayList.add‬الذي حللنا أداءه في الفصل الثالث قائمة المصفوفة ‪.ArrayList‬‬

‫زمن متتاليةٍ من‬


‫ِ‬ ‫ولنفس السبب‪َّ ،‬‬
‫يتضح أن التابع ‪ MyHashMap.put‬يَستغرِق زمنًا ثابتًا إذا حسبنا متوسط‬

‫االس‪www‬تدعاءات‪ .‬يعتم‪ww‬د ه‪ww‬ذا التفس‪www‬ير عىل التحلي‪www‬ل بالتس‪www‬ديد ‪ amortized analysis‬ال‪www‬ذي ش‪www‬رحناه في‬

‫نفس الفصل‪.‬‬

‫‪w‬دئي للخرائ‪ww‬ط الفرعي‪ww‬ة ‪ k‬يس‪ww‬اوي ‪ ،2‬وأن عام‪ww‬ل التحمي‪ww‬ل يس‪ww‬اوي ‪ ،1‬واآلن‪ ،‬لنفحص‬
‫َّ‬ ‫لنفترض أن العدد المب‪w‬‬

‫الزمن الذي يَستغرِقه التابع ‪ put‬إلضافة متتاليةٍ من المفاتيح‪ .‬سنَ ُع ّد عدد المرات التي سنض‪w‬طرّ خالله‪w‬ا لحس‪w‬اب‬

‫عمل واحدة‪.‬‬
‫ٍ‬ ‫لمفتاح وإضافته لخريطةٍ فرع ّيةٍ ‪ ،‬وسيكون ذلك بمنزلةِ وحد ِة‬
‫ٍ‬ ‫شيفرة التعمية‬

‫أيضا عند استدعائه‬ ‫ً‬


‫واحدة من وحدات العمل‪ ،‬وس ُين ِّفذ ً‬ ‫عمل‬
‫ٍ‬ ‫س ُين ِّفذ التابع ‪ put‬عند استدعائه أل ّول مر ٍة وحدة‬

‫أما في المرة الثالثة‪ ،‬فسيضطرّ إلع‪ww‬ادة حس‪ww‬اب ش‪ww‬يفرات التعمي‪ww‬ة‪ ،‬وبالت‪ww‬الي‪،‬‬


‫عمل واحدةٍ‪ّ .‬‬
‫ٍ‬ ‫َ‬
‫وحدة‬ ‫في المرة الثانية‬

‫س ُين ِّفذ عدد ‪ 2‬من وحدات العمل لكي يَ ِ‬


‫حسب شيفرات تعمية المف‪ww‬اتيح الموج‪ww‬ودة بالفع‪ww‬ل باإلض‪ww‬افة إىل وح‪ww‬دة‬

‫عمل أخرى لحساب شيفرة تعمية المفتاح الجديد‪.‬‬


‫ٍ‬

‫‪101‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪HashMap‬‬

‫عمل واح‪ww‬دةٍ‪،‬‬
‫ٍ‬ ‫واآلن‪ ،‬أصبح حجم الجدول ‪ ،4‬وبالتالي‪ ،‬س ُين ِّفذ التابع ‪ put‬عند استدعائه في المرة التالية وحدة‬

‫ولكن‪ ،‬في المرة التالية التي سيضطرّ خاللها الستدعاء ‪ ،rehash‬فإنه س ُين ِّفذ ‪ 4‬وحدات عم‪ٍ w‬‬
‫‪w‬ل لحس‪ww‬اب ش‪ww‬يفرات‬

‫عمل إضافيةٍ للمفتاح الجديد‪.‬‬


‫ٍ‬ ‫تعميةِ المفاتيح الموجودة ووحدة‬

‫‪w‬اح جدي‪ٍ w‬د في األس‪ww‬فل‬ ‫تُ ِّ‬


‫وضح الصورة التالية هذا النمط‪ ،‬حيث يظهر العمل الالزم لحساب شيفرة تعمي‪ww‬ة مفت‪ٍ w‬‬
‫بينما يَظهَ ر العمل اإلضافي كبرج‪.‬‬

‫إذا أنزلنا األبراج كما تقترح األسهم‪ ،‬سيمأل كلّ واح ٍد منها الفراغ الموجود قبل ال‪ww‬برج الت‪ww‬الي‪ ،‬وسنحص‪ww‬ل عىل‬

‫ارتفاع منتظمٍ يساوي ‪ 2‬وحدة عمل‪ .‬يُ ِّ‬


‫وضح ذلك أن متوسط العمل لكل استدعا ٍء للت‪ww‬ابع ‪ put‬ه‪ww‬و ‪ 2‬وح‪ww‬دة عم‪ww‬ل‪،‬‬ ‫ٍ‬
‫مما يَعنِي أنه يَستغرِق زمنًا ثابتًا في المتوسط‪.‬‬

‫وضح الرسم البياني مدى أهمية مضاعفة عدد الخرائط الفرعية ‪ k‬عندما نعيد حساب شيفرات التعمية؛ فل‪ww‬و‬
‫يُ ِ‬
‫ً‬
‫ثابتة إىل ‪ k‬بداًل من مض‪ww‬اعفتها‪ ،‬س‪ww‬تكون األب‪ww‬راج قريب‪ww‬ة ج‪w‬دًا من بعض‪ww‬ها‪ ،‬وس‪ww‬تتراكم ف‪ww‬وق بعض‪ww‬ها‪،‬‬ ‫ً‬
‫قيمة‬ ‫أضفنا‬

‫وعندها‪ ،‬لن نحصل عىل زمن ثابت‪.‬‬

‫‪ 11.3‬مقايضات ما بني الزمن واألداء‬


‫رأينا أن التوابع ‪ containsKey‬و ‪ get‬و ‪ remove‬تَستغرِق زمنًا ثابتًا‪ ،‬وأن الت‪ww‬ابع ‪ put‬يَس‪ww‬تغرِق زمنً‪ww‬ا ثاب ًت‪ww‬ا‬

‫في المتوسط‪ ،‬وهذا أمر ٌ رائ ٌع بحق‪ ،‬فأداء تلك العمليات هو نفسه تقريبًا بغض النظر عن حجم الجدول‪.‬‬

‫بس‪w‬يط تَس‪w‬تغرِق ك‪w‬لّ وح‪w‬د ٍة عم‪w‬ل في‪w‬ه نفس مق‪w‬دار‬


‫ٍ‬ ‫يَعتمِ د تحليلنا ألداء تلك العمليات عىل نموذج معالج‪w‬ةٍ‬

‫الزمن‪ ،‬ولكن الحواسيب الحقيقية أعق ُد من ذلك بكثير‪ ،‬فتبلغ أقصى سرعتها عن‪ww‬دما تتعام‪ww‬ل م‪ww‬ع هياك‪ww‬ل بيان‪ww‬ات‬

‫ُوضع في الذاكرة المخبئية ‪ ،cache‬وتكون أبطأ قلياًل عندما ال يتناس‪ww‬ب حجم هياك‪ww‬ل البيان‪ww‬ات‬
‫صغيرة بما يكفي لت َ‬

‫مع الذاكرة المخبئية ولكن مع إمكانية وضعها في الذاكرة‪ ،‬وتكون أبط‪ww‬أ بكث‪ww‬ير ٍ إذا لم يتناس‪ww‬ب حجم الهياك‪ww‬ل ح‪ww‬تى‬

‫مع الذاكرة‪.‬‬

‫‪102‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪HashMap‬‬

‫الم‪ْ w‬دخَل قيم‪ً w‬‬


‫‪w‬ة وليس‬ ‫ٌ‬
‫مشكلة أخرى‪ ،‬وهي أن التعمية ال تك‪ww‬ون ذات فائ‪ww‬د ٍة في ه‪ww‬ذا التنفي‪ww‬ذ إذا ك‪ww‬ان ُ‬ ‫هنالك‬
‫ٌ‬
‫فعالة‬ ‫ٌ‬
‫طريقة‬ ‫خطيٌّ ألنه مضطرّ للبحث في كل الخرائط الفرعية‪ ،‬فليس هناك‬
‫حا‪ ،‬فالتابع ‪ّ containsValue‬‬
‫مفتا ً‬
‫للبحث عن قيمة ما والعثور عىل مفتاحها المقابل (أو مفاتيحها)‪.‬‬

‫باإلضافة إىل ما سبق‪ ،‬فإن بعض التوابع التي كانت تَس‪ww‬تغرِق زمنً‪ww‬ا ثاب ًت‪ww‬ا في الص‪ww‬نف ‪ MyLinearMap‬ق‪ww‬د‬

‫خط ّي ًة‪ .‬انظر إىل التابع التالي عىل سبيل المثال‪:‬‬


‫أصبحت ّ‬

‫{ )(‪public void clear‬‬


‫{ )‪for (int i=0; i<maps.size(); i++‬‬
‫;)(‪maps.get(i).clear‬‬
‫}‬
‫}‬

‫يضطرّ التابع ‪ clear‬لتفريغ جميع الخرائط الفرع ّيةِ التي يتناسب عددها مع ‪ ،n‬وبالت‪ww‬الي‪ ،‬ه‪ww‬ذا الت‪ww‬ابعُ ّ‬
‫خطيٌّ‪.‬‬

‫لحسن الحظ‪ ،‬ال يُستخدَم هذا التابع كثيرًا‪ ،‬ولذا فما يزال هذا التنفيذ مقبواًل في غالبية التطبيقات‪.‬‬

‫‪ 11.4‬تشخيص الصنف ‪MyHashMap‬‬


‫سنفحص أواًل ما إذا كان التابع ‪ MyHashMap.put‬يستغرق زمنًا خط ًيا‪.‬‬

‫ن ِّفذ األمر ‪ ant build‬لتصريف ملفات الشيفرة‪ ،‬ثم ن ِّفذ األمر ‪ .ant ProfileMapPut‬يقيس األمر زمن‬

‫تشغيل التابع ‪( HashMap.put‬الذي تو ِّفره جافا) مع أحجام مختلفةٍ للمشكلة‪ ،‬ويَع‪ww‬رِض زمن التش‪ww‬غيل م‪w‬ع حجم‬

‫الكلي لع‪w‬دد‬
‫ُّ‬ ‫المشكلة بمقياس لوغاريتمي‪-‬لوغاريتمي‪ .‬إذا كانت العملية تستغرق زمنًا ثابتًا‪ ،‬ينبغي أن يكون الزمن‬

‫خط مستقيمٍ ميله يساوي ‪ .1‬عندما ش َّغلنا تلك الشيفرة‪ ،‬ك‪ww‬ان المي‪ww‬ل‬
‫‪ n‬من العمليات خط ًّيا‪ ،‬ونحصل عندها عىل ٍّ‬
‫المقدَّر قريبًا من ‪ ،1‬وهو ما يتوافق مع تحليلنا للتابع‪ .‬ينبغي أن تحصل عىل نتيجةٍ مشابهة‪.‬‬
‫ُ‬

‫ع‪w‬دِّل الص‪ww‬نف ‪ ProfileMapPut.java‬لكي يُش‪w‬خ ِّص التنفي‪ww‬ذ ‪ MyHashMap‬الخ‪ww‬اص ب‪ww‬ك وليس تنفي‪ww‬ذ‬

‫الجافا ‪ .HashMap‬ش ِّغل شيفرة التشخيص م‪ww‬ر ًة أخ‪w‬رى‪ ،‬وافحص م‪ww‬ا إذا ك‪ww‬ان المي‪ww‬ل قريبً‪ww‬ا من ‪ .1‬ق‪ww‬د تض‪ww‬طرّ إىل‬

‫مناسب من أحجام المشكلة يبلغ زمن تش‪ww‬غيلها أج‪ww‬زا َء‬


‫ٍ‬ ‫نطاق‬
‫ٍ‬ ‫تعديل قيم ‪ startN‬و ‪ endMillis‬لكي تعثر عىل‬

‫صغير ًة من الثانية‪ ،‬وفي نفس الوقت ال يتعدى بضعة آالف‪.‬‬

‫عندما ّ‬
‫شغلنا تلك الشيفرة‪ ،‬وجدنا أن الميلَ يساوي ‪ 1.7‬تقريبًا‪ ،‬مم‪ww‬ا يش‪ww‬ير إىل أن ذل‪ww‬ك التنفي‪ww‬ذ ال يس‪ww‬تغرق‬

‫ق باألداء‪.‬‬ ‫ِّ‬
‫برمجي ُمتعل ٍ‬
‫ٍّ‬ ‫زمنًا ثابتًا‪ .‬في الحقيقة‪ ،‬هو يحتوي عىل خطٍأ‬

‫ّ‬
‫نتوق‪ww‬ع قب‪ww‬ل أن‬ ‫عليك أن تعثر عىل ذلك الخطِأ وتصلحَه وتتأ َّكد من أن التابع ‪ put‬يستغرق زمنًا ثابتًا كم‪ww‬ا كن‪w‬ا‬

‫تنتقل إىل القسم التالي‪.‬‬

‫‪103‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪HashMap‬‬

‫‪ 11.5‬إصالح الصنف ‪MyHashMap‬‬


‫تتمثل مشكلة الصنف ‪ MyHashMap‬بالتابع ‪ size‬الموروث من الصنف ‪ .MyBetterMap‬انظر إىل شيفرته‬

‫فيما يلي‪:‬‬

‫{ )(‪public int size‬‬


‫;‪int total = 0‬‬
‫{ )‪for (MyLinearMap<K, V> map: maps‬‬
‫;)(‪total += map.size‬‬
‫}‬
‫;‪return total‬‬
‫}‬

‫ي‪ .‬نظ‪w‬رًا ألنن‪ww‬ا نزي‪ww‬د ع‪ww‬دد‬ ‫ّ‬


‫كما ترى يضطرّ التابع للمرور ع‪ww‬بر جمي‪ww‬ع الخرائ‪ww‬ط الفرعي‪ww‬ة لكي يحس‪ww‬ب الحجم الكل ّ‬
‫الم ْدخَالت ‪ ،n‬ف‪ww‬إن ‪ k‬يتناس‪ww‬ب م‪ww‬ع ‪ ،n‬ول‪ww‬ذلك‪ ،‬يس‪ww‬تغرق تنفي‪ww‬ذ الت‪ww‬ابع ‪ size‬زمنً‪ww‬ا‬
‫الخرائط الفرعية ‪ k‬بزيادة عدد ُ‬
‫خط ًّيا‪.‬‬
‫ّ‬

‫خط ًّيا ً‬
‫أيضا ألنه يَستخدِم التابع ‪ size‬كما هو ُمب َّينٌ في الشيفرة التالية‪:‬‬ ‫يجعل ذلك التابع ‪ّ put‬‬

‫{ )‪public V put(K key, V value‬‬


‫;)‪V oldValue = super.put(key, value‬‬

‫{ )‪if (size() > maps.size() * FACTOR‬‬


‫;)(‪rehash‬‬
‫}‬
‫;‪return oldValue‬‬
‫}‬

‫َ‬
‫ثابت الزمن‪.‬‬ ‫خط ًّيا‪ ،‬فإننا نهدر كل ما فعلناه لجعل التابع ‪put‬‬
‫إذا تركنا التابع ‪ّ size‬‬

‫الم‪ْ w‬دخَالت ض‪w‬من متغ‪ww‬ير نس‪ww‬خة‬


‫بسيط رأيناه من قب‪ww‬ل‪ ،‬وه‪ww‬و أنن‪ww‬ا س‪ww‬نحتفظ بع‪w‬دد ُ‬
‫ٌ‬ ‫ل‬
‫لحسن الحظ‪ ،‬هناك ح ٌّ‬

‫‪ ،instance variable‬وسنُحدِّثه كلما استدعينا تابعً ا يُجرِي تعدياًل عليه‪.‬‬

‫ستجد الحل في مستودع الكتاب في الملف ‪ .MyFixedHashMap.java‬انظر إىل بداية تعريف الصنف‪:‬‬

‫‪public class MyFixedHashMap<K, V> extends MyHashMap<K, V> implements‬‬


‫{ >‪Map<K, V‬‬

‫;‪private int size = 0‬‬

‫‪104‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪HashMap‬‬

‫{ )(‪public void clear‬‬


‫;)(‪super.clear‬‬
‫;‪size = 0‬‬
‫}‬

‫بداًل من تعديل الصنف ‪ ،MyHashMap‬عرَّفنا صن ًفا جدي‪w‬دًا يمت‪ّ w‬د من‪ww‬ه‪ ،‬وأض‪ww‬فنا إلي‪ww‬ه متغ‪ww‬ير النس‪ww‬خة ‪،size‬‬

‫وضبطنا قيمتَه المبدئ َّية إىل صفر‪.‬‬

‫بس‪w‬يطا عىل الت‪w‬ابع ‪ .clear‬اس‪w‬تدعينا أواًل نس‪w‬خة ‪ُ clear‬‬


‫المعرَّف‪w‬ة في الص‪w‬نف األعىل‬ ‫ً‬ ‫أيض‪w‬ا تع‪w‬دياًل‬
‫أجرينا ً‬

‫(لتفريغ الخرائط الفرعية)‪ ،‬ثم حدثنا قيمة ‪.size‬‬

‫كانت التعديالت عىل التابعين ‪ remove‬و ‪ put‬أعقد قلياًل ؛ ألننا عندما نستدعي نسخها في الص‪ww‬نف األعىل‪،‬‬

‫فإننا ال نستطيع معرفة ما إذا كان حجم الخرائط الفرع ّية قد تغ ّير أم ال‪ .‬تُ ِّ‬
‫وضح الشيفرة التالية الطريقة ال‪ww‬تي حاولن‪ww‬ا‬

‫بها معالجة تلك المشكلة‪:‬‬

‫{ )‪public V remove(Object key‬‬


‫;)‪MyLinearMap<K, V> map = chooseMap(key‬‬
‫;)(‪size -= map.size‬‬
‫;)‪V oldValue = map.remove(key‬‬
‫;)(‪size += map.size‬‬
‫;‪return oldValue‬‬
‫}‬

‫يَستخدِم التابعُ ‪ remove‬التابعَ ‪ chooseMap‬لكي يعثر عىل الخريطة المناسبة‪ ،‬ثم يَطرَ ح حجمها‪ .‬بع‪ww‬د ذل‪ww‬ك‪،‬‬

‫يَستدعِ ي تابع الخريطة الفرعية ‪ remove‬الذي قد يُغ ّير حجم الخريطة‪ ،‬حيث يعتمد ذلك عىل ما إذا ك‪ww‬ان ق‪ww‬د وج‪ww‬د‬

‫المفتاح فيها أم ال‪ ،‬ثم يضيف الحجم الجديد للخريطة الفرعية إىل ‪ ،size‬وبالتالي تصبح القيمة النهائية صحيحة‪.‬‬

‫أعدنا كتابة التابع ‪ put‬باتباع نفس األسلوب‪:‬‬

‫{ )‪public V put(K key, V value‬‬


‫;)‪MyLinearMap<K, V> map = chooseMap(key‬‬
‫;)(‪size -= map.size‬‬
‫;)‪V oldValue = map.put(key, value‬‬
‫;)(‪size += map.size‬‬

‫{ )‪if (size() > maps.size() * FACTOR‬‬


‫;‪size = 0‬‬

‫‪105‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪HashMap‬‬

‫;)(‪rehash‬‬
‫}‬
‫;‪return oldValue‬‬
‫}‬

‫واجهنا نفس المشكلة هنا‪ :‬عندما استدعينا تابع الخريطة الفرعية ‪ ،put‬فإننا ال نعرف م‪ww‬ا إذا ك‪ww‬ان ق‪ww‬د أض‪ww‬اف‬

‫مدخاًل جديدًا أم ال‪ ،‬ولذلك استخدمنا نفس الحل‪ ،‬أي بطرح الحجم القديم‪ ،‬ثم إضافة الحجم الجديد‪.‬‬

‫بسيطا‪:‬‬
‫ً‬ ‫واآلن‪ ،‬أصبح تنفيذ التابع ‪size‬‬

‫{ )(‪public int size‬‬


‫;‪return size‬‬
‫}‬

‫ويستغرق زمنًا ثابتًا بوضوح‪.‬‬

‫ي إلض‪ww‬افة ع‪ww‬دد ‪ n‬من المف‪ww‬اتيح يتناس‪ww‬ب م‪ww‬ع ‪ ،n‬ويع‪ww‬ني‬ ‫ّ‬


‫عندما شخَّصنا أداء هذا الحل‪ ،‬وجدنا أن الزمنَ الكل َّ‬
‫ذلك أن كلّ استدعا ٍء للتابع ‪ put‬يستغرق زمنًا ثابتًا كما هو ُم ّ‬
‫توقع‪.‬‬

‫‪ 11.6‬مخططات أصناف ‪UML‬‬


‫كان أحد التحديات التي واجهناها عند العمل مع شيفرة هذا الفصل هو وجود ع‪ww‬د ٍد كب‪ww‬ير ٍ من األص‪ww‬ناف ال‪ww‬تي‬

‫يعتمد بعضها عىل بعض‪ .‬انظر إىل العالقات بين تلك األصناف‪:‬‬

‫‪ MyLinearMap‬يحتوي عىل ‪ LinkedList‬ويُن ِّفذ ‪.Map‬‬ ‫•‬

‫‪ MyBetterMap‬يحتوي عىل الكثير من كائنات الصنف ‪ MyLinearMap‬ويُن ِّفذ ‪.Map‬‬ ‫•‬

‫كائن‪ww‬ات تنتمي إىل الص‪ww‬نف‬


‫ٍ‬ ‫‪ MyHashMap‬يمت‪ww‬د من الص‪ww‬نف ‪ ،MyBetterMap‬ول‪ww‬ذلك يحت‪ww‬وي عىل‬ ‫•‬

‫‪ MyLinearMap‬ويُن ِّفذ ‪.Map‬‬

‫‪ MyFixedHashMap‬يمتد من الصنف ‪ MyHashMap‬ويُن ِّفذ ‪.Map‬‬ ‫•‬

‫‪w‬ناف ‪UML‬‬
‫‪w‬ات أص‪ِ w‬‬
‫لتسهيل فهمِ هذا النوع من العالق‪ww‬ات‪ ،‬يلج‪ww‬أ مهندس‪ww‬و البرمجي‪ww‬ات إىل اس‪ww‬تخدامِ مخطط‪ِ w‬‬
‫ِ‬
‫‪-‬اختص‪ww‬ا ًرا إىل لغ‪ww‬ة النمذج‪ww‬ة الموح‪ww‬دة ‪ .Unified Modeling Language‬تُع‪ّ ww‬د مخطط‪ww‬ات األص‪ww‬ناف ‪class‬‬
‫ً‬
‫واحدة من المعايير الرسومية التي تُعرِّفها ‪.UML‬‬ ‫‪diagram‬‬

‫صنف في تلك المخططات بصندوق‪ ،‬بينما تُم َثل العالقات بين األصناف بأس‪ww‬هم‪ .‬تَع‪w‬رِض الص‪ww‬ورة‬
‫ٍ‬ ‫يُم َّثل كل‬
‫المس‪ww‬تخدَمة في التم‪ww‬رين الس‪ww‬ابق‪ ،‬وهي ُمول َّ ٌ‬
‫دة تلقائ ًّيا باس‪ww‬تخدام أداة‬ ‫التالي‪ww‬ة مخط‪ww‬ط أص‪ww‬ناف ‪ UML‬لألص‪ww‬ناف ُ‬
‫‪ yUML‬المتاحة عبر اإلنترنت‪.‬‬

‫‪106‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪HashMap‬‬

‫بأنواع مختلفة من األسهم‪:‬‬


‫ٍ‬ ‫تُم َّثل العالقات المختلفة‬

‫تشير األسهم ذات الرؤوس الصلبة إىل عالقات من نوع ‪ .HAS-A‬عىل سبيل المثال‪ ،‬تحت‪ww‬وي ك‪ww‬لّ نس‪ww‬خةٍ‬ ‫•‬

‫نس‪ww‬خ متع‪ww‬دد ٍة من الص‪ww‬نف ‪ ،MyLinearMap‬ول‪ww‬ذلك هي متص‪ww‬لة‬


‫ٍ‬ ‫من الص‪ww‬نف ‪ MyBetterMap‬عىل‬

‫بأسهم صلبة‪.‬‬

‫والخطوط الص‪ww‬لبة إىل عالق‪ww‬ات من ن‪ww‬وع ‪ .IS-A‬عىل س‪ww‬بيل المث‪ww‬ال‪،‬‬


‫ِ‬ ‫تشير األسهم ذات الرؤوس المجوفةِ‬ ‫•‬

‫يمتد الصنف ‪ MyHashMap‬من الصنف ‪ ،MyBetterMap‬ولذلك ستجدهما موصولين بسهم ‪.IS-A‬‬

‫المتقطع‪ww‬ة إىل أن الص‪ww‬نف يُن ِّفذ واجه‪ww‬ة‪ .‬تُن ِّفذ جمي‪ww‬ع‬


‫ّ‬ ‫تش‪ww‬ير األس‪ww‬هم ذات ال‪ww‬رؤوس المج ّوف‪ww‬ة والخط‪ww‬وط‬ ‫•‬
‫َ‬
‫الواجهة ‪.`Map‬‬ ‫األصناف في هذا المخطط‬

‫تُو ِّفر مخططات أصناف ‪ UML‬طريق‪ً w‬‬


‫‪w‬ة م‪ww‬وجز ًة لتوض‪ww‬يح الكث‪w‬ير من المعلوم‪ww‬ات عن مجموع‪ww‬ةٍ من األص‪w‬ناف‪،‬‬

‫تصاميم بديلة‪ ،‬وفي مراحل التنفي‪ww‬ذ لمش‪ww‬اركة التص‪ww‬ور الع‪ww‬ام عن‬


‫َ‬ ‫وتُستخدَم عاد ًة في مراحل التصميم لإلشارة إىل‬

‫المشروع‪ ،‬وفي مراحل النشر لتوثيق التصميم‪.‬‬

‫‪107‬‬
‫‪ .12‬الواجهة ‪TreeMap‬‬

‫س‪wwww‬نناقش في ه‪wwww‬ذا الفص‪wwww‬ل تنفي‪ً wwww‬ذا جدي‪wwww‬دًا للواجه‪wwww‬ة ‪ Map‬يُع‪wwww‬رَف باس‪wwww‬م ش‪wwww‬جرة البحث الثنائية‬

‫‪ .binary search tree‬يشيع استخدام هذا التنفيذ عند الحاجة إىل االحتفاظ بترتيب العناصر‪.‬‬

‫‪ 12.1‬ما هي مشكلة التعمية ‪hashing‬؟‬


‫المن ِّفذ لها ‪ HashMap‬الذي تُ‪ww‬و ِّفره جاف‪ww‬ا‪ .‬إذا كنت ق‪ww‬د‬
‫يُفت َرض أن تكون عىل معرفةٍ بالواجهة ‪ ،Map‬وبالصنف ُ‬
‫قرأت الفصل السابق الذي ن ّفذنا فيه نفس الواجهة باستخدام جدول ‪ ،hash table‬ف ُيفترَض أنك تَعرِف الكيفي‪ww‬ة‬

‫والسبب الذي ألجله تَستغرِق توابع ذلك التنفيذ زمنًا ثابتًا‪.‬‬


‫َ‬ ‫عمل بها الصنف ‪،HashMap‬‬
‫التي يَ َ‬

‫يشيع استخدام الصنف ‪ HashMap‬بفضل كفاءته العالية‪ ،‬ولكنه مع ذلك ليس التنفيذ الوحيد للواجه‪ww‬ة ‪،Map‬‬

‫أسباب عديد ٌة قد تدفعك الختيار تنفي ٍذ آخرَ‪ ،‬منها‪:‬‬


‫ٌ‬ ‫فهناك‬

‫‪ .1‬قد تستغرق عملية حساب شيفرة التعمية زمنًا طوياًل ‪ .‬فعىل ال‪ww‬رغم من أن عملي‪ww‬ات الص‪ww‬نف ‪HashMap‬‬

‫تستغرق زمنًا ثابتًا‪ ،‬فقد يكون ذلك الزمن كبيرًا‪.‬‬

‫عمل التعمية بشكل ج ّي ٍد فقط عن‪ww‬دما تُ‪ww‬وزِّ ع دال‪ُ w‬‬


‫‪w‬ة التعمي‪ww‬ةِ ‪ hash function‬المف‪ww‬اتيحَ بالتس‪ww‬اوي عىل‬ ‫‪ .2‬تَ َ‬
‫ٍ‬
‫‪w‬ة مع ّي ٌ‬
‫ن‪w‬ة عىل‬ ‫خريط‪w‬ة فرع ّي ٌ‬
‫ٌ‬ ‫تصميم دوالِّ التعمية ال يُع ّد أمرًا س‪w‬هاًل ‪ ،‬ف‪w‬إذا احت‪w‬وت‬
‫َ‬ ‫الخرائط الفرعية‪ ،‬ولكنّ‬

‫مفاتيحَ كثيرةٍ‪ ،‬تقل كفاءة الصنف ‪.HashMap‬‬

‫ن‪ ،‬بل قد يتغير ترتيبه‪ww‬ا عن‪ww‬د إع‪ww‬ادة ض‪ww‬بط حجم الج‪ww‬دول‬


‫لترتيب مع ّي ٍ‬
‫ٍ‬ ‫‪ .3‬ال تُخزَّن المفاتيح في الجدول وف ًقا‬

‫وإعادة حساب شيفرات التعمية للمفاتيح‪ .‬بالنس‪ww‬بة لبعض التطبيق‪ww‬ات‪ ،‬ق‪ww‬د يك‪ww‬ون الحف‪ww‬اظ عىل ت‪ww‬رتيب‬

‫المفاتيح ضروريًا أو مفيدًا عىل األقل‪.‬‬


‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪TreeMap‬‬

‫من الصعب حل كل تلك المشكالت في الوقت نفسه‪ ،‬ومع ذلك‪ ،‬تُو ِّفر جافا التنفيذ ‪ TreeMap‬ال‪ww‬ذي يُع‪ww‬الِج‬

‫ً‬
‫بعضا منها‪:‬‬

‫الصنف دال ّ َة تعميةٍ ‪ ،‬وبالتالي‪ ،‬يتجنَّب الزمن اإلضافي الالزم لحساب شيفرات التعمي‪ww‬ة‪،‬‬
‫ُ‬ ‫‪ .1‬ال يَستخدِم ذلك‬

‫صعوبات اختيار دالّةِ تعميةٍ مناسبة‪.‬‬


‫ِ‬ ‫كما يُجنّبُنا‬

‫بحث ثنائ ّي‪ww‬ةٍ ‪ ،‬مم‪ww‬ا يُس ‪w‬هِّل من التنق‪ww‬ل في المف‪ww‬اتيح‬


‫ٍ‬ ‫‪ .2‬تُخزَّن المفاتيح في الصنف ‪ TreeMap‬بهيئة شجر ِة‬

‫خطي‪.‬‬
‫وبزمن ّ‬
‫ٍ‬ ‫ن‬
‫لترتيب مع ّي ٍ‬
‫ٍ‬ ‫وف ًقا‬

‫‪ .3‬يتناسب زمن تنفيذ غالب ّية توابع الصنف ‪ TreeMap‬مع )‪ ،log(n‬وال‪ww‬تي رغم أنه‪ww‬ا ليس‪ww‬ت بكف‪ww‬اءة ال‪ww‬زمن‬
‫ً‬
‫جيدة جدًا‪.‬‬ ‫الثابت‪ ،‬ولكنها ما تزال‬

‫سنشرح طريقة عمل أشجار البحث الثنائية في القسم التالي ثم سنستخدِمها لتنفيذ الواجه‪ww‬ة ‪ ،Map‬وأخ‪ww‬يرًا‪،‬‬

‫المن َّفذة باستخدام شجرة‪.‬‬


‫التوابع األساس ّيةِ في الخرائط ُ‬
‫ِ‬ ‫سنُحلّل أداء‬

‫‪ 12.2‬أشجار البحث الثنائية‬


‫مفتاح‪ ،‬كما تتو ّفر فيها "خاصية ‪ "BST‬التي‬
‫ٍ‬ ‫شجرة البحث الثنائية عبار ٌة عن شجر ٍة تحتوي كلُّ عقد ٍة فيها عىل‬

‫تنص عىل التالي‪:‬‬

‫ٌ‬
‫ابنة يسرى‪ ،‬فال ب ُ ّد أن تكون قيم جميع المفاتيح الموجودة في الشجرة الفرعية‬ ‫أب عقد ٌة‬
‫‪ .1‬إذا كان ألي عقد ٍة ٍ‬
‫اليسرى أصغرَ من قيمة مفتاح تلك العقدة‪.‬‬

‫ٌ‬
‫ابنة يمنى‪ ،‬فال ب ُ ّد أن تكون قيم جميع المفاتيح الموجودة في الشجرة الفرعي‪ww‬ة‬ ‫أب عقد ٌة‬
‫‪ .2‬إذا كان ألي عقد ٍة ٍ‬
‫اليمنى أكبرَ من قيمة مفتاح تلك العقدة‪.‬‬

‫‪109‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪TreeMap‬‬

‫ٌ‬
‫م‪w‬أخوذة من مقال‪w‬ةِ‬ ‫الش‪w‬روط الس‪w‬ابقة‪ .‬ه‪w‬ذه الص‪w‬ورة‬
‫َ‬ ‫تَعرِض الص‪w‬ورة الس‪w‬ابقة ش‪w‬جرة أع‪w‬دا ٍد ص‪w‬حيحةٍ تُح ِّقق‬

‫ويكيبيديا موضوعها أشجار البحث الثنائية‪ ،‬والتي قد تفيدك لحل هذا التمرين‪.‬‬

‫الحِ ظ أن مفتاح عقدة الجذر يساوي ‪ .8‬يُمكِنك التأ ّكد من أن مفاتيح العقد الموجودة عىل يس‪ww‬ار عق‪ww‬دة الج‪ww‬ذر‬

‫أقلّ من ‪ 8‬بينما مفاتيح العقد الموجودة عىل يمينها أكب َر من ‪ .8‬تأ ّكد من تح ّقق نفس الشرط للعقد األخرى‪.‬‬

‫بحث ثنائ ّيةٍ زمنًا ط‪ww‬وياًل ‪ ،‬ألن‪ww‬ك غ‪ww‬ير ُمض‪ww‬طرّ للبحث في كام‪ww‬ل‬
‫ٍ‬ ‫مفتاح ما ضمن شجر ِة‬
‫ٍ‬ ‫ال يَستغرِق البحث عن‬
‫َ‬
‫الخوارزمية التالية‪:‬‬ ‫ثم‪ ،‬تُطبِّق‬
‫الشجرة‪ ،‬وإنما عليك أن تبدأ من جذر الشجرة‪ ،‬ومن ّ‬

‫‪ .1‬افحص قيمة المفتاح الهدف‪ target‬الذي تبحث عنه وطابقه مع قيمة مفتاح العقدة الحالية‪ .‬ف‪ww‬إذا كان‪ww‬ا‬

‫متساويين‪ ،‬فقد انتهيت بالفعل‪.‬‬

‫الحالي‪ ،‬ابحث في الشجرة الموج‪ww‬ودة عىل اليس‪ww‬ار‪ ،‬ف‪ww‬إذا‬


‫ِّ‬ ‫أما إذا كان المفتاح ‪ target‬أصغرَ من المفتاح‬
‫‪ّ .2‬‬
‫لم تكن موجود ًة‪ ،‬فهذا يَعنِي أن المفتاح ‪ target‬غيرُ موجو ٍد في الشجرة‪.‬‬

‫الحالي‪ ،‬ابحث في الشجرة الموجودة عىل اليمين‪ .‬فإذا لم‬


‫ِّ‬ ‫وأما إذا كان المفتاح ‪ target‬أكبرَ من المفتاح‬
‫ّ‬ ‫‪.3‬‬

‫تكن موجودة‪ ،‬فهذا يَعنِي أنّ المفتاح ‪ target‬غير موجود في الشجرة‪.‬‬

‫‪w‬توى ض‪ww‬من الش‪ww‬جرة‪ .‬فعىل س‪ww‬بيل‬


‫ً‬ ‫يَعنِي ما سبق أنك مضطر ٌّ للبحث في عقد ٍة ابن‪ww‬ة واح‪ww‬د ٍة فق‪ww‬ط لك‪ww‬ل مس‪w‬‬

‫المثال‪ ،‬إذا كنت تبحث عن مفتاح ‪ target‬قيمت‪ww‬ه تس‪ww‬اوي ‪ 4‬في الرس‪ww‬مة الس‪ww‬ابقة‪ ،‬فعلي‪ww‬ك أن تب‪ww‬دأ من عق‪ww‬دة‬

‫الجذر التي تحتوي عىل المفتاح ‪ ،8‬وألن المفتاح المطلوب أقلَّ من ‪ ،8‬فس‪ww‬تذهب إىل اليس‪ww‬ار‪ ،‬وألن‪ww‬ه أك‪ww‬بر من ‪،3‬‬

‫فستذهب إىل اليمين‪ ،‬وألنه أقل من ‪ ،6‬فستذهب إىل اليسار‪ ،‬ثم ستعثر عىل المفتاح الذي تبحث عنه‪.‬‬

‫تطلّب البحث عن المفتاح في المث‪ww‬ال الس‪ww‬ابق ‪ 4‬عملي‪ِ w‬‬


‫‪w‬ات موازن‪ww‬ةٍ رغم أنّ الش‪ww‬جرة تحت‪ww‬وي عىل ‪ 9‬مف‪ww‬اتيح‪.‬‬

‫يتناسب عدد الموازنات المطلوبة في العموم مع ارتفاع الشجرة وليس مع عدد المفاتيح الموجودة فيها‪.‬‬

‫بارتف‪w‬اع قص‪w‬ير ٍ‬
‫ٍ‬ ‫ما الذي نستنتجه من ذلك بخصوص العالقة بين ارتفاع الش‪w‬جرة ‪ h‬وع‪w‬دد العق‪w‬د ‪n‬؟ إذا ب‪w‬دأنا‬

‫وزدناه تدريج ًّيا‪ ،‬فسنحصل عىل التالي‪:‬‬

‫إذا كان ارتفاع الشجرة ‪ h‬يساوي ‪ ،1‬فإن عدد العقد ‪ n‬ضمن تلك الشجرة يساوي ‪.1‬‬ ‫•‬

‫ن‪ ،‬وبالتالي‪ ،‬يص‪ww‬بح ع‪ww‬دد العق‪ww‬د ‪n‬‬ ‫ُأ‬


‫وإذا كان ارتفاع الشجرة ‪ h‬يساوي ‪ ،2‬ف ُيمكِننا أن نضيف عقدتين خرَيَ ْي ِ‬ ‫•‬

‫في الشجرة مساويًا للقيمة ‪.3‬‬

‫أربع عق ‪ٍ w‬د أخ‪ww‬رى‪ ،‬وبالت‪ww‬الي‪ ،‬يص‪ww‬بح‬


‫ِ‬ ‫وإذا كان ارتفاع الشجرة ‪ h‬يساوي ‪ ،3‬ف ُيمكِننا أن نضيف ما يصل إىل‬ ‫•‬

‫عدد العقد ‪ n‬مساويًا للقيمة ‪.7‬‬

‫وإذا كان ارتفاع الشجرة ‪ h‬يساوي ‪ ،4‬يُمكِننا أن نضيف ما يصل إىل ثماني عق ٍد أخ‪ww‬رى‪ ،‬وبالت‪ww‬الي‪ ،‬يص‪ww‬بح‬ ‫•‬

‫عدد العقد ‪ n‬مساويًا للقيمة ‪.15‬‬

‫‪110‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪TreeMap‬‬

‫ربما الحظت النمط المشترك بين تلك األمثلة‪ .‬إذا َّ‬


‫رقمنا مستويات الشجرة ت‪ww‬دريج ًّيا من ‪ 1‬إىل ‪ ،h‬ف‪ww‬إن ع‪ww‬دد‬

‫‪w‬الي في ع‪ww‬دد ‪ h‬من‬


‫ص ‪w‬ل إىل ‪ 2i-1‬كح ‪ٍّ w‬د أقص‪ww‬ى‪ ،‬وبالت‪ww‬الي‪ ،‬يك‪ww‬ون ع‪ww‬د ُد العق ‪ِ w‬د اإلجم‪ُّ w‬‬
‫‪w‬توى ‪ i‬يَ ِ‬
‫ً‬ ‫ي مس‪w‬‬
‫العق‪ww‬د في أ ّ‬
‫المستويات هو ‪ .2h-1‬إذا كان‪:‬‬

‫‪n = 2h - 1‬‬

‫بتطبيق لوغاريتم األساس ‪ 2‬عىل طرفي المعادلة السابقة‪ ،‬نحصل عىل التالي‪:‬‬

‫‪log2 n ≈ h‬‬

‫‪w‬توى فيه‪ww‬ا يحت‪ww‬وي عىل‬ ‫ً‬


‫ممتلئة؛ أي إذا كان كل مس‪w‬‬ ‫إ ًذا‪ ،‬يتناسب ارتفاع الشجرة مع )‪ log(n‬إذا كانت الشجرة‬
‫ً‬
‫العدد األقصى المسموح به من العقد‪.‬‬

‫بحث ثنائ ّي‪ww‬ةٍ م‪ww‬ع )‪ .log(n‬يُع‪ّ w‬د ذل‪ww‬ك ص‪ww‬حيحًا س‪ww‬وا ٌء‬
‫ٍ‬ ‫مفتاح ضمن ش‪ww‬جر ِة‬
‫ٍ‬ ‫البحث عن‬
‫ِ‬ ‫وبالتالي‪ ،‬يتناسب زمنُ‬

‫أكانت الشجر ُة ممتلئة كلّ ًّيا أم جزئ ًيا‪ ،‬ولكنه ليس صحيحًا في المطلق‪ ،‬وهو ما سنراه الح ًقا‪.‬‬

‫يُطلَق عىل الخوارزميات التي تَستغرِق زمنًا يتناسب م‪w‬ع )‪ log(n‬اس‪w‬م "خوارزمي‪ww‬ة لوغاريتمي‪w‬ة"‪ ،‬وتنتمي إىل‬

‫ترتيب النمو ))‪.O(log(n‬‬

‫‪ 12.3‬تمرين ‪10‬‬
‫بحث ثنائ ّيةٍ ‪.‬‬
‫ٍ‬ ‫ستكتب في هذا التمرين تنفي ًذا للواجهة ‪ Map‬باستخدام شجر ِة‬

‫المبدئي للصنف ‪:MyTreeMap‬‬


‫ِّ‬ ‫التعريف‬
‫ِ‬ ‫انظر إىل‬

‫{ >‪public class MyTreeMap<K, V> implements Map<K, V‬‬

‫;‪private int size = 0‬‬


‫;‪private Node root = null‬‬

‫يحتفظ متغ ّيرُ النسخةِ ‪ size‬بع‪w‬دد المف‪w‬اتيح بينم‪ww‬ا يحت‪w‬وي ‪ root‬عىل مرج‪ww‬ع ‪ reference‬يش‪w‬ير إىل عق‪ww‬دة‬
‫ً‬
‫فارغ‪ww‬ة‪ ،‬يحت‪ww‬وي ‪ root‬عىل القيم‪ww‬ة ‪ null‬وتك‪ww‬ون قيم‪ww‬ة ‪size‬‬ ‫الخاص‪ww‬ةِ بالش‪ww‬جرة‪ .‬إذا ك‪ww‬انت الش‪ww‬جرة‬
‫ّ‬ ‫الج‪ww‬ذر‬
‫ً‬
‫مساوية للصفر‪.‬‬

‫المعرَّف داخل الصنف ‪:MyTreeMap‬‬


‫انظر إىل الصنف ‪ُ Node‬‬

‫{ ‪protected class Node‬‬


‫;‪public K key‬‬
‫;‪public V value‬‬
‫;‪public Node left = null‬‬

‫‪111‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪TreeMap‬‬

‫;‪public Node right = null‬‬

‫{ )‪public Node(K key, V value‬‬


‫;‪this.key = key‬‬
‫;‪this.value = value‬‬
‫}‬
‫}‬

‫تحتوي كل عق‪ww‬د ٍة عىل زوج مفت‪ww‬اح‪/‬قيم‪ww‬ة وعىل مراج‪ww‬عَ تش‪ww‬ير إىل العق‪ww‬د األبن‪ww‬اء ‪ left‬و ‪ .right‬ق‪ww‬د تك‪ww‬ون‬

‫إحداهما أو كلتاهما فارغة أي تحتوي عىل القيمة ‪.null‬‬

‫من السهل تنفيذ بعض توابع الواجهة ‪ Map‬مثل ‪ size‬و ‪:clear‬‬

‫{ )(‪public int size‬‬


‫;‪return size‬‬
‫}‬

‫{ )(‪public void clear‬‬


‫;‪size = 0‬‬
‫;‪root = null‬‬
‫}‬

‫من الواضح أن التابع ‪ size‬يَستغرِق زمنًا ثابتًا‪.‬‬

‫قد تظن للوهلة األوىل أن التابع ‪ clear‬يستغرق زمنًا ثابتًا‪ ،‬ولكن فكر بالتالي‪ :‬عندما تُضبَط قيم‪ww‬ة ‪ root‬إىل‬

‫كانس المهمالت ‪ garbage collector‬العق َد الموجودة في الشجرة ويَستغرِق إلنجاز ذلك‬


‫ُ‬ ‫القيمة ‪ ،null‬يستعيد‬

‫حسب العمل الذي يقوم به كانس المهمالت؟ ربما‪.‬‬


‫زمنًا خط ًيا‪ .‬هل ينبغي أن يُ َ‬

‫األهم ‪ get‬و ‪.put‬‬


‫ّ‬ ‫ستكتب في القسم التالي تنفي ًذا لبعض التوابع األخرى ال س ّيما التابعين‬

‫‪ 12.4‬تنفيذ الصنف ‪TreeMap‬‬


‫ستجد ملفات الشيفرة التالية في مستودع الكتاب‪:‬‬

‫مبدئي للتوابع غير المكتملة‪.‬‬


‫ٍّ‬ ‫الم َّ‬
‫وضحة في األعىل مع تصورٍ‬ ‫‪ :MyTreeMap.java‬يحتوي عىل الشيفرة ُ‬ ‫•‬

‫‪ : MyTreeMapTest.java‬يحتوي عىل اختبارات وحد ٍة للصنف ‪.MyTreeMap‬‬ ‫•‬

‫‪ .ant‬قد تفشل بعض‬ ‫ن ِّفذ األمر ‪ ant build‬لتصريف ملفات الشيفرة‪ ،‬ثم ن ِّفذ األمر ‪MyTreeMapTest‬‬

‫االختبارات ألنّ هناك بعض التوابع التي ينبغي عليك إكمالها أواًل ‪.‬‬

‫‪112‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪TreeMap‬‬

‫و َّفرنا تصو ًرا مبدئ ًيا للتابعين ‪ get‬و ‪ .containsKey‬يَستخدِم كالهما التابع ‪ُ findNode‬‬
‫المعرَّف باس‪ww‬تخدام‬

‫المعدِّل ‪ ،private‬ألنه ليس جزءًا من الواجهة ‪ .Map‬انظر إىل بداية تعريفه‪:‬‬


‫ُ‬

‫{ )‪private Node findNode(Object target‬‬


‫{ )‪if (target == null‬‬
‫;)(‪throw new IllegalArgumentException‬‬
‫}‬

‫)"‪@SuppressWarnings("unchecked‬‬
‫;‪Comparable<? super K> k = (Comparable<? super K>) target‬‬

‫!‪// TODO: FILL THIS IN‬‬


‫;‪return null‬‬
‫}‬

‫يشير المعامل ‪ target‬إىل المفتاح الذي نبحث عنه‪ .‬إذا ك‪ww‬انت قيم‪ww‬ة ‪ target‬تس‪w‬اوي ‪ ،null‬يُبلِّغ الت‪ww‬ابع‬

‫اعتراض ‪ .exception‬في الواقع‪ ،‬بإمكان بعض تنفي‪ww‬ذات الواجه‪ww‬ة ‪ Map‬معالج‪ww‬ة الح‪ww‬االت ال‪ww‬تي‬
‫ٍ‬ ‫‪ findNode‬عن‬

‫بحث ثنائ ّي‪w‬ةٍ ‪ ،‬فال ب ُ‪ّ w‬د أن نتمكّن من‬


‫ٍ‬ ‫تكون فيها قيمة المفتاح فارغة‪ ،‬ولكن ألنن‪w‬ا في ه‪w‬ذا التنفي‪w‬ذ نَس‪w‬تخدِم ش‪w‬جرة‬

‫بس‪w‬ط األم‪ww‬ور‪ ،‬لن نَس‪َ w‬‬


‫‪w‬مح له‪ww‬ذا‬ ‫موازنةِ المفاتيح‪ ،‬ولذلك‪ ،‬يُشكِّل التعامل م‪ww‬ع القيم‪ww‬ة ‪ null‬مش‪ً w‬‬
‫‪w‬كلة‪ ،‬ول‪ww‬ذا ولكي ن ُ ِّ‬
‫التنفيذ باستخدام القيمة ‪ null‬كمفتاح‪.‬‬

‫مفت‪w‬اح ض‪ww‬من الش‪w‬جرة‪ .‬تش‪w‬ير‬


‫ٍ‬ ‫تُ ِّ‬
‫وضح األسطر التالية كيف يمكِننا أن نوازن قيمة المفتاح ‪ target‬مع قيم‪ww‬ة‬

‫المص‪www‬رِّف يتعام‪www‬ل م‪www‬ع ‪ target‬كم‪www‬ا ل‪www‬و أن‪www‬ه ينتمي‬ ‫ُ‬


‫نس‪www‬خة الت‪www‬ابعين ‪ get‬و ‪ containsKey‬إىل أن ُ‬
‫إىل الن‪wwww‬وع ‪ ،Object‬وألنن‪wwww‬ا نري‪wwww‬د موازنت‪wwww‬ه م‪wwww‬ع المف‪wwww‬اتيح‪ ،‬فإنن‪wwww‬ا نح‪ِّ wwww‬ول ن‪wwww‬وع ‪ target‬إىل الن‪wwww‬وع‬

‫ي من أص‪ww‬نافه األعىل‬
‫ك‪ww‬ائن من الن‪ww‬وع ‪ K‬أو أ ٍّ‬
‫ٍ‬ ‫>‪ Comparable<? super K‬لكي يُص‪ِ ww‬بح ق‪ww‬اباًل للموازن‪ww‬ة م‪ww‬ع‬

‫‪ .superclass‬يُمكِنك قراءة المزيد عن أنواع محارف البدل (باللغة اإلنجليزية)‪.‬‬

‫َ‬
‫احتراف التعامل مع نظام األنواع في لغة جافا‪ ،‬ف‪ww‬دورك فق‪ww‬ط ه‪ww‬و أن تُكمِ ‪ w‬ل‬ ‫ليس المقصو ُد من هذا التمرين‬

‫التابع ‪ .findNode‬إذا وجد ذلك التابع عقد ًة تحتوي عىل قيم‪ww‬ة ‪ target‬كمفت‪ww‬اح‪ ،‬فعلي‪ww‬ه أن يعي‪ww‬دها‪ ،‬أم‪ww‬ا إذا لم‬

‫يجدها‪ ،‬فعليه أن يعيد القيمة ‪ .null‬ينبغي أن تنجح اختب‪ww‬ارات الت‪ww‬ابعين ‪ get‬و ‪ containsKey‬بع‪ww‬د أن تنتهي‬

‫من إكمال هذا التابع‪.‬‬

‫ينبغي للحل الخاص بك أن يبحث في مسارٍ واح ٍد فقط ضمن الشجرة ال أن يبحث في كامل الشجرة‪ ،‬أي‬

‫س َيستغرِق زمنًا يتناسب مع ارتفاع الشجرة‪.‬‬

‫واآلن‪ ،‬عليك أن تُكمِ ل التابع ‪ ،containsValue‬ولمس‪ww‬اعدتك عىل ذل‪ww‬ك‪ ،‬و َّفرن‪ww‬ا الت‪ww‬ابع المس‪ww‬اعد ‪equals‬‬

‫المخزَّن‪ww‬ة في‬
‫ن‪ .‬عىل العكس من المف‪ww‬اتيح‪ ،‬ق‪ww‬د ال تك‪ww‬ون القيم ُ‬
‫‪w‬اح مع ّي ٍ‬
‫الذي يُوازِن بين قيمة ‪ target‬وقيمة مفت‪ٍ w‬‬

‫‪113‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪TreeMap‬‬

‫ً‬
‫الشجرة قابلة للموازنة‪ ،‬وبالتالي‪ ،‬ال يُمكِننا أن نَستخدِم معها التابع ‪ ،compareTo‬وإنما علين‪ww‬ا أن نَس‪ww‬تدعِ َ‬
‫ي الت‪ww‬ابعَ‬

‫‪ equals‬بالمتغير ‪.target‬‬

‫بخالف التابع ‪ ،findNode‬سيضطرّ التابع ‪ containsValue‬للبحث في كامل الش‪ww‬جرة‪ ،‬أي يتناس‪ww‬ب زمن‬

‫تشغيله مع عدد المفاتيح ‪ n‬وليس مع ارتفاع الشجرة ‪.h‬‬

‫ً‬
‫مبدئية تعالج الحاالت البسيطة فقط‪:‬‬ ‫واآلن‪ ،‬أكمل متنَ التابع ‪ .put‬و َّفرنا له شيفر ًة‬

‫{ )‪public V put(K key, V value‬‬


‫{ )‪if (key == null‬‬
‫;)(‪throw new IllegalArgumentException‬‬
‫}‬
‫{ )‪if (root == null‬‬
‫;)‪root = new Node(key, value‬‬
‫;‪size++‬‬
‫;‪return null‬‬
‫}‬
‫;)‪return putHelper(root, key, value‬‬
‫}‬

‫{ )‪private V putHelper(Node node, K key, V value‬‬


‫‪// TODO: Fill this in.‬‬
‫}‬

‫إذا حاولت اس‪ww‬تخدَام القيم‪ww‬ة الفارغ‪ww‬ة ‪ null‬كمفت‪ww‬اح‪ ،‬س‪ُ w‬يبلّغ ‪ put‬عن اع‪ww‬تراض‪ .‬إذا ك‪ww‬انت الش‪ww‬جرة فارغ‪ً w‬‬
‫‪w‬ة‪،‬‬ ‫ٍ‬
‫المعرَّف فيها‪.‬‬ ‫ً‬
‫جديدة‪ ،‬ويُه ِّيُئ المتغير ‪ُ root‬‬ ‫ً‬
‫عقدة‬ ‫نشئ التابع ‪put‬‬
‫س ُي ِ‬

‫المع‪w‬دِّل ‪ private‬ألن‪ww‬ه ليس‬


‫المع‪w‬رَّف باس‪ww‬تخدام ُ‬ ‫ً‬
‫فارغة‪ ،‬فإنه يَستدعِ ي الت‪ww‬ابع ‪ُ putHelper‬‬ ‫أما إذا لم تكن‬

‫جزءًا من الواجهة ‪.Map‬‬

‫أكمل متنَ التابع ‪ putHelper‬واجعله يبحث ضمن الشجرة وف ًقا لما يلي‪:‬‬

‫‪ .1‬إذا كان المفتاح ‪ key‬موجودًا بالفعل ضمن الشجرة‪ ،‬عليه أن يَستبدِل القيمة الجدي‪w‬دة بالقيم‪w‬ة القديم‪ww‬ة‪،‬‬

‫ثم يعيدها‪.‬‬

‫ً‬
‫جدي‪w‬دة‪ ،‬ثم يض‪w‬يفها إىل المك‪w‬ان‬ ‫نش‪w‬ئ عق‪ً w‬‬
‫‪w‬دة‬ ‫‪ .2‬إذا لم يكن المفتاح ‪ key‬موج‪w‬ودًا في الش‪w‬جرة‪ ،‬فعلي‪ww‬ه أن يُ ِ‬

‫الصحيح‪ ،‬وأخيرًا‪ ،‬يعيد القيمة ‪.null‬‬

‫‪114‬‬
‫هياكل البيانات للمبرمجين‬ ‫الواجهة ‪TreeMap‬‬

‫ينبغي أن يَستغرِق التابع ‪ put‬زمنًا يتناسب مع ارتفاع الش‪ww‬جرة ‪ h‬وليس م‪ww‬ع ع‪ww‬دد العناص‪ww‬ر ‪ .n‬س‪ww‬يكون من‬
‫ً‬
‫واحدة فقط‪ ،‬ولكن إذا كان البحث فيها م‪w‬رّتين أس‪ww‬هلَ بالنس‪ww‬بة ل‪ww‬ك‪ ،‬فال ب‪ww‬أس‪.‬‬ ‫األفضل لو بحثت في الشجرة ً‬
‫مرة‬

‫سيكون التنفيذ أبطأ‪ ،‬ولكنّه لن يؤثر عىل ترتيب نموه‪.‬‬

‫وأخي ًرا‪ ،‬عليك أن تُكمِ ل متن التابع ‪ .keySet‬يعيد ذلك التابع ‪-‬وف ًقا لـللتوثيق (باللغ‪ww‬ة اإلنجليزية)‪ -‬قيم‪ww‬ة من‬

‫ي وف ًق‪ww‬ا للت‪ww‬ابع ‪ .compareTo‬كن‪ww‬ا ق‪ww‬د‬


‫‪w‬ترتيب تص‪ww‬اعد ٍّ‬
‫ٍ‬ ‫النوع ‪ Set‬بإمكانه‪ww‬ا الم‪ww‬رور ع‪ww‬بر جمي‪ww‬ع مف‪ww‬اتيح الش‪ww‬جرة ب‪w‬‬
‫اِستخدَمنا الصنف ‪ HashSet‬في الفصل الثامن المفهرس ‪ .Indexer‬يُع ‪ّ w‬د ذل‪ww‬ك الص‪ww‬نف تنفي ‪ً w‬‬
‫ذا للواجه‪ww‬ة ‪Set‬‬

‫ولكنه ال يحافظ عىل ترتيب المفاتيح‪ .‬في المقاب‪ww‬ل‪ ،‬يت‪ww‬و َّفر التنفي‪ww‬ذ ‪ LinkedHashSet‬ال‪ww‬ذي يحاف‪ww‬ظ عىل ت‪ww‬رتيب‬

‫المفاتيح‪.‬‬

‫ً‬
‫قيمة من النوع ‪ LinkedHashSet‬ويعيدها كما يلي‪:‬‬ ‫نشئ التابع ‪keySet‬‬
‫يُ ِ‬

‫{ )(‪public Set<K> keySet‬‬


‫;)(>‪Set<K> set = new LinkedHashSet<K‬‬
‫;‪return set‬‬
‫}‬

‫ي‪.‬‬
‫بترتيب تصاعد ّ‬
‫ٍ‬ ‫ُ‬
‫يضيف المفاتيح من الشجرة إىل المجموعة ‪set‬‬ ‫عليك أن تُكمِ ل هذا التابع بحيث تجعلُه‬

‫ُ‬
‫قراءة بعض‬ ‫تابع مساع ٍد‪ ،‬وقد ترغب بجعله تعاوديًّا ‪ ،recursive‬كما قد يساعدك عىل الحلِّ‬
‫ٍ‬ ‫قد تحتاج إىل كتابةٍ‬

‫المعلومات عن أسلوب "في الترتيب" التنقل في الشجرة (باللغة اإلنجليزية)‪.‬‬

‫ينبغي أن تنجح جميع االختبارات بعد أن تنتهي من إكمال هذا التابع‪ .‬س ‪w‬نَعرِض ح‪ww‬ل ه‪ww‬ذا التم‪ww‬رين ونفحص‬

‫أداء التوابع األساسية في الصنف في فصل الحق من هذا الكتاب‪.‬‬

‫‪115‬‬
‫‪ .13‬شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫المن ّفذة باستخدام ش‪w‬جرة‪ ،‬وبع‪w‬دها‬


‫سنناقش في هذا الفصل حل تمرين الفصل السابق‪ ،‬ونختبر أداء الخرائط ُ‬
‫سنناقش إحدى مشاكل ذلك التنفيذ والحلّ الذي يقدمه الصنف ‪ TreeMap‬لتلك المشكلة‪.‬‬

‫‪ 13.1‬الصنف ‪MyTreeMap‬‬
‫و َّفرنا في الفصل المشار إليها تص‪ّ w‬و ًرا مب‪ww‬دئ ًيا للص‪ww‬نف ‪ ،MyTreeMap‬وتركن‪ww‬ا للق‪ww‬ارئ مهم‪ww‬ة إكم‪ww‬ال توابع‪ww‬ه‪.‬‬

‫وسنُكملها اآلنَ معً ا‪ ،‬ولْنبدأ بالتابع ‪:findNode‬‬

‫{ )‪private Node findNode(Object target‬‬


‫ً‬
‫مفتاحا ولكن ليس في هذه الحالة ‪//‬‬ ‫بعض التنفيذات تعد ‪null‬‬
‫{ )‪if (target == null‬‬
‫;)(‪throw new IllegalArgumentException‬‬
‫}‬

‫الم ِّ‬
‫صرف ‪//‬‬ ‫هذا ما ُيسعد ُ‬

‫)"‪@SuppressWarnings("unchecked‬‬
‫;‪Comparable<? super K> k = (Comparable<? super K>) target‬‬

‫البحث الحقيقي ‪//‬‬


‫;‪Node node = root‬‬
‫{ )‪while (node != null‬‬
‫;)‪int cmp = k.compareTo(node.key‬‬
‫)‪if (cmp < 0‬‬
‫هياكل البيانات للمبرمجين‬ ‫شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫;‪node = node.left‬‬
‫)‪else if (cmp > 0‬‬
‫;‪node = node.right‬‬
‫‪else‬‬
‫;‪return node‬‬
‫}‬
‫;‪return null‬‬
‫}‬

‫يَستخدِم التابع‪ww‬ان ‪ containsKey‬و ‪ get‬الت‪ww‬ابعَ ‪ ،findNode‬وألن‪ww‬ه ليس ج‪ww‬زءًا من الواجه‪ww‬ة ‪ ،Map‬عرَّفن‪ww‬اه‬

‫المعدّل ‪ .private‬يُمثِل المعامل ‪ target‬المفتاحَ الذي نبحث عنه‪ .‬كنا ق‪ww‬د ش‪ww‬رحنا الج‪ww‬زء األول من‬
‫باستخدام ُ‬
‫هذا التابع في الفصل المشار إليه‪:‬‬

‫كمفتاح في هذا التنفيذ‪.‬‬ ‫ً‬


‫صالحة‬ ‫ً‬
‫قيمة‬ ‫ال تُع ّ‬
‫د ‪null‬‬ ‫•‬
‫ٍ‬

‫المعام‪ww‬ل ‪ target‬إىل ‪ Comparable‬قب‪ww‬ل أن نَس‪ww‬تدعِ َ‬


‫ي تابعَ‪ ww‬ه ‪.compareTo‬‬ ‫ِ‬ ‫ينبغي أن نح‪ِّ ww‬ولَ ن‪ww‬و َ‬
‫ع‬ ‫•‬

‫عمل مع أي ن‪ww‬وع يُن ِّفذ الواجه‪ww‬ة ‪ ،Comparable‬كم‪ww‬ا‬


‫اِستخدَمنا أكثر أنواع محارف البدل عمومية‪ ،‬حيث يَ َ‬
‫أن تابعه ‪ compareTo‬يَستق ِبل النوع ‪ K‬أو أيًّا من أنواعه األعىل ‪.supertype‬‬

‫يُجرَى البحث عىل النحو التالي‪ :‬نض‪ww‬بط متغ‪ww‬ير الحلق‪ww‬ة ‪ node‬إىل عق‪ww‬دة الج‪ww‬ذر‪ ،‬وفي ك‪ww‬لّ تك‪ww‬رارٍ‪ ،‬ن‪ww‬وازن بين‬

‫المفتاح ‪ target‬وقيمة ‪ .node.key‬إذا كان ‪ target‬أصغ َر من مفتاح العقدة الحال ّية‪ ،‬سننتقل إىل عقدة االبن‬

‫اليسرى‪ ،‬أما إذا كان أكبرَ منه‪ ،‬سننتقل إىل عقدة االبن اليمنى‪ ،‬وإذا كانا متساويين‪ ،‬سنعيد العقدة الحال ّية‪.‬‬

‫إذا وصلنا إىل قاع الشجرة دون أن نعثر عىل المفتاح المطلوب‪ ،‬فهذا يَعنِي أنه غير موجود فيها‪ ،‬وس‪w‬نعيد في‬

‫تلك الحالة القيمة الفارغة ‪.null‬‬

‫‪ 13.2‬البحث عن القيم ‪values‬‬


‫كما أوضحنا في نفس الفصل المشار إليها في األعىل‪ ،‬يتناس‪ww‬ب زمن تنفي‪ww‬ذ الت‪ww‬ابع ‪ findNode‬م‪ww‬ع ارتف‪ww‬اع‬

‫الشجرة وليس مع عدد العقد الموجودة فيها؛ وذلك ألننا غير مضطرّين للبحث في كامل الش‪ww‬جرة‪ ،‬ولكن بالنس‪ww‬بة‬

‫للتابع ‪ ،containsValue‬فإننا سنضطرّ للبحث بالقيم وليس المف‪ww‬اتيح‪ ،‬وألن خاص‪ww‬ية ‪ BST‬ال تُطبَّق عىل القيم‪،‬‬

‫فإننا سنضطرّ إىل البحث في كامل الشجرة‪.‬‬

‫يَستخدِم الحلُّ التالي التعاود ‪:recursion‬‬

‫{ )‪public boolean containsValue(Object target‬‬


‫;)‪return containsValueHelper(root, target‬‬
‫}‬

‫‪117‬‬
‫هياكل البيانات للمبرمجين‬ ‫شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫{ )‪private boolean containsValueHelper(Node node, Object target‬‬


‫{ )‪if (node == null‬‬
‫;‪return false‬‬
‫}‬
‫{ ))‪if (equals(target, node.value‬‬
‫;‪return true‬‬
‫}‬
‫{ ))‪if (containsValueHelper(node.left, target‬‬
‫;‪return true‬‬
‫}‬
‫{ ))‪if (containsValueHelper(node.right, target‬‬
‫;‪return true‬‬
‫}‬
‫;‪return false‬‬
‫}‬

‫إضافي يحت‪ww‬وي عىل عق‪ww‬دة الج‪ww‬ذر‬


‫ٍّ‬ ‫معامل‬
‫ٍ‬ ‫يَستق ِبل التابعُ ‪ containsValue‬المعاملَ ‪ ،target‬ويُمرِّره مع‬

‫إىل التابع ‪.containsValueHelper‬‬

‫عمل التابع ‪ containsValueHelper‬وف ًقا لما يلي‪:‬‬


‫يَ َ‬

‫ً‬
‫مساوية للقيمة الفارغة ‪،null‬‬ ‫تفحص تعليمة ‪ if‬األوىل الحالة األساسية للتعاود‪ :‬إذا كانت قيمة ‪node‬‬ ‫•‬

‫فإن التابع وصل إىل قاع الشجرة دون إيجاد القيم‪ww‬ة المطلوب‪ww‬ة ‪ ،target‬ويعي‪ww‬د عن‪ww‬دها القيم‪ww‬ة ‪.false‬‬

‫انتبه‪ ،‬يعني ذلك أن القيمة ‪ target‬غير موجود ٍة في واح ٍد فقط من مس‪ww‬ارات الش‪ww‬جرة ال في مس‪ww‬ارات‬

‫الشجرة كلّها‪ ،‬ولذا ما يزال من الممكن العثور عليها في مسارٍ آخر‪.‬‬

‫تفحص تعليمة ‪ if‬الثانية ما إذا كان التابع قد وجد القيمة المطلوبة‪ ،‬وفي تلك الحالة‪ ،‬يعيد التابع القيمة‬ ‫•‬

‫‪ ،true‬أما إذا لم يجدها‪ ،‬فإنه يستمر في البحث‪.‬‬

‫تَستدعِ ي الحالة الشرطية الثالثة التابعَ تعاوديً‪ww‬ا لكي يبحث عن نفس القيم‪ww‬ة‪ ،‬أي ‪ ،target‬في الش‪ww‬جرة‬ ‫•‬
‫الفرعية اليسرى‪ .‬إذا وجدها فيها‪ ،‬فإن‪ww‬ه يعي‪ww‬د القيم‪ww‬ة ‪ true‬مباش‪ً w‬‬
‫‪w‬رة دون أن يح‪ww‬اول البحث في الش‪ww‬جرة‬

‫الفرعية اليمنى‪ ،‬أما إذا لم يجدها فيها‪ ،‬فإنه يستمر في البحث‪.‬‬

‫تبحث الحالة الشرطية الرابعة عن القيمة المطلوبة في الش‪ww‬جرة الفرعي‪ww‬ة اليم‪ww‬نى‪ .‬إذا وج‪ww‬دها فيه‪ww‬ا‪ ،‬فإن‪ww‬ه‬ ‫•‬

‫يعيد القيمة ‪ ،true‬أما إذا لم يجدها‪ ،‬فإنه يعيد القيمة ‪.false‬‬

‫يمرّ التابع السابق عبر كل عقد ٍة من الشجرة‪ ،‬ولهذا‪ ،‬يَستغرِق زمنًا يتناسب مع عدد العقد‪.‬‬

‫‪118‬‬
‫هياكل البيانات للمبرمجين‬ Binary Search Tree ‫شجرة البحث الثنائي‬

put ‫ تنفيذ التابع‬13.3


‫اح‬w‫ون المفت‬w‫دما يك‬w‫ األوىل عن‬:‫التين‬w‫ع ح‬w‫ل م‬w‫ه أن يتعام‬w‫؛ ألن علي‬get ‫ابع‬w‫ أعقد قليال ً من الت‬put ‫يُع ّد التابع‬

‫ون‬ww‫دما ال يك‬ww‫ة عن‬ww‫ والثاني‬،‫ة‬ww‫ وينبغي عندها أن يَستب ِدله ويعيد القيمة القديم‬،‫عطى موجودًا في الشجرة بالفعل‬
َ ‫الم‬
ُ
.‫نشئ عقد ًة جديد ًة ثم يضعها في المكان الصحيح‬
ِ ُ‫ وعندها ينبغي أن ي‬،‫موجودًا‬

:‫كنا قد و ّفرنا الشيفرة المبدئية التالية لذلك التابع في الفصل المذكور‬

public V put(K key, V value) {


if (key == null) {
throw new IllegalArgumentException();
}
if (root == null) {
root = new Node(key, value);
size++;
return null;
}
return putHelper(root, key, value);
}

:‫ انظر إىل شيفرته فيما يلي‬.putHelper ‫وكان المطلوب هو إكمال متن التابع‬

private V putHelper(Node node, K key, V value) {


Comparable<? super K> k = (Comparable<? super K>) key;
int cmp = k.compareTo(node.key);

if (cmp < 0) {
if (node.left == null) {
node.left = new Node(key, value);
size++;
return null;
} else {
return putHelper(node.left, key, value);
}
}
if (cmp > 0) {
if (node.right == null) {
node.right = new Node(key, value);

119
‫هياكل البيانات للمبرمجين‬ ‫شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫;‪size++‬‬
‫;‪return null‬‬
‫{ ‪} else‬‬
‫;)‪return putHelper(node.right, key, value‬‬
‫}‬
‫}‬
‫;‪V oldValue = node.value‬‬
‫;‪node.value = value‬‬
‫;‪return oldValue‬‬
‫}‬

‫يُضبَط المعاملُ األ ّولُ ‪ node‬مبدئ ًيا إىل عقدة الجذر ‪ ،root‬وفي كل مر ٍة نَستدعِ ي فيها الت‪ww‬ابع تعاوديًّا‪ ،‬يش‪ww‬ير‬

‫المعامل إىل شجر ٍة فرع ّيةٍ مختلفةٍ ‪ .‬مثل التابع ‪ ،get‬اِستخدَمنا التابع ‪ compareTo‬لتحديد المسار الذي س‪ww‬نتبعه‬

‫في الشجرة‪ .‬إذا تح ّقق الشرط ‪ ،cmp < 0‬يكون المفتاح المطلوب إضافته أقلّ من ‪ ،node.key‬وعندها يك‪ww‬ون‬

‫علينا فحص الشجرة الفرعية اليسرى‪ .‬هنالك حالتان‪:‬‬

‫ً‬
‫فارغة‪ ،‬فإن ‪ node.left‬تحتوي عىل ‪ ،null‬وعندها نكون قد وصلنا إىل ق‪ww‬اع‬ ‫إذا كانت الشجر ُة الفرع ّي ُة‬ ‫•‬

‫الشجرة دون أن نعثر عىل المفتاح ‪ .key‬في تلك الحالة‪ ،‬نكون قد تأ ّكدنا من أن المفتاح ‪ key‬غير موج‪ww‬ود‬

‫نشئ عق‪ww‬د ًة جدي‪ww‬د ًة‪ ،‬ونض‪ww‬يفها‬


‫في الشجرة‪ ،‬وعرفنا المكان الذي ينبغي أن نضيف المفتاح إليه‪ ،‬ولذلك‪ ،‬ن ُ ِ‬

‫كعقد ٍة ابنةٍ يسرى للعقدة ‪.node‬‬

‫ً‬
‫فارغة‪ ،‬نَستدعِ ي التابع تعاوديًّا للبحث في الشجرة الفرعية اليسرى‪.‬‬ ‫إن لم تكن الشجرة‬ ‫•‬

‫في المقابل‪ ،‬إذا تح ّقق الشرط ‪ ،cmp > 0‬يكون المفتاح المطلوب إضافته أك‪ww‬بر من ‪ ،node.key‬وعن‪ww‬دها‬

‫يكون علينا فحص الشجرة الفرعية اليمنى‪ ،‬وسيكون علينا معالج‪ww‬ة نفس الح‪ww‬التين الس‪ww‬ابقتين‪ .‬أخ‪ww‬ي ًرا‪ ،‬إذا تح ّق‪ww‬ق‬

‫الش‪ww‬رط ‪ ،cmp == 0‬نك‪ww‬ون ق‪ww‬د عثرن‪ww‬ا عىل المفت‪ww‬اح داخ‪ww‬ل الش‪ww‬جرة‪ ،‬وعن‪ww‬دها‪ ،‬نس‪ww‬تطيع أن نس‪ww‬تبدله ونعيد‬

‫القيمة القديمة‪.‬‬

‫ي‪ .‬يُمكِن‪ww‬ك القي‪ww‬ام‬


‫‪w‬لوب تك‪ww‬رار ٍّ‬ ‫كتبنا هذا التابع تعاوديًّا لكي نُس‪w‬هِّل من قراءت‪ww‬ه‪ ،‬ولكن يُمكِن كتابت‪w‬ه ً‬
‫أيض‪w‬ا بأس‪ٍ w‬‬
‫بذلك كتمرين‪.‬‬

‫‪ 13.4‬التنقل بالرتتيب ‪In-order‬‬


‫ً‬
‫مجموعة من النوع ‪ Set‬تحتوي عىل مفاتيح الشجرة ُمرتَّبة‬ ‫كنا قد طلبنا منك كتابة التابع ‪ keySet‬لكي يعيد‬

‫تصاعديًّا‪ .‬ال يعيد هذا التابع في التنفيذات األخ‪ww‬رى من الواجه‪ww‬ة ‪ Map‬المف‪ww‬اتيح وف ًق‪ww‬ا أل ّ‬
‫ي ت‪ww‬رتيب‪ ،‬ولكن ألن ه‪ww‬ذا‬

‫التنفي َذ يتمتع بالبساطة والكفاءة‪ ،‬فإنه يَ َ‬


‫سمح لنا بترتيب المفاتيح‪ ،‬وعلينا أن نَستفيد من ذلك‪.‬‬

‫انظر إىل شيفرة التابع فيما يلي‪:‬‬

‫‪120‬‬
‫هياكل البيانات للمبرمجين‬ ‫شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫{ )(‪public Set<K> keySet‬‬


‫;)(>‪Set<K> set = new LinkedHashSet<K‬‬
‫;)‪addInOrder(root, set‬‬
‫;‪return set‬‬
‫}‬

‫{ )‪private void addInOrder(Node node, Set<K> set‬‬


‫;‪if (node == null) return‬‬
‫;)‪addInOrder(node.left, set‬‬
‫;)‪set.add(node.key‬‬
‫;)‪addInOrder(node.right, set‬‬
‫}‬

‫ً‬
‫قيمة من النوع ‪ LinkedHashSet‬في التابع ‪ .keySet‬يُن ِّفذ ذل‪ww‬ك الن‪ww‬وع الواجه‪ww‬ة ‪Set‬‬ ‫كما ترى فقد أنشأنا‬

‫ويحافظ عىل ترتيب العناصر (بخالف معظم تنفيذات تلك الواجه‪ww‬ة)‪ .‬نَس‪ww‬تدعِ ي بع‪ww‬د ذل‪ww‬ك الت‪ww‬ابع ‪addInOrder‬‬

‫للتنقل في الشجرة‪.‬‬

‫َّ‬
‫تتوقع‪ -‬للتنق‪ww‬ل في الش‪ww‬جرة‬ ‫يشير المعامل األول ‪ node‬مبدئ ًّيا إىل جذر الشجرة‪ ،‬ونَستخدِمه ‪-‬كما يُفترَض أن‬

‫تعاوديًا‪ .‬يجتاز التابع ‪ addInOrder‬الشجرة بأسلوب "في الترتيب" المعروف‪.‬‬

‫ي شي ٍء‬ ‫ٌ‬
‫فارغة‪ ،‬وعندها يعود التابع دون إضافة أ ّ‬ ‫َ‬
‫الفرعية‬ ‫َ‬
‫الشجرة‬ ‫ً‬
‫فارغة‪ ،‬يَعنِي ذلك أن‬ ‫إذا كانت العقدة ‪node‬‬
‫ً‬
‫فارغة‪ ،‬نقوم بما يلي‪:‬‬ ‫إىل المجموعة ‪ ،set‬أما إذا لم تكن‬

‫‪ .1‬نجتاز الشجرة الفرعية اليسرى بالترتيب‪.‬‬

‫‪ .2‬نضيف ‪.node.key‬‬

‫‪ .3‬نجتاز الشجرة الفرعية اليمنى بالترتيب‪.‬‬

‫ت‪ww‬ذ ّكر أن خاص‪ww‬ية ‪ BST‬تض‪ww‬من أن تك‪ww‬ون جمي‪ww‬ع العق‪ww‬د الموج‪ww‬ودة في الش‪ww‬جرة الفرعي‪ww‬ة اليس‪ww‬رى أق‪ww‬لَّ من‬

‫‪ node.key‬وأن تكون جميع العقد الموجودة في الشجرة الفرعية اليمنى أكبرَ من‪ww‬ه‪ ،‬أي أنن‪ww‬ا نض‪ww‬يف ‪node.key‬‬

‫إىل الترتيب الصحيح‪.‬‬

‫بتطبيق نفس المبدأ تعاوديًا‪ ،‬نستنتج أن عناص‪ww‬ر الش‪ww‬جرة الفرعي‪ww‬ة اليس‪ww‬رى واليم‪ww‬نى ُمرتَّب‪ww‬ة‪ ،‬كم‪ww‬ا أن الحال‪ww‬ة‬
‫ً‬
‫فارغة‪ ،‬فإنن‪w‬ا ال نض‪w‬يف أيّ‪w‬ة مف‪w‬اتيح‪ .‬يَعنِي م‪w‬ا س‪w‬بق أن ه‪w‬ذا الت‪w‬ابعَ‬ ‫األساسية صحيحة‪ :‬إذا كانت الشجرة الفرعية‬

‫يضيف جميع المفاتيح وف ًقا لترتيبها الصحيح‪.‬‬

‫وألن هذا التابع يمرّ عبر كل عقد ٍة ضمن الشجرة مثل‪w‬ه مث‪w‬ل الت‪w‬ابع ‪ ،containsValue‬فإن‪w‬ه يَس‪w‬تغرِق زمنً‪w‬ا‬

‫يتناسب مع ‪.n‬‬

‫‪121‬‬
‫هياكل البيانات للمبرمجين‬ ‫شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫‪ 13.5‬التوابع اللوغاريتمية‬
‫يَستغرِق التابعان ‪ get‬و ‪ put‬في الصنف ‪ MyTreeMap‬زمنًا يتناس‪ww‬ب م‪ww‬ع ارتف‪ww‬اع الش‪ww‬جرة ‪ .h‬أوض‪ww‬حنا في‬

‫‪w‬توى منه‪ww‬ا يحت‪ww‬وي عىل الح‪ww‬د األقص‪ww‬ى من ع‪ww‬دد‬ ‫ً‬


‫ممتلئة أي كان كل مس‪w‬‬ ‫الفصل المشار إليه أنه إذا كانت الشجرة‬
‫ً‬
‫العقد المسموح به‪ ،‬فإن ارتفاع تلك الشجرة يكون متناسبًا مع )‪.log(n‬‬

‫نفترض اآلن أن التابعين ‪ get‬و ‪ set‬يستغرقان زمنًا لوغاريتم ًي‪ww‬ا‪ ،‬أي زمنً‪ww‬ا يتناس‪ww‬ب م‪ww‬ع )‪ ،log(n‬م‪ww‬ع أنن‪ww‬ا ال‬
‫ضمن أن تكون الش‪ww‬جرة ممتلئ‪ً w‬‬
‫‪w‬ة دائم‪ww‬اً‪ .‬يعتم‪ww‬د ش‪ww‬كل الش‪ww‬جرة في العم‪ww‬وم عىل المف‪ww‬اتيح وعىل ال‪ww‬ترتيب ال‪ww‬ذي‬ ‫نَ َ‬
‫تُضاف به‪.‬‬

‫سنختبر التنفيذ الذي كتبناه بمجموعتي بيان‪ww‬ات لكي ن‪ww‬رى كي‪ww‬ف يعم‪ww‬ل‪ .‬المجموع‪ww‬ة األوىل عب‪ww‬ار ٌة عن قائم‪ww‬ةٍ‬

‫عالم‪ww‬ات زمن ّي‪ww‬ةٍ ‪timestamp‬‬


‫ٍ‬ ‫تحت‪ww‬وي عىل سالس‪ww‬لَ نص‪ّ ww‬يةٍ عش‪ww‬وائ ّيةٍ ‪ ،‬والثاني‪ww‬ة عب‪ww‬ار ٌة عن قائم‪ww‬ةٍ تحت‪ww‬وي عىل‬

‫ُمرتَّبةٍ تصاعديًّا‪.‬‬

‫ُ‬
‫التالية السالسلَ النص ّية العشوائية‪:‬‬ ‫تُولِّد الشيفر ُة‬

‫;)(>‪Map<String, Integer> map = new MyTreeMap<String, Integer‬‬

‫{ )‪for (int i=0; i<n; i++‬‬


‫;)(‪String uuid = UUID.randomUUID().toString‬‬
‫;)‪map.put(uuid, 0‬‬
‫}‬

‫يق‪ww‬ع تعري‪ww‬ف الص‪ww‬نف ‪ UUID‬ض‪ww‬من حزم‪ww‬ة ‪ ،java.util‬ويُمكِن‪ww‬ه أن يُولِّد ُمع‪ww‬رِّف هوي‪ww‬ةٍ فري‪ww‬دًا عموم ًي‪ww‬ا‬

‫َ‬
‫ذات فائد ٍة كب‪ww‬ير ٍة في مخت ِل‪ww‬ف أن‪ww‬واع‬ ‫بأسلوب عشوائي‪ .‬تُع ّد تلك ُ‬
‫المعرّفات‬ ‫ٍ‬ ‫‪universally unique identifier‬‬

‫التطبيقات‪ ،‬ولكننا سنَستخدِمها في هذا المثال كطريقةٍ سهلةٍ لتوليد سالسلَ نص ّيةٍ عشوائ ّيةٍ ‪.‬‬

‫النه‪ww‬ائي‪ ،‬وحص‪ww‬لنا عىل‬


‫ّ‬ ‫ّ‬
‫ش‪ww‬غلنا الش‪ww‬يفرة التالي‪ww‬ة م‪ww‬ع ‪ n=16384‬وحس‪ww‬بنا زمن التنفي‪ww‬ذ وارتف‪ww‬اع الش‪ww‬جر ِة‬

‫الخرج التالي‪:‬‬

‫‪Time in milliseconds = 151‬‬


‫‪Final size of MyTreeMap = 16384‬‬
‫‪log base 2 of size of MyTreeMap = 14.0‬‬
‫‪Final height of MyTreeMap = 33‬‬

‫أضفنا ً‬
‫أيضا قيمة اللوغاريتم لألساس ‪ 2‬إىل الخريطة لكي نرى طول الشجرة إذا كانت ممتلئة‪ .‬تش‪ww‬ير النتيج‪ww‬ة‬

‫بارتفاع يساوي ‪ 14‬تحتوي عىل ‪ 16,384‬عقدة‪.‬‬ ‫ً‬


‫ممتلئة‬ ‫إىل أن شجر ًة‬
‫ٍ‬

‫‪122‬‬
‫هياكل البيانات للمبرمجين‬ ‫شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫في الواقع‪ ،‬ارتفاع شجرة السالسل النصية العشوائية الفعلي هو ‪ ،33‬وهو أكبر من الحد األدنى النظ‪ww‬ري ولكن‬
‫عقدة‪ ،‬سنضطرّ إلج‪ww‬راء ‪ 33‬موازن‪ً w‬‬
‫‪w‬ة‪ ،‬أي‬ ‫ً‬ ‫مفتاح ضمن تجميعةٍ مكونةٍ من ‪16,384‬‬ ‫ليس بشكل كبير‪ .‬لكي نعثر عىل‬
‫ٍ‬
‫أسر ع بـ‪ً 500‬‬
‫مرة تقريبًا من البحث الخطي ‪.linear search‬‬

‫يُع ّد هذا األداء نموذج ًيا للسالسل النص ّية العشوائ ّية والمفاتيح األخرى التي ال تض‪ww‬اف وف ًق‪ww‬ا أل ّ‬
‫ي ت‪ww‬رتيب‪ .‬رغم‬

‫ي األدنى أو ثالثةِ أضعافِه‪ ،‬فهو ما يزال متناسبًا مع )‪،log(n‬‬


‫النهائي يصل إىل ضعف الح َد النظر َ‬
‫ّ‬ ‫أن ارتفاع الشجرة‬

‫أي أقل بكثير من ‪ ،n‬حيث تنمو قيمة )‪ log(n‬ببط ٍء مع زي‪ww‬ادة قيم‪ww‬ة ‪ n‬لدرج‪ww‬ةٍ يَص‪ww‬عُ ب معه‪ww‬ا التمي‪ww‬يز بين ال‪ww‬زمن‬

‫الثابت والزمن اللوغاريتمي عمل ًيا‪.‬‬

‫دائما‪ .‬ل‪ww‬نرى م‪ww‬ا ق‪ww‬د يح‪ww‬دث عن‪ww‬د إض‪ww‬افة المف‪ww‬اتيح‬


‫ً‬ ‫في المقابل‪ ،‬ال تَ َ‬
‫عمل أشجار البحث الثنائية بهذه الكفاءة‬
‫ً‬
‫زمنية ‪-‬بوحدة النانو ثانية‪ -‬كمفاتيح‪:‬‬ ‫عالمات‬
‫ٍ‬ ‫ي‪ .‬يَستخدِم المثال التالي‬
‫بترتيب تصاعد ٍّ‬
‫ٍ‬

‫;)(>‪MyTreeMap<String, Integer> map = new MyTreeMap<String, Integer‬‬

‫{ )‪for (int i=0; i<n; i++‬‬


‫;))(‪String timestamp = Long.toString(System.nanoTime‬‬
‫;)‪map.put(timestamp, 0‬‬
‫}‬

‫نقض ‪w‬ي بوح‪ww‬دة الن‪ww‬انو ثاني‪ww‬ة‪.‬‬


‫الم ِ‬
‫يعيد ‪ System.nanoTime‬عددًا صحيحًا من النوع ‪ long‬يشير إىل ال‪ww‬زمن ُ‬
‫نحصل عىل عد ٍد أكبرَ قلياًل في كلّ مر ٍة نَستدعيه فيها‪ .‬عندما نُح‪ِّ w‬ول تل‪ww‬ك العالم‪ww‬ات الزمني‪ww‬ة إىل سالس‪ww‬لَ نص‪ّ w‬يةٍ ‪،‬‬

‫فإنها تكون ُمرتَّبة أبجديًّا‪.‬‬

‫لنرى ما نحصل عليه عند التشغيل‪:‬‬

‫‪Time in milliseconds = 1158‬‬


‫‪Final size of MyTreeMap = 16384‬‬
‫‪log base 2 of size of MyTreeMap = 14.0‬‬
‫‪Final height of MyTreeMap = 16384‬‬

‫َ‬
‫سبعة أضعاف زمن التشغيل في الحالة الس‪ww‬ابقة‪ .‬إذا كنت تتس‪ww‬اءل عن‬ ‫يتجاوز زمن التشغيل في هذه الحالة‬

‫النهائي ‪.16384‬‬
‫ّ‬ ‫ً‬
‫نظرة عىل ارتفاع الشجر ِة‬ ‫السبب‪ ،‬فألق‬

‫‪123‬‬
‫هياكل البيانات للمبرمجين‬ ‫شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫عمل بها التابع ‪ ،put‬فقد تفهم م‪ww‬ا يح‪ww‬دث‪ :‬ففي ك‪ww‬ل م‪ww‬ر ٍة نض‪ww‬يف فيه‪ww‬ا‬
‫إذا أمعنت النظر في الطريقة التي يَ َ‬
‫دائم‪ w‬ا الختي‪ww‬ار الش‪ww‬جرة‬
‫ً‬ ‫مفتاحً ا جديدًا‪ ،‬فإنه يكون أكبر من جميع المفاتيح الموجودة في الشجرة‪ ،‬وبالتالي‪ ،‬نض‪ww‬طرّ‬
‫دائما العقدة الجديدة كعقدة ابن يمنى للعقدة الواقعة عىل أقصى اليمين‪ .‬نحصل ب‪ww‬ذلك‬
‫ً‬ ‫الفرعية اليمنى‪ ،‬ونضيف‬

‫عىل شجرة غير متزنة ‪ unbalanced‬تحتوي عىل عق ٍد أبناء يمنى فقط‪.‬‬

‫يتناس‪www‬ب ارتف‪www‬اع تل‪www‬ك الش‪www‬جرة م‪www‬ع ‪ n‬وليس )‪ ،log(n‬ول‪www‬ذلك يص‪www‬بح أداء الت‪www‬ابعين ‪ get‬و ‪ set‬خط ًيا‬

‫ال لوغاريتم ًيا‪.‬‬

‫تَعرِض الصورة السابقة مثااًل عن ش‪ww‬جرتين إح‪ww‬داهما متزن‪ww‬ة واألخ‪ww‬رى غ‪ww‬ير متزن‪ww‬ة‪ .‬يُمكِنن‪ww‬ا أن ن‪ww‬رى أن ارتف‪ww‬اع‬

‫الشجرة المتزنة يساوي ‪ 4‬وعدد العقد الكلية يساوي ‪ 1−24‬أي ‪ .15‬تحتوي الش‪ww‬جرة غ‪ww‬ير المتزن‪ww‬ة عىل نفس ع‪ww‬دد‬

‫العقد‪ ،‬ولكن ارتفاعها يساوي ‪.15‬‬

‫‪ 13.6‬األشجار المزتنة ذاتيا ‪Self-balancing trees‬‬


‫هناك حاّل ن محتمالن لتلك المشكلة‪:‬‬

‫دائما‪.‬‬
‫ً‬ ‫يُمكِننا أن نتجنّب إضافة المفاتيح إىل الخريطة بالترتيب‪ ،‬ولكن هذا الحل ليس ممكنًا‬ ‫•‬

‫نشئ شجر ًة قادر ًة عىل التعامل مع المفاتيح المرتّبة تعاماًل أفضل‪.‬‬


‫يُمكِننا أن ن ُ ِ‬ ‫•‬

‫عديدة لتنفيذه‪ .‬يُمكِننا مثاًل أن نُعدّل الت‪ww‬ابع ‪ put‬لكي نجعل‪ww‬ه يفحص‬


‫ٌ‬ ‫ُ‬
‫طرائق‬ ‫يبدو الحل الثاني أفضل‪ ،‬وتتو ّفر‬

‫ما إذا كانت الشجرة قد أصبحت غير متزنة‪ ،‬وعندها‪ ،‬يعيد ترتيب العق‪ww‬د‪ .‬يُطلَ‪ww‬ق عىل األش‪ww‬جار ال‪ww‬تي تتم‪ww‬يز بتل‪ww‬ك‬

‫المقدرة اس‪ww‬م "األش‪ww‬جار المتزن‪ww‬ة ذات ًي‪ww‬ا"‪ ،‬ومن أش‪ww‬هرها ش‪ww‬جرة ‪( AVL‬اختص‪ww‬ار ‪ Adelson-Velskii Tree‬حيث إن‬

‫‪ Adelson‬و ‪ Velskii‬هما مبتكرا هذه الشجرة)‪ ،‬وشجرة ‪ red-black‬التي يَستخدِمها صنف الجافا ‪.TreeMap‬‬

‫إذا استخدمنا الصنف ‪ TreeMap‬بداًل من الصنف ‪ MyTreeMap‬في الشيفرة السابقة‪ ،‬سيصبح زمن تشغيل‬

‫ومثال العالمات الزمنية هو نفسه‪ ،‬بل في الحقيقة‪ ،‬سيكون مثال العالمات الزمنية أسر ع‬
‫ِ‬ ‫مثال السالسل النصية‬
‫ِ‬
‫عىل الرغم من أن المفاتيح ُمرتَّبة؛ ألنه يَستغرِق وقتًا أقل لحساب شيفرة التعمية ‪.hash‬‬

‫‪124‬‬
‫هياكل البيانات للمبرمجين‬ ‫شجرة البحث الثنائي ‪Binary Search Tree‬‬

‫ٌ‬
‫قادرة عىل تنفيذ التابعين ‪ get‬و ‪ put‬بزمن لوغاريتمي بش‪ww‬رط‬ ‫نستخلص مما سبق أن أشجار البحث الثنائية‬

‫كاف‪ .‬في المقابل‪ ،‬تتجنَّب األشجار المتزنة ذات ًيا تلك‬


‫ٍ‬ ‫بشكل‬
‫ٍ‬ ‫لترتيب يحافظ عىل اتزانها‬
‫ٍ‬ ‫إضافة المفاتيح إليها وف ًقا‬

‫المشكلة بإنجاز بعض العمل اإلضافي في كلّ مر ٍة يُضاف فيها مفتاح جديد‪.‬‬

‫يُمكِنك قراءة المزيد عن األشجار المتزنة ذات ًّيا‪.‬‬

‫‪ 13.7‬تمرين إضافي‬
‫لم نُن ِّفذ التابع ‪ remove‬في ذاك التمرين‪ ،‬ولكن يُمكِنك أن تُج‪w‬رِّب كتابت‪ww‬ه اآلن‪ .‬إذا ح‪ww‬ذفت عق‪ً w‬‬
‫‪w‬دة من وس‪ww‬ط‬

‫الشجرة‪ ،‬ستضطرّ إىل إعادة ترتيب العقد المتبقية لكي تحافظ عىل خاصية ‪ .BST‬ربما بإمكانك التفكير في طريقة‬

‫االطالع عىل الرابط (باللغة اإلنجليزية)‪.‬‬


‫ّ‬ ‫إنجاز ذلك أو‬

‫تُع ّد عمليتا حذف عقد ٍة وإعادة الشجرة إىل االتزان عمليتين متشابهتين‪ ،‬لذا إذا أتممت هذا التمرين‪ ،‬ستفهم‬

‫فهما أعمق‪.‬‬
‫ً‬ ‫طريقة عمل األشجار المتزنة ذات ًيا‬

‫‪125‬‬
‫‪ .14‬حفظ البيانات عرب ‪Redis‬‬

‫سنُكمِ ل في التمارين القليلة القادمة بناء محرك البحث الذي تحدثنا عن‪ww‬ه في الفص‪ww‬ل الس‪ww‬ادس التنق‪ww‬ل في‬

‫بحث من الوظائف التالية‪:‬‬


‫ٍ‬ ‫ي محرك‬
‫الشجرة‪ .‬يتك َّون أ ّ‬

‫برنامج بإمكانه تحميلُ صفحة إنترنت وتحليلُها واس‪ww‬تخراجُ النص وأ ِّ‬


‫ي‬ ‫ٍ‬ ‫الزحف ‪ :crawling‬ويُن ّف ُذ من خالل‬ ‫•‬

‫صفحات أخرى‪.‬‬
‫ٍ‬ ‫روابط إىل‬

‫الفهرسة ‪ :indexing‬وتن ّف ُذ من خالل هيكل بيانات ‪ data structure‬بإمكانه البحث عن كلم‪ww‬ة والعث‪ww‬ور‬ ‫•‬

‫عىل الصفحات التي تحتوي عىل تلك الكلمة‪.‬‬

‫المفه‪ww‬رِس واختي‪ww‬ار الص‪ww‬فحات األك‪ww‬ثر ص‪ww‬لة بكلم‪ww‬ات‬ ‫ٌ‬


‫طريقة لتجمي‪ww‬ع نت‪ww‬ائج ُ‬ ‫االسترجاع ‪ :retrieval‬وهي‬ ‫•‬

‫البحث‪.‬‬

‫فهرسا بالفعل باستخدام خرائط‬


‫ً‬ ‫إذا كنت قد أتممت تمرين الفصل الثامن المفهرس ‪ ،Indexer‬فقد ن َّفذت ُم‬
‫ً‬
‫جديدة تُخزِّن النتائج في قاعدة بيانات‪.‬‬ ‫ً‬
‫نسخة‬ ‫ُنشئ‬
‫جافا‪ .‬سنناقش هذا التمرين هنا وسن ِ‬

‫وإذا كنت قد أكملت تمرين الفصل السابع كل الط‪ww‬رق ت‪ww‬ؤدي إىل روما‪ ،‬فق‪ww‬د ن َّفذت بالفع‪ww‬ل زاح ًف‪ww‬ا يَت ِب‪ww‬ع أول‬

‫رابط تجده في رتل ‪ ،queue‬وتتبع تل‪ww‬ك الرواب‪ww‬ط‬


‫ٍ‬ ‫أعم تُخزِّن كل‬ ‫ً‬
‫نسخة ّ‬ ‫ُنشئ في التمرين التالي‬
‫رابط يعثرُ عليه‪ .‬سن ِ‬
‫ٍ‬

‫بالترتيب‪.‬‬

‫في النهاية‪ ،‬ستُكلّف بالعمل عىل برنامج االسترجاع‪.‬‬

‫ً‬
‫فرصة أكبر التخ‪w‬اذ الق‪w‬رارات المتعلق‪ww‬ة بالتص‪ww‬ميم‪.‬‬ ‫شيفرة مبدئ ّي ًة أقصر في هذه التمارين‪ ،‬وسنعطيك‬
‫ً‬ ‫سنُو ِّفر‬

‫وتجدر اإلشارة إىل أن هذه التمارين ذات نهايات مفتوحة‪ ،‬أي سنطرح عليك فقط بعض األهداف البس‪ww‬يطة ال‪ww‬تي‬

‫قدما إذا أردت المزيد من التحدي‪.‬‬


‫ً‬ ‫ضي‬
‫الم ِ‬
‫يتعين عليك الوصول إليها‪ ،‬ولكنك تستطيع بالطبع ُ‬
‫هياكل البيانات للمبرمجين‬ ‫حفظ البيانات عبر ‪Redis‬‬

‫المفهرِس‪.‬‬
‫واآلن‪ ،‬سنبدأ بالنسخة الجديدة من ُ‬

‫‪ 14.1‬قاعدة بيانات ‪Redis‬‬


‫بيانات‪ :‬األول هو كائنٌ من الن‪ww‬وع ‪TermCounter‬‬ ‫ي‬ ‫َ‬ ‫تُخزِّن النسخة السابقة من ُ‬
‫ٍ‬ ‫المفهرِس البيانات في هيكل ْ‬
‫بحث بعدد المرات التي ظهرت فيها الكلمة في ص‪ww‬فحة إن‪ww‬ترنت معين‪ww‬ةٍ ‪ ،‬والث‪ww‬اني ك‪ww‬ائنٌ من الن‪ww‬وع‬
‫ٍ‬ ‫يَربُط كل كلمة‬

‫‪ Index‬يربُط كلمة البحث بمجموعة الصفحات التي ظهرت فيها‪.‬‬

‫يُخزَّن هيكال البيانات في ذاكرة التطبيق‪ ،‬ولذا يتالشيان بمجرد انتهاء البرنامج‪ .‬توص‪ww‬ف البيان‪ww‬ات ال‪ww‬تي تُخ‪w‬زَّن‬

‫فقط في ذاكرة التطبيق بأنها "متطايرة ‪"volatile‬؛ ألنها تزول بمجرد انتهاء البرنامج‪.‬‬

‫في المقاب‪ww‬ل‪ ،‬تُوص‪ww‬ف البيان‪ww‬ات ال‪ww‬تي تظ‪ww‬ل موج‪ww‬ود ًة بع‪ww‬د انته‪ww‬اء البرن‪ww‬امج ال‪ww‬ذي أنش‪ww‬أها بأنه‪ww‬ا "مس‪ww‬تمرة‬

‫المخزَّنة‬
‫المخزَّنة في نظام الملفات فهي ُمستمرة في العموم‪ ،‬وكذلك البيانات ُ‬
‫‪ ."persistent‬مثال ذلك الملفات ُ‬
‫في قاعدة بيانات ً‬
‫أيضا مستمرة‪.‬‬

‫يُع ّد تخزين البيانات في ملف واح‪w‬دة من أبس‪ww‬ط ط‪ww‬رق حف‪ww‬ظ البيان‪ww‬ات‪ ،‬ف ُيمكِنن‪ww‬ا ببس‪ww‬اطة أن نح‪ِّ w‬ول هياك‪ww‬ل‬

‫تضمنة للبيانات إىل صيغة ‪ JSON‬ثم نكتبها إىل ملف قبل انتهاء البرنامج‪ .‬عندما نُش ِّغل البرنامج م‪ww‬رة‬
‫ِّ‬ ‫الم‬
‫البيانات ُ‬
‫أخرى‪ ،‬سنتمكَّن من قراءة الملف وإعادة بناء هياكل البيانات‪.‬‬

‫ولكن هناك مشكلتان في هذا الحل‪:‬‬

‫‪ .1‬عاد ًة ما تكون عمليتا قراءة هياكل البيانات الضخمة (مثل مفهرس الويب) وكتابتها عمليتين بطيئتين‪.‬‬

‫‪ .2‬قد ال تستوعب مساحة ذاكرة برنامج واحد هيكل البيانات بأكمله‪.‬‬

‫نحو غير متوقع (نتيجة النقطاع الكهرباء مثاًل )‪ ،‬سنفقد جميع التغييرات التي‬
‫ٍ‬ ‫‪ .3‬إذا انتهى برنامجٌ معينٌ عىل‬

‫أجريناها عىل البيانات منذ آخر مرة فتحنا فيها البرنامج‪.‬‬

‫وهناك طريقة أخرى لحفظ البيانات وهي قواعد البيانات‪ .‬إذ تُع ّد قواع ُد البيانات البديلَ األفض‪ww‬لَ ‪ ،‬فهي تُ‪ww‬و ِّفر‬
‫ٌ‬
‫قادرة عىل قراءة جز ٍء من قاعدة البيانات أو كتابته دون الحاج‪ww‬ة إىل ق‪ww‬راءة قاع‪ww‬دة‬ ‫ً‬
‫مستمرة‪ ،‬كما أنها‬ ‫تخزين‬ ‫َ‬
‫مساحة‬
‫ٍ‬
‫البيانات أو كتابتها بالكامل‪.‬‬

‫تتو َّفر الكثير من أنواع نظم إدارة قواعد البيانات ‪ ،DBMS‬ويتمتع ك ٌّ‬
‫ل منها بإمكانيات مختلفة‪ .‬ويُمكِنك ق‪ww‬راءة‬

‫مقارنة بين أنظمة إدارة قواعد البيانات العالقية واالطالع عىل سلسلة تصميم قواعد البيانات‪.‬‬

‫تُو ِّفر قاعدة بيانات ‪- Redis‬التي سنَستخدِمها في ه‪ww‬ذا التم‪ww‬رين‪ -‬هياك‪ww‬ل بيان‪ww‬ات مس‪ww‬تمرة مش‪ً w‬‬
‫‪w‬ابهة لهياك‪ww‬ل‬

‫البيانات التي تُو ِّفرها جافا‪ ،‬فهي تُو ِّفر‪:‬‬

‫قائمة سالسل نصية مشابهة للنوع ‪.List‬‬ ‫•‬

‫جداول مشابهة للنوع ‪.Map‬‬ ‫•‬

‫‪127‬‬
‫هياكل البيانات للمبرمجين‬ ‫حفظ البيانات عبر ‪Redis‬‬

‫مجموعات من السالسل النصية مشابهة للنوع ‪.Set‬‬ ‫•‬

‫تُع ّد ‪ Redis‬قاعدة بيانات من نوع زوج مفتاح‪/‬قيمة‪ ،‬ويَع‪ww‬ني ذل‪ww‬ك أن هياك‪ww‬ل البيان‪ww‬ات (القيم) ال‪ww‬تي تُخزِّنه‪ww‬ا‬
‫تكون ُمعرَّ ً‬
‫فة بواسطة سالسل نصية فريدة (مفاتيح)‪ .‬تلعب المفاتيح في قاعدة بيان‪ww‬ات ‪ Redis‬نفس ال‪ww‬دور ال‪ww‬ذي‬
‫ً‬
‫أمثلة عىل ذلك الح ًقا‪.‬‬ ‫تلعبه المراجع ‪ references‬في لغة جافا‪ ،‬أي أنّها تُعرِّف هوية الكائنات‪ .‬سنرى‬

‫‪ 14.2‬خوادم وعمالء ‪Redis‬‬


‫ً‬
‫عادة ما تَ َ‬
‫عمل قاعدة بيانات ‪ Redis‬كخدمةٍ عن بعد‪ ،‬فكلمة ‪ Redis‬هي في الحقيق‪ww‬ة اختص‪ww‬ار لعب‪ww‬ارة "خ‪ww‬ادم‬

‫قاموس عن بعد ‪ ،"REmote DIctionary Server‬ولنتمكن من استخدامها‪ ،‬علينا أن نُش‪ِّ w‬غل خ‪w‬ادم ‪ Redis‬في‬

‫مكان ما ثم ن َ ِ‬
‫تصل به عبر عميل ‪ .Redis‬من الممكن إعداد ذلك الخادم بأكثرَ من طريقةٍ ‪ ،‬كما يُمكِنن‪ww‬ا االختي‪ww‬ار من‬ ‫ٍ‬
‫بين العديد من برامج العمالء‪ ،‬وسنَستخدِم في هذا التمرين ما يلي‪:‬‬

‫‪ .1‬بداًل من أن نُثبِّت الخادم ونُش ِّغله بأنفسنا‪ ،‬سنَس‪ww‬تخدِم خدم‪ً w‬‬


‫‪w‬ة مث‪ww‬ل ‪ .RedisToGo‬تُش ‪ِّ w‬غل تل‪ww‬ك الخدم‪ww‬ة‬
‫‪w‬ة مجاني‪ً w‬‬
‫‪w‬ة بم‪ww‬وار َد تتناس‪ww‬ب م‪ww‬ع متطلب‪ww‬ات ه‪ww‬ذا‬ ‫قاعدة بيانات ‪ Redis‬عىل الس‪ww‬حابة ‪ ،cloud‬وتُق‪w‬دِّم خط‪ً w‬‬

‫التمرين‪.‬‬

‫أصناف وتوابعَ يُمكِنها العم‪ww‬ل‬


‫ٍ‬ ‫ٌ‬
‫عبارة عن مكتبة جافا تحتوي عىل‬ ‫‪ .2‬بالنسبة للعميل‪ ،‬سنَستخدِم ‪ ،Jedis‬وهو‬

‫مع قاعدة بيانات ‪.Redis‬‬

‫فصلة التالية لمساعدتك عىل البدء‪:‬‬


‫الم َّ‬
‫انظر إىل التعليمات ُ‬

‫أنشئ حسابًا عىل موقع ‪ ،RedisToGo‬واختر الخطة التي تريدها (ربما الخطة المجانية)‪.‬‬
‫ِ‬ ‫•‬

‫أنشئ نسخة آلة افتراضية يعم‪w‬ل عليه‪w‬ا خ‪w‬ادم ‪ .Redis‬إذا نق‪w‬رت عىل تب‪w‬ويب "‪ ،"Instances‬س‪w‬تجد أن‬
‫ِ‬ ‫•‬

‫النسخة الجديدة ُمعرَّفة باسم استضافة ورقم منفذ‪ .‬كان اسم النسخة الخاصة بنا مثاًل هو ‪.dory-10534‬‬

‫انقر عىل اسم النسخة لكي تفتح صفحة اإلعدادات‪ ،‬وسجِّل اسم ُمح ‪w‬دّد الم‪ww‬وارد الموح‪ww‬د ‪ URL‬الموج‪ww‬ود‬ ‫•‬

‫أعىل الصفحة‪ .‬سيكون مشابهً ا لما يلي‪:‬‬

‫‪redis://redistogo:1234567feedfacebeefa1e1234567@dory.redistogo.com:10534‬‬

‫يحتوي محدد الموارد الموحد السابق ذكره عىل اسم االستضافة الخاص بالخادم ‪،dory.redistogo.com‬‬

‫المك َّون‪ww‬ة‬
‫ورقم المنفذ ‪ ،10534‬وكلمة المرور التي سنحتاج إليها لالتصال بالخادم‪ ،‬وهي السلسلة النص‪ww‬ية الطويل‪ww‬ة ُ‬
‫من أحرف وأعدا ٍد في المنتصف‪ .‬ستحتاج تلك المعلومات في الخطوة التالية‪.‬‬

‫‪ 14.3‬إنشاء مفهرس يعتمد عىل ‪Redis‬‬


‫ستجد الملفات التالية الخاصة بالتمرين في مستودع الكتاب‪:‬‬

‫‪128‬‬
‫هياكل البيانات للمبرمجين‬ ‫حفظ البيانات عبر ‪Redis‬‬

‫‪ :JedisMaker.java‬يحت‪ww‬وي عىل بعض األمثل‪ww‬ة عىل االتص‪ww‬ال بخ‪ww‬ادم ‪ Redis‬وتش‪ww‬غيل بعض تواب‪ww‬ع‬ ‫•‬

‫‪.Jedis‬‬

‫‪ :JedisIndex.java‬يحتوي عىل شيفر ٍة مبدئيةٍ لهذا التمرين‪.‬‬ ‫•‬

‫‪ :JedisIndexTest.java‬يحتوي عىل اختبارات للصنف ‪.JedisIndex‬‬ ‫•‬

‫‪ :WikiFetcher.java‬يحتوي عىل شيفر ٍة تقرأ ص‪ww‬فحات إن‪ww‬ترنت وتُحلِّله‪ww‬ا باس‪ww‬تخدام مكتب‪ww‬ة ‪.jsoup‬‬ ‫•‬

‫كتبنا تلك الشيفرة في تمارين الفصول المشار إليها باألعىل‪.‬‬

‫ستجد ً‬
‫أيضا الملفات التالية التي كتبناها في نفس تلك التمارين‪:‬‬

‫سا باستخدام هياكل بيانات تُو ِّفرها جافا‪.‬‬


‫‪ :Index.java‬يُن ِّفذ مفهر ِ ً‬ ‫•‬

‫ً‬
‫خريطة تربُط كلمات البحث بعدد مرات حدوثها‪.‬‬ ‫‪ :TermCounter.java‬يُمثِل‬ ‫•‬

‫‪ :WikiNodeIterable.java‬يمرّ عبر عقد شجرة ‪ DOM‬الناتجة من مكتبة ‪.jsoup‬‬ ‫•‬

‫إذا تمكَّنت من كتابة نسخك الخاصة من تلك الملف‪ww‬ات‪ ،‬يُمكِن‪ww‬ك اس‪ww‬تخدامها له‪ww‬ذا التم‪ww‬رين‪ .‬إذا لم تكن ق‪ww‬د‬

‫أكملت تلك التمارين أو أكملتها ولكنك غير متأ ّكد مم‪ww‬ا إذا ك‪ww‬انت تَ َ‬
‫عم‪ w‬ل عىل النح‪ww‬و الص‪ww‬حيح‪ ،‬يُمكِن‪ww‬ك أن تَ َ‬
‫نس‪w‬خ‬

‫الحلول من مجلد ‪.solutions‬‬

‫واآلن‪ ،‬ستكون خطوتك األوىل هي استخدام عميل ‪ Jedis‬لالتصال بخادم ‪ Redis‬الخاص بك‪ .‬يُ ِّ‬
‫وضح الص‪ww‬نف‬

‫‪w‬جل‬ ‫ٍّ‬
‫ملف‪ ،‬ثم يتص‪ww‬ل ب‪ww‬ه‪ ،‬ويُس‪ِ w‬‬ ‫‪ RedisMaker.java‬طريقة القيام بذلك‪ :‬عليه أواًل أن يقرأ معلومات الخادم من‬

‫دخوله باستخدام كلمة المرور‪ ،‬وأخي ًرا‪ ،‬يُعيد كائنًا من النوع ‪ Jedis‬الذي يُمكِن استخدامه لتنفيذ عمليات ‪.Redis‬‬

‫س‪ww‬تجد الص‪ww‬نف ‪ُ JedisMaker‬معرَّ ًف‪ww‬ا في المل‪ww‬ف ‪ .JedisMaker.java‬يَ َ‬


‫عم‪ w‬ل ذل‪ww‬ك الص‪ww‬نف كص‪ww‬نف‬

‫نش‪w‬ئ كائنً‪ww‬ا من الن‪ww‬وع ‪ .Jedis‬يُمكِن‪ww‬ك أن تَس‪ww‬تخدِم ذل‪ww‬ك‬


‫مساعد حيث يحتوي عىل التابع الساكن ‪ make‬الذي يُ ِ‬

‫الكائن بعد التصديق عليه لالتصال بقاعدة بيانات ‪ Redis‬الخاصة بك‪.‬‬

‫ٍّ‬
‫مل‪w‬ف اس‪w‬مه ‪ redis_url.txt‬موج‪w‬و ٍد في المجل‪w‬د‬ ‫يقرأ الصنف ‪ JedisMaker‬بيانات خ‪ww‬ادم ‪ Redis‬من‬

‫‪:src/resources‬‬

‫نصوص إلنشاء وتعديل الملف التالي‪:‬‬


‫ٍ‬ ‫استخدم ُمحرّ َر‬ ‫•‬

‫‪ThinkDataStructures/code/src/resources/redis_url.txt.‬‬

‫ضع فيه ُمحدّد موارد الخادم الخاص بك‪ .‬إذا كنت تَستخدِم خدمة ‪ ،RedisToGo‬سيكون مح‪ww‬دد الم‪ww‬وارد‬ ‫•‬

‫مشابهً ا لما يلي‪:‬‬

‫‪129‬‬
‫هياكل البيانات للمبرمجين‬ Redis ‫حفظ البيانات عبر‬

redis://
redistogo:1234567feedfacebeefa1e1234567@dory.redistogo.com:10534

‫ك عن‬ww‫وع ذل‬ww‫ك تجنُّب وق‬ww‫ يُمكِن‬.Redis ‫ادم‬ww‫رور خ‬ww‫ال تضع هذا الملف في مجل ٍد عا ٍّم ألنه يحتوي عىل كلمة م‬

.‫ الموجود في مستودع الكتاب لمنع وضع الملف فيه‬.gitignore ‫طريق الخطأ باستخدام الملف‬

w‫ لتشغيل المثال التوض‬ant JedisMaker ‫ لتصريف ملفات الشيفرة واألمر‬ant build ‫ن ِّفذ األمر‬
‫يحي‬w
ّ
:main ‫بالتابع‬

public static void main(String[] args) {


Jedis jedis = make();

// String
jedis.set("mykey", "myvalue");
String value = jedis.get("mykey");
System.out.println("Got value: " + value);

// Set
jedis.sadd("myset", "element1", "element2", "element3");
System.out.println("element2 is member: " +
jedis.sismember("myset", "element2"));

// List
jedis.rpush("mylist", "element1", "element2", "element3");
System.out.println("element at index 1: " +
jedis.lindex("mylist", 1));

// Hash
jedis.hset("myhash", "word1", Integer.toString(2));
jedis.hincrBy("myhash", "word2", 1);
System.out.println("frequency of word1: " +
jedis.hget("myhash", "word1"));
System.out.println("frequency of word1: " +
jedis.hget("myhash", "word2"));

jedis.close();
}

130
‫هياكل البيانات للمبرمجين‬ ‫حفظ البيانات عبر ‪Redis‬‬

‫يَعرِض المثال أنواع البيانات والتوابع التي ستحتاج إليها غالبًا في هذا التمرين‪ .‬ينبغي أن تحصل عىل الخرج‬

‫التالي عند تشغيله‪:‬‬

‫‪Got value: myvalue‬‬


‫‪element2 is member: true‬‬
‫‪element at index 1: element2‬‬
‫‪frequency of word1: 2‬‬
‫‪frequency of word2: 1‬‬

‫سنشرح طريقة عمل تلك الشيفرة في القسم التالي‪.‬‬

‫‪ 14.4‬أنواع البيانات في قاعدة بيانات ‪Redis‬‬

‫تَ َ‬
‫عمل ‪ Redis‬كخريطةٍ تربط مفاتيحَ (سالسل نص ّية) بقيم‪ .‬قد تنتمي تلك القيم إىل مجموع‪ww‬ة أن‪ww‬واع مختلف‪ww‬ة‬

‫من البيانات‪ .‬يُع ّد النوع ‪ string‬واحدًا من أبسط األنواع التي تُو ِّفره‪ww‬ا قاع‪ww‬دة بيان‪ww‬ات ‪ .Redis‬الح‪ww‬ظ أنن‪ww‬ا س‪ww‬نكتب‬

‫أنواع بيانات ‪ Redis‬بخطوط مائلة لنُميزها عن أنواع جافا‪.‬‬

‫سنَستخدِم التابع ‪ jedis.set‬إلضافة سلسلةٍ نص ّيةٍ من النوع ‪ string‬إىل قاع‪w‬دة البيان‪w‬ات‪ .‬ق‪w‬د تج‪w‬د ذل‪w‬ك‬

‫مألو ًفا‪ ،‬فه‪ww‬و يش‪ww‬به الت‪ww‬ابع ‪ ،Map.put‬حيث تُم ِث‪ww‬ل المع‪ww‬امالت ُ‬


‫المم‪w‬رَّرة المفت‪ww‬اح الجدي‪ww‬د وقيمت‪ww‬ه المقابل‪ww‬ة‪ .‬في‬

‫معين والعثور عىل قيمته‪ .‬انظر الشيفرة إىل التالية‪:‬‬


‫ٍ‬ ‫مفتاح‬
‫ٍ‬ ‫المقابل‪ ،‬يُستخدَم التابع ‪ jedis.get‬للبحث عن‬

‫;)"‪jedis.set("mykey", "myvalue‬‬
‫;)"‪String value = jedis.get("mykey‬‬

‫كان المفتاح هو "‪ "mykey‬والقيمة هي "‪ "myvalue‬في هذا المثال‪.‬‬

‫‪w‬كل مش‪ww‬ابهٍ للن‪ww‬وع >‪ Set<String‬في جاف‪ww‬ا‪ .‬إذا أردت أن‬ ‫تُو ِّفر ‪ Redis‬هيكل البيانات ‪ set‬ال‪ww‬ذي يَ َ‬
‫عم‪ w‬ل بش‪ٍ w‬‬
‫ح‪w‬ا يُح‪w‬دّد هوي‪w‬ة تل‪w‬ك المجموع‪w‬ة‪ ،‬ثم اِس‪w‬تخدِم الت‪w‬ابع‬
‫تضيف عنص ًرا جديدًا إىل مجموعةٍ من النوع ‪ ،set‬اختر مفتا ً‬
‫‪ jedis.sadd‬كما يلي‪:‬‬

‫;)"‪jedis.sadd("myset", "element1", "element2", "element3‬‬


‫;)"‪boolean flag = jedis.sismember("myset", "element2‬‬

‫الحِ ظ أنه ليس من الضروري إنشاء المجموعة بخط‪ww‬و ٍة منفص‪ww‬لةٍ ‪ ،‬حيث تُنش‪ww‬ؤها ‪ Redis‬إن لم تكن موج‪ww‬ود ًة‪.‬‬
‫ً‬
‫مجموعة من النوع ‪ set‬اسمها ‪ myset‬تحتوي عىل ثالثة عناصر‪.‬‬ ‫تُ ِ‬
‫نشئ ‪ Redis‬في هذا المثال‬

‫يفحص التابع ‪ jedis.sismember‬ما إذا كان عنصر معين موجودًا في مجموعة من الن‪ww‬وع ‪ .set‬تَس‪ww‬تغرِق‬

‫عمليتا إضافة العناصر وفحص عضويّتها زمنًا ثابتًا‪.‬‬

‫‪131‬‬
‫هياكل البيانات للمبرمجين‬ ‫حفظ البيانات عبر ‪Redis‬‬

‫تُ‪ww‬و ِّفر ‪ً Redis‬‬


‫أيض‪ww‬ا هيك‪ww‬ل البيان‪ww‬ات ‪ list‬ال‪ww‬ذي يش‪ww‬به الن‪ww‬وع >‪ List<String‬في جاف‪ww‬ا‪ .‬يض‪ww‬يف الت‪ww‬ابع‬

‫‪ jedis.rpush‬العناصر إىل النهاية اليمنى من القائمة‪ ،‬كما يلي‪:‬‬

‫;)"‪jedis.rpush("mylist", "element1", "element2", "element3‬‬


‫;)‪String element = jedis.lindex("mylist", 1‬‬

‫نشئ هذا المث‪ww‬ال قائم‪ً w‬‬


‫‪w‬ة من‬ ‫مثلما سبق‪ ،‬ال يُع ّد إنشاء هيكل البيانات قبل إضافة العناصر إليه أمرًا ضروريًا‪ .‬يُ ِ‬

‫النوع ‪ list‬اسمها ‪ mylist‬تحتوي عىل ثالثة عناصر‪.‬‬

‫ص‪w‬حيح‪ ،‬ويعي‪w‬د عنص‪w‬رَ القائم‪ww‬ةِ المش‪w‬ا َر إلي‪w‬ه‪.‬‬


‫ٍ‬ ‫فهرس‪w‬ا ه‪ww‬و عب‪ٌ w‬‬
‫‪w‬ارة عن ع‪ww‬د ٍد‬ ‫ً‬ ‫يَستق ِبل التابع ‪jedis.lindex‬‬

‫تَستغرِق عمليتا إضافة العناصر واسترجاعها زمنًا ثابتًا‪.‬‬

‫أخيرًا‪ ،‬تُو ِّفر ‪ Redis‬الهيكل ‪ hash‬الذي يش‪ww‬به الن‪w‬وع >‪ Map<String, String‬في جاف‪ww‬ا‪ .‬يض‪ww‬يف الت‪ww‬ابع‬

‫خاًل جديدًا إىل الجدول عىل النحو التالي‪:‬‬


‫‪ُ jedis.hset‬مد َ‬

‫;))‪jedis.hset("myhash", "word1", Integer.toString(2‬‬


‫;)"‪String value = jedis.hget("myhash", "word1‬‬

‫مدخل واح ٍد يربُط المفتاح ‪ word1‬بالقيمة "‪."2‬‬


‫ٍ‬ ‫نشئ هذا المثال جدواًل جديدًا اسمه ‪ myhash‬يحتوي عىل‬
‫يُ ِ‬

‫تنتمي المفاتيح والقيم إىل النوع ‪ ،string‬ولذلك‪ ،‬إذا أردنا أن نُخزِّن عددًا صحيحًا من النوع ‪ ،Integer‬علينا‬

‫أن نُح ِّوله أواًل إىل النوع ‪ String‬قبل أن نَستدعِ ي التابع ‪ .hset‬وبالمثل‪ ،‬عندما نبحث عن قيمة باستخدام التابع‬
‫‪ ،hget‬ستكون النتيجة من النوع ‪ ،String‬ولذلك‪ ،‬قد نضطرّ إىل تحويلها ً‬
‫مرة أخرى إىل النوع ‪.Integer‬‬

‫قد يكون العمل مع النوع ‪ hash‬في قاع‪ww‬دة بيان‪ww‬ات ‪ Redis‬مرب ًك‪ww‬ا نوعً‪ w‬ا م‪w‬ا؛ ألنن‪ww‬ا نَس‪ww‬تخدِم مفت‪ww‬احين‪ ،‬األول‬

‫لتحديد الجدول الذي نريده‪ ،‬والثاني لتحديد القيمة ال‪ww‬تي نري‪ww‬دها من الج‪ww‬دول‪ .‬في س‪ww‬ياق قاع‪ww‬دة بيان‪ww‬ات ‪،Redis‬‬

‫معين من الن‪ww‬وع ‪hash‬‬


‫ٍ‬ ‫جدول‬
‫ٍ‬ ‫يُطلَق عىل المفتاح الثاني اسم "الحقل ‪ ،"field‬أي يشير مفتاح مثل ‪ myhash‬إىل‬

‫بينما يشير حقلٌ مثل ‪ word1‬إىل قيمةٍ ضمن ذلك الجدول‪.‬‬

‫عاد ًة ما تكون القيم ُ‬


‫المخزَّنة في ج‪w‬داول من الن‪w‬وع ‪ hash‬أع‪ww‬دادًا ص‪w‬حيحة‪ ،‬ول‪w‬ذلك‪ ،‬ي‪w‬و ِّفر نظ‪w‬ام إدارة قاع‪ww‬دة‬

‫بيان‪ww‬ات ‪ Redis‬بعض التواب‪ww‬ع الخاص‪ww‬ة ال‪ww‬تي تعام‪ww‬ل القيم وكأنه‪ww‬ا أع‪ww‬داد مث‪ww‬ل الت‪ww‬ابع ‪ .hincrby‬انظ‪ww‬ر إىل‬

‫المثال التالي‪:‬‬

‫;)‪jedis.hincrBy("myhash", "word2", 1‬‬

‫يسترجع هذا التابع المفتاح ‪ ،myhash‬ويحصل عىل القيمة الحالية المرتبطة بالحقل ‪( word2‬أو عىل الص‪ww‬فر‬

‫إذا لم يكن الحقل موجودًا)‪ ،‬ثم يزيدها بمقدار ‪ ،1‬ويكتبها مر ًة أخرى في الجدول‪.‬‬

‫الم ْدخَالت إىل جدول من النوع ‪ hash‬واسترجاعها وزيادتها زمنًا ثابتًا‪.‬‬


‫تستغرق عمليات إضافة ُ‬

‫‪132‬‬
‫هياكل البيانات للمبرمجين‬ ‫حفظ البيانات عبر ‪Redis‬‬

‫‪ 14.5‬تمرين ‪11‬‬
‫فهرس قادرٍ عىل تخزين النت‪ww‬ائج في قاع‪ww‬دة‬
‫ٍ‬ ‫بوصولك إىل هنا‪ ،‬أصبح لديك كل المعلومات الضرورية إلنشاء ُم‬

‫بيانات ‪.Redis‬‬

‫واآلن‪ ،‬ن ِّفذ األمر ‪ .ant JedisIndexTest‬ستفشل بعض االختبارات ألنه ما يزال أمامنا بعض العمل‪.‬‬

‫يختبر الصنف ‪ JedisIndexTest‬التوابع التالية‪:‬‬

‫‪ :JedisIndex‬يستقبل هذا الباني كائنًا من النوع ‪ Jedis‬كمعامل‪.‬‬ ‫•‬

‫سلسلة نص ‪ّ w‬ي ًة من الن‪ww‬وع ‪ String‬تُم ِث‪ww‬ل‬


‫ً‬ ‫‪ :indexPage‬يضيف صفحة إنترنت إىل المفهرس‪ .‬يَستق ِبل‬ ‫•‬

‫ُمحدّد م‪ww‬وارد ‪ URL‬باإلض‪ww‬افة إىل ك‪ww‬ائن من الن‪ww‬وع ‪ Elements‬يحت‪ww‬وي عىل عناص‪ww‬ر الص‪ww‬فحة المطل‪ww‬وب‬

‫فهرستها‪.‬‬

‫بحث ويعي‪ww‬د خريط‪ً w‬‬


‫‪w‬ة من الن‪ww‬وع >‪ Map<String, Integer‬ترب ُ‪ww‬ط ك‪ww‬ل‬ ‫ٍ‬ ‫َ‬
‫كلمة‬ ‫‪ :getCounts‬يستقبل‬ ‫•‬

‫محدد موار َد يحتوي عىل تلك الكلمة بعدد مرات ظهورها في تلك الصفحة‪.‬‬

‫وضح المثال التالي طريقة استخدامِ تلك التوابع‪:‬‬


‫يُ ِ‬

‫;)(‪WikiFetcher wf = new WikiFetcher‬‬


‫= ‪String url1‬‬

‫;")‪"http://en.wikipedia.org/wiki/Java_(programming_language‬‬
‫;)‪Elements paragraphs = wf.readWikipedia(url1‬‬

‫;)(‪Jedis jedis = JedisMaker.make‬‬


‫;)‪JedisIndex index = new JedisIndex(jedis‬‬
‫;)‪index.indexPage(url1, paragraphs‬‬
‫;)"‪Map<String, Integer> map = index.getCounts("the‬‬

‫إذا بحثن‪ww‬ا عن ‪ url1‬في الخريط‪ww‬ة الناتج‪ww‬ة ‪ ،map‬ينبغي أن نحص‪ww‬ل عىل ‪ ،339‬وه‪ww‬و ع‪ww‬دد م‪ww‬رات ظه‪ww‬ور كلمة‬

‫"‪ "the‬في مقالة ويكيبيديا عن لغة جافا (نسخة المقالة التي خزّناها)‪.‬‬

‫إذا حاولنا فهرسة الصفحة ً‬


‫مرة أخرى‪ ،‬ستحُلّ النتائج الجديدة محل النتائج القديمة‪.‬‬

‫إذا أردت تحويل هياكل البيانات من جافا إىل ‪ ،Redis‬فتذ ّكر أن ك‪ww‬ل ك‪ٍ w‬‬
‫‪w‬ائن ُمخ ‪w‬ز ٍّن في قاع‪ww‬دة بيان‪ww‬ات ‪Redis‬‬

‫مفتاح فري ٍد من النوع ‪ .string‬إذا كان لديك نوعان من الكائنات في نفس قاع‪ww‬دة البيان‪ww‬ات‪ ،‬فق‪ww‬د‬
‫ٍ‬ ‫ُمعرَّ ٌف بواسطة‬

‫ترغب في إضافة كلمةٍ إىل بداية المفاتيح لتمييزها عن بعض‪ww‬ها‪ .‬عىل س‪ww‬بيل المث‪ww‬ال‪ ،‬ل‪ww‬دينا النوع‪ww‬ان التالي‪ww‬ان من‬

‫الكائنات‪:‬‬

‫‪133‬‬
‫هياكل البيانات للمبرمجين‬ ‫حفظ البيانات عبر ‪Redis‬‬

‫يُمثِل النوع ‪ URLSet‬مجموع‪ً w‬‬


‫‪w‬ة من الن‪ww‬وع ‪ set‬في قاع‪ww‬دة بيان‪ww‬ات ‪ .Redis‬تحت‪ww‬وي تل‪ww‬ك المجموع‪ww‬ة عىل‬ ‫•‬

‫بحث معينة‪ .‬يبدأ المفتاح الخاص بكل قيم‪ww‬ةٍ من‬


‫ٍ‬ ‫محددات الموارد الموحدة ‪ URL‬التي تحتوي عىل كلمةٍ‬

‫النوع ‪ URLSet‬بكلمة "‪ ،"URLSet:‬وبالتالي‪ ،‬لكي نحصل عىل محددات الموارد الموح‪w‬دة ال‪ww‬تي تحت‪ww‬وي‬

‫عىل كلمة "‪ ،"the‬علينا أن نسترجع المجموعة التي مفتاحها هو "‪."URLSet:the‬‬

‫يُمثِل النوع ‪ TermCounter‬جدواًل من النوع ‪ hash‬في قاع‪ww‬دة بيان‪ww‬ات ‪ .Redis‬يرب ُ‪w‬ط ه‪ww‬ذا الج‪w‬دول ك‪ww‬ل‬ ‫•‬

‫‪w‬اص بك‪ww‬ل قيم‪ww‬ة من الن‪ww‬وع‬


‫كلمة بحث ظهرت في صفحة مع ّينة بعدد مرات ظهوره‪ww‬ا‪ .‬يب‪ww‬دأ المفت‪ww‬اح الخ‪ُّ w‬‬
‫‪ TermCounter‬بكلمة "‪ "TermCounter:‬وينتهي بمح‪ww‬دد الم‪ww‬وارد الموحّ‪ِ w‬د الخ‪ww‬اص بالص‪ww‬فحة ال‪ww‬تي‬

‫نبحث فيها‪.‬‬

‫بحث‪ ،‬وعىل قيم‪ww‬ةٍ من الن‪ww‬وع‬


‫ٍ‬ ‫يحت‪ww‬وي التنفي‪ww‬ذ الخ‪ww‬اص بن‪ww‬ا عىل قيم‪ww‬ةٍ من الن‪ww‬وع ‪ URLSet‬لك‪ww‬ل كلم‪ww‬ة‬

‫فهرسة‪ .‬و َّفرن‪ww‬ا ً‬


‫أيض ‪w‬ا الت‪ww‬ابعين المس‪ww‬اعدين ‪ urlSetKey‬و ‪termCounterKey‬‬ ‫َ‬ ‫‪ TermCounter‬لكل صفحة ُم‬

‫إلنشاء تلك المفاتيح‪.‬‬

‫‪ 14.6‬المزيد من االقرتاحات‬
‫أصبح لديك اآلن كل المعلومات الضرورية لحل التمرين‪ ،‬ل‪ww‬ذا يُمكِن‪ww‬ك أن تب‪ww‬دأ اآلن إذا أردت‪ ،‬ولكن م‪ww‬ا ي‪ww‬زال‬

‫هناك بعض االقتراحات القليلة التي ننصحك بقراءتها‪:‬‬

‫ً‬
‫مساعدة أقلّ في ه‪ww‬ذا التم‪ww‬رين‪ ،‬ون‪ww‬ترك ل‪ww‬ك حريّ‪ww‬ة أك‪ww‬بر في اتخ‪ww‬اذ بعض الق‪ww‬رارات المتعلق‪ww‬ة‬ ‫سنو ِّفر لك‬ ‫•‬

‫بالتصميم‪ .‬سيكون عليك تحديدًا أن تُفكِّر بالطريقة التي ست ِّ‬


‫ُقسم به‪ww‬ا المش‪ww‬كلة إىل أج‪ww‬زا َء ص‪ww‬غير ٍة يُمكِن‬

‫كامل‪ .‬إذا حاولت كتاب‪ww‬ة الح‪ww‬ل بالكام‪ww‬ل‬


‫ٍ‬ ‫ُجمع تلك األجزاء إىل حلٍّ‬
‫اختبار كلٍّ منها عىل حدة‪ .‬بعد ذلك‪ ،‬ست ِّ‬
‫عىل خطو ٍة واحد ٍة بدون اختبار األجزاء األصغر‪ ،‬فقد تستغرِق وقتًا طوياًل جدًا لتنقيح األخطاء‪.‬‬

‫المخزَّن‪ww‬ة في قواع‪w‬د‬ ‫ً‬


‫واحدة من تحديات العم‪w‬ل م‪w‬ع البيان‪w‬ات المس‪w‬تمرة‪ ،‬ألن الهياك‪w‬ل ُ‬ ‫تُم ِّثل االستمرارية‬ ‫•‬

‫البيانات قد تتغير في كل مر ٍة تُش ِّغل فيها البرنامج‪ .‬فإذا تسبَّبت بخطٍأ في قاعدة البيانات‪ ،‬سيكون عليك‬

‫إصالحه أو البدء من جديد‪ .‬ولكي نُبقِي األم‪ww‬ور تحت الس‪ww‬يطرة‪ ،‬و ّفرن‪ww‬ا ل‪ww‬ك التواب‪ww‬ع ‪ deleteURLSets‬و‬

‫‪ deleteTermCounters‬و ‪ deleteAllKeys‬التي تستطيع أن تَستخدِمها لتنظيف قاعدة البيان‪ww‬ات‬

‫المفهرِس‪.‬‬ ‫والبدء من جديد‪ .‬يُمكِنك ً‬


‫أيضا استخدام التابع ‪ printIndex‬لطباعة محتويات ُ‬

‫رس‪w‬ل رس‪ً w‬‬


‫‪w‬الة إىل الخ‪ww‬ادم ال‪ww‬ذي يُن ِّفذ ب‪ww‬دوره األم‪ww‬ر‬ ‫في كلّ مر ٍة تستدعي فيها أيًّا من توابع ‪ ،Jedis‬فإن‪ww‬ه يُ ِ‬ ‫•‬

‫المطلوب‪ ،‬ثم ير ّد برسالة‪ .‬إذا ن َّفذت الكثير من العمليات الصغيرة‪ ،‬فستحتاج إىل وقت طويل لمعالجتها‪،‬‬

‫وله‪ww‬ذا‪ ،‬من األفض‪ww‬ل تجمي‪ww‬ع متتالي‪ww‬ة من العملي‪ww‬ات ض‪ww‬من معامل‪ww‬ة واح‪ww‬دة من الن‪ww‬وع ‪Transaction‬‬

‫لتحسين األداء‪.‬‬

‫عىل سبيل المثال‪ ،‬انظر إىل تلك النسخة البسيطة من التابع ‪:deleteAllKeys‬‬

‫‪134‬‬
‫هياكل البيانات للمبرمجين‬ ‫حفظ البيانات عبر ‪Redis‬‬

‫{ )(‪public void deleteAllKeys‬‬


‫;)"*"(‪Set<String> keys = jedis.keys‬‬
‫{ )‪for (String key: keys‬‬
‫;)‪jedis.del(key‬‬
‫}‬
‫}‬

‫اتص‪w‬ال م‪w‬ع الخ‪w‬ادم وانتظ‪w‬ار ال‪w‬رد‪ .‬إذا ك‪w‬ان‬


‫ٍ‬ ‫في كل مر ٍة تَستدعِ ي خاللها التابع ‪ ،del‬يضطرّ العميل إىل إج‪w‬راء‬

‫صفحات‪ ،‬فقد يَستغرِق تنفي‪ww‬ذ ذل‪ww‬ك الت‪ww‬ابع وق ًت‪ww‬ا ط‪ww‬وياًل ‪ .‬ب‪ww‬داًل من ذل‪ww‬ك‪ ،‬يُمكِن‪ww‬ك أن‬
‫ٍ‬ ‫المفهرِس يحتوي عىل بضع‬
‫ُ‬
‫كائن من النوع ‪ Transaction‬عىل النحو التالي‪:‬‬
‫ٍ‬ ‫تُسرِّ ع تلك العملية باستخدام‬

‫{ )(‪public void deleteAllKeys‬‬


‫;)"*"(‪Set<String> keys = jedis.keys‬‬
‫;)(‪Transaction t = jedis.multi‬‬
‫{ )‪for (String key: keys‬‬
‫;)‪t.del(key‬‬
‫}‬
‫;)(‪t.exec‬‬
‫}‬

‫يعيد التابع ‪ jedis.multi‬كائنًا من النوع ‪ .Transaction‬يُ‪ww‬و ِّفر ه‪ww‬ذا الك‪ww‬ائن جمي‪ww‬ع التواب‪ww‬ع المتاح‪ww‬ة في‬

‫‪w‬ائن من الن‪ww‬وع ‪ ،Transaction‬ف‪ww‬إن العمي‪ww‬ل ال‬


‫كائنات النوع ‪ .Jedis‬عن‪ww‬دما تس‪ww‬تدعي أيًّا من تل‪ww‬ك التواب‪ww‬ع بك‪ٍ w‬‬
‫يُن ِّفذها تلقائ ًيا‪ ،‬وال يتصل مع الخادم‪ ،‬وإنما يُخزِّن تلك العملي‪ww‬ات إىل أن تَس‪ww‬تدعِ ي الت‪ww‬ابع ‪ ،exec‬وعن‪ww‬دها‪ ،‬يُ ِ‬
‫رس‪w‬ل‬

‫المخزَّنة إىل الخادم في نفس الوقت‪ ،‬وهو ما يكون أسر ع عاد ًة‪.‬‬
‫العمليات ُ‬
‫ِ‬ ‫جميعَ‬

‫‪ 14.7‬تلميحات بسيطة بشأن التصميم‬


‫اآلن وقد أصبح لديك جميع المعلومات المطلوبة‪ ،‬يمكنك البدء في حل التمرين‪ .‬إذا لم يكن ل‪ww‬ديك فك‪ٌ w‬‬
‫‪w‬رة عن‬

‫طريقة البدء‪ ،‬يُمكِنك العودة لقراءة المزيد من التلميحات البسيطة‪.‬‬

‫ال تتابع القراءة قبل أن تُش ِّغل شيفرة االختبار‪ ،‬وتُج ‪w‬رِّب بعض أوام‪ww‬ر ‪ Redis‬البس‪ww‬يطة‪ ،‬وتكتب بعض التواب‪ww‬ع‬

‫الموجودة في الملف ‪.JedisIndex.java‬‬

‫إذا لم تتمكّن من متابعة الحل فعاًل ‪ ،‬إليك بعض التوابع التي قد ترغب في العمل عليها‪:‬‬

‫**‪/‬‬
‫ً‬
‫موحدا إلى المجموعة الخاصة بكلمة *‬ ‫أضف محدد موارد‬
‫‪*/‬‬

‫‪135‬‬
‫هياكل البيانات للمبرمجين‬ ‫حفظ البيانات عبر ‪Redis‬‬

‫}{ )‪public void add(String term, TermCounter tc‬‬

‫**‪/‬‬
‫ابحث عن كلمة وأعد مجموعة ُم ِّ‬
‫حددات الموارد الموحدة التي تحتوي عليها *‬
‫‪*/‬‬
‫}{ )‪public Set<String> getURLs(String term‬‬

‫**‪/‬‬
‫أعد عدد مرات ظهور كلمة معينة بمحدد موارد موحد *‬
‫‪*/‬‬
‫}{ )‪public Integer getCount(String url, String term‬‬

‫**‪/‬‬
‫أضف محتويات كائن من النوع `‪ `TermCounter‬إلى قاعدة بيانات ‪* Redis‬‬
‫‪*/‬‬
‫}{ )‪public List<Object> pushTermCounterToRedis(TermCounter tc‬‬

‫تلك هي التوابع التي استخدمناها في الحل‪ ،‬ولكنه‪ww‬ا ليس‪ww‬ت بالتأكي‪ww‬د الطريق‪ww‬ة الوحي‪ww‬دة لتقس‪ww‬يم المش‪ww‬كلة‪.‬‬

‫تماما إذا لم تجدها كذلك‪.‬‬


‫ً‬ ‫لذلك‪ ،‬استعن بتلك االقتراحات فقط إذا وجدتها مفيدة‪ ،‬وتجاهلها‬

‫تابع منها أواًل ‪ ،‬فبمعرفة الطريقة التي ينبغي بها أن تختبر تابعً ا معينً‪ww‬ا‪ ،‬ع‪ww‬اد ًة م‪ww‬ا‬
‫ٍ‬ ‫اكتب بعض االختبارات لكل‬

‫تتك َّون لديك فكر ٌة عن طريقة كتابته‪.‬‬

‫وفقك الله!‬

‫‪136‬‬
‫‪ .15‬الزحف عىل ويكيبيديا‬

‫سنُقدِّم في هذا الفصل حل تم‪ww‬رين الفص‪ww‬ل الس‪ww‬ابق‪ .‬بع‪ww‬د ذل‪ww‬ك‪ ،‬س ‪w‬نُحلِّل أداء خوارزمي‪ww‬ة فهرس‪ww‬ة ص‪ww‬فحات‬

‫بسيطا‪.‬‬
‫ً‬ ‫َ‬
‫زاحف ويب ‪Web crawler‬‬ ‫اإلنترنت‪ ،‬ثم سنبني‬

‫‪ 15.1‬المفهرس المبين عىل قاعدة بيانات ‪Redis‬‬


‫سنُخزِّن هيكلي البيانات التاليين في قاعدة بيانات ‪:Redis‬‬

‫بحث كائنٌ من النوع ‪ URLSet‬هو عب‪ww‬ار ٌة عن مجموع‪ww‬ة ‪ Set‬في قاع‪ww‬دة بيان‪ww‬ات ‪Redis‬‬
‫ٍ‬ ‫س ُيقابِل كلَّ كلمة‬ ‫•‬

‫تحتوي عىل ُمحدِّدات الموارد الموحدة ‪ URLs‬التي تحتوي عىل تلك الكلمة‪.‬‬

‫س ُيقابِل كل ُمحدّد موارد موحد كائنًا من النوع ‪ TermCounter‬يُم ّث‪ww‬ل ج‪ww‬دول ‪ Hash‬في قاع‪ww‬دة بيان‪ww‬ات‬ ‫•‬

‫‪ Redis‬يربُط كل كلمة بحث بعدد مرات ظهورها‪.‬‬

‫يُمكِن‪ww‬ك مراجع‪ww‬ة أن‪ww‬واع البيان‪ww‬ات ال‪ww‬تي ناقش‪ww‬ناها في الفص‪ww‬ل المش‪ww‬ار إلي‪ww‬ه‪ ،‬كم‪ww‬ا يُمكِن‪ww‬ك ق‪ww‬راءة المزي‪ww‬د عن‬

‫المجموعات والجداول بقاعدة بيانات ‪ Redis‬من توثيقها الرسمي‪.‬‬

‫بحث ويعيد مفت‪ww‬اح ك‪ww‬ائن الص‪ww‬نف ‪ URLSet‬المقاب‪ww‬ل في‬


‫ٍ‬ ‫يُعرِّف الصنف ‪ JedisIndex‬تابعً ا يَستق ِبل كلمة‬

‫قاعدة بيانات ‪:Redis‬‬

‫{ )‪private String urlSetKey(String term‬‬


‫;‪return "URLSet:" + term‬‬
‫}‬

‫ُ‬
‫الصنف المذكور التابع ‪ ،termCounterKey‬والذي يستقبل ُمح ‪w‬دّد م‪ww‬وارد موح ‪w‬دًا ويعي‪ww‬د مفت‪ww‬اح‬ ‫كما يُعرِّف‬

‫كائن الصنف ‪ TermCounter‬المقابل في قاعدة بيانات ‪:Redis‬‬


‫هياكل البيانات للمبرمجين‬ ‫الزحف عىل ويكيبيديا‬

‫{ )‪private String termCounterKey(String url‬‬


‫;‪return "TermCounter:" + url‬‬
‫}‬

‫يَستق ِبل تابع الفهرسة ‪ indexPage‬محد َد موارد موح ‪w‬دًا وكائنً‪ww‬ا من الن‪ww‬وع ‪ Elements‬يحت‪ww‬وي عىل ش‪ww‬جرة‬

‫‪ DOM‬للفقرات التي نريد فهرستها‪:‬‬

‫{ )‪public void indexPage(String url, Elements paragraphs‬‬


‫;)‪System.out.println("Indexing " + url‬‬

‫وأحص كلمات الفقرات النصية ‪//‬‬


‫ِ‬ ‫أنشئ كائنًا من النوع ‪TermCounter‬‬

‫;)‪TermCounter tc = new TermCounter(url‬‬


‫;)‪tc.processElements(paragraphs‬‬

‫أضف محتويات الكائن إلى قاعدة بيانات ‪// Redis‬‬


‫;)‪pushTermCounterToRedis(tc‬‬
‫}‬

‫سنقوم بالتالي لكي نُفهرِّس الصفحة‪:‬‬

‫‪ .1‬ن ُ ِ‬
‫نشئ كائنًا من النوع ‪ TermCounter‬يُمثِل محتويات الصفحة باستخدام شيفرة تمرين الفصل المش‪ww‬ار‬

‫إليه باألعىل‪.‬‬

‫‪ .2‬نُضيف محتويات ذلك الكائن في قاعدة بيانات ‪.Redis‬‬

‫تُ ِّ‬
‫وضح الشيفرة التالية طريقة إضافة كائنات النوع ‪ TermCounter‬إىل قاعدة بيانات ‪:Redis‬‬

‫{ )‪public List<Object> pushTermCounterToRedis(TermCounter tc‬‬


‫;)(‪Transaction t = jedis.multi‬‬

‫;)(‪String url = tc.getLabel‬‬


‫;)‪String hashname = termCounterKey(url‬‬

‫ً‬
‫مسبقا‪ ،‬احذف الجدول القديم ‪//‬‬ ‫إذا كانت الصفحة مفهرسة‬
‫;)‪t.del(hashname‬‬

‫ً‬
‫جديدا إلى الفهرس ‪//‬‬ ‫ً‬
‫وعنصرا‬ ‫ً‬
‫جديدا في كائن الصنف ‪TermCounter‬‬ ‫ً‬
‫مدخلا‬ ‫أضف‬

‫‪138‬‬
‫هياكل البيانات للمبرمجين‬ ‫الزحف عىل ويكيبيديا‬

‫لكل كلمة بحث ‪//‬‬


‫{ ))(‪for (String term: tc.keySet‬‬
‫;)‪Integer count = tc.get(term‬‬
‫;))(‪t.hset(hashname, term, count.toString‬‬
‫;)‪t.sadd(urlSetKey(term), url‬‬
‫}‬
‫;)(‪List<Object> res = t.exec‬‬
‫;‪return res‬‬
‫}‬

‫ً‬
‫معاملة من النوع ‪ Transaction‬لتجميع العمليات‪ ،‬ثم يرسلها جميعً ‪ w‬ا إىل الخ‪ww‬ادم عىل‬ ‫يَستخدم هذا التابع‬

‫خطوة واحدة‪ .‬تُع ّد تلك الطريقة أسر ع بكثير من إرسال متتاليةٍ من العمليات الصغيرة‪.‬‬

‫يمرّ التابع عبر العناصر الموجودة في كائن الصنف ‪ ،TermCounter‬ويُن ِّفذ التالي من أجل كل عنصر ٍ منها‪:‬‬

‫كائن من النوع ‪- TermCounter‬أو ينشئه إن لم يجده‪ -‬في قاعدة بيانات ‪ ،Redis‬ثم يض‪ww‬يف‬
‫ٍ‬ ‫‪ .1‬يبحث عن‬

‫حقاًل فيه يُمثِل العنصر الجديد‪.‬‬

‫كائن من النوع ‪- URLSet‬أو ينشئه إن لم يج‪ww‬ده‪ -‬في قاع‪ww‬دة بيان‪ww‬ات ‪ ،Redis‬ثم يض‪ww‬يف إلي‪ww‬ه‬
‫ٍ‬ ‫‪ .2‬يبحث عن‬

‫محدّد الموارد الموحد الحالي‪.‬‬

‫إذا كنا قد فه َرسنا تلك الصفحة من قبل‪ ،‬علينا أن نحذف كائن الصنف ‪ TermCounter‬القديم الذي يمثله‪ww‬ا‬

‫قبل أن نضيف المحتويات الجديدة‪.‬‬

‫هذا هو كل ما نحتاج إليه لفهرسة الصفحات الجديدة‪.‬‬

‫بحث ويعي‪ww‬د خريط‪ً w‬‬


‫‪w‬ة ترب‪ww‬ط‬ ‫ٍ‬ ‫طلب الجز ُء الثاني من التمرين كتاب‪َ w‬‬
‫‪w‬ة الت‪ww‬ابع ‪ getCounts‬ال‪ww‬ذي يَس‪ww‬تق ِبل كلم‪ww‬ة‬ ‫َ‬
‫محددات الموارد الموحدة التي ظهرت فيها تلك الكلمة بعدد مرات ظهورها فيها‪ .‬انظر إىل تنفيذ التابع‪:‬‬

‫{ )‪public Map<String, Integer> getCounts(String term‬‬


‫;)(>‪Map<String, Integer> map = new HashMap<String, Integer‬‬
‫;)‪Set<String> urls = getURLs(term‬‬
‫{ )‪for (String url: urls‬‬
‫;)‪Integer count = getCount(url, term‬‬
‫;)‪map.put(url, count‬‬
‫}‬
‫;‪return map‬‬
‫}‬

‫يَستخدِم هذا التابعُ تابعين مساعدين‪:‬‬

‫‪139‬‬
‫هياكل البيانات للمبرمجين‬ ‫الزحف عىل ويكيبيديا‬

‫ً‬
‫مجموعة من النوع ‪ Set‬تحتوي عىل مح‪ww‬ددات الم‪ww‬وارد الموح‪ww‬دة‬ ‫بحث ويعيد‬
‫ٍ‬ ‫‪ :getURLs‬يَستق ِبل كلمة‬ ‫•‬

‫التي ظهرت فيها الكلمة‪.‬‬

‫‪ :getCount‬يَستق ِبل محدد موارد موحدًا ‪ URI‬وكلم‪ww‬ة بحث‪ ،‬ويعي‪ww‬د ع‪ww‬دد م‪ww‬رات ظه‪ww‬ور الكلم‪ww‬ة بمح‪ww‬دد‬ ‫•‬

‫الممرَّر‪.‬‬
‫الموارد ُ‬

‫انظر تنفيذات تلك التوابع‪:‬‬

‫{ )‪public Set<String> getURLs(String term‬‬


‫;))‪Set<String> set = jedis.smembers(urlSetKey(term‬‬
‫;‪return set‬‬
‫}‬

‫{ )‪public Integer getCount(String url, String term‬‬


‫;)‪String redisKey = termCounterKey(url‬‬
‫;)‪String count = jedis.hget(redisKey, term‬‬
‫;)‪return new Integer(count‬‬
‫}‬

‫المفهرِس‪.‬‬
‫صم ّمنا بها ُ‬
‫ّ‬ ‫ً‬
‫نتيجة للطريقة التي‬ ‫تَ َ‬
‫عمل تلك التوابع بكفاء ٍة‬

‫‪ 15.2‬تحليل أداء عملية البحث‬


‫لنفترض أننا فهرسنا عددًا مق‪ww‬داره ‪ N‬من الص‪ww‬فحات‪ ،‬وتوص‪ww‬لنا إىل ع‪ww‬د ٍد مق‪ww‬داره ‪ M‬من كلم‪ww‬ات البحث‪ .‬كم‬

‫الوقت الذي سيستغرقه البحث عن كلمةٍ معينةٍ ؟ فكر قبل أن تكمل القراءة‪.‬‬

‫سنُن ِّفذ التابع ‪ getCounts‬للبحث عن كلمةٍ ‪ ،‬يُن ِّفذ ذلك التابع ما يلي‪:‬‬

‫ً‬
‫خريطة من النوع ‪.HashMap‬‬ ‫نشئ‬
‫‪ .1‬يُ ِ‬

‫‪ .2‬يُن ِّفذ التابع ‪ getURLs‬ليسترجع مجموعة ُمحدِّدات الموارد الموحدة‪.‬‬

‫خاًل إىل الخريطة‪.‬‬


‫‪ .3‬يَستدعِ ي التابع ‪ getCount‬لكل ُمحدِّد موارد‪ ،‬ويضيف ُم ْد َ‬

‫يستغرق التابع ‪ getURLs‬زمنًا يتناسب مع عدد محددات الموارد الموحدة ال‪ww‬تي تحت‪ww‬وي عىل كلم‪ww‬ة البحث‪.‬‬

‫صل إىل ‪ -N‬في حالة الكلمات الشائعة‪.‬‬


‫قد يكون عددًا صغيرًا بالنسبة للكلمات النادرة‪ ،‬ولكنه قد يكون كبيرًا ‪-‬قد يَ ِ‬

‫كائن من الن‪ww‬وع ‪ TermCounter‬في قاع‪ww‬دة بيان‪ww‬ات‬


‫ٍ‬ ‫سنُن ِّفذ داخل الحلقة التابعَ ‪ getCount‬الذي يبحث عن‬

‫خاًل إىل خريطةٍ من النوع ‪ .HashMap‬تستغرق جميع تلك العمليات زمنًا‬


‫‪ ،Redis‬ثم يبحث عن كلمةٍ ‪ ،‬ويضيف ُمد ْ‬

‫‪140‬‬
‫هياكل البيانات للمبرمجين‬ ‫الزحف عىل ويكيبيديا‬

‫ثابتًا‪ ،‬وبالتالي‪ ،‬ينتمي الت‪ww‬ابع ‪ getCounts‬في المجم‪ww‬ل إىل المجموع‪ww‬ة )‪ O(N‬في أس‪ww‬وأ الح‪ww‬االت‪ ،‬ولكن عمل ًي‪ww‬ا‪،‬‬
‫ً‬
‫عادة عد ٌد أصغر بكثير ٍ من ‪.N‬‬ ‫يتناسب زمن تنفيذه مع عدد الصفحات التي تحتوي عىل تلك الكلمة‪ ،‬وهو‬

‫وأما فيما يتعلق بتحليل الخوارزميات‪ ،‬فإن تلك الخوارزمية تتميز بأقصى قدرٍ من الكف‪ww‬اءة‪ ،‬ولكنه‪ww‬ا م‪ww‬ع ذل‪ww‬ك‬
‫ٌ‬
‫بطيئة ألنها ترسل الكثير من العمليات الصغيرة إىل قاعدة بيانات ‪ .Redis‬من الممكن تحس‪ww‬ين أدائه‪ww‬ا باس‪ww‬تخدام‬

‫معامل‪www‬ةٍ من الن‪www‬وع ‪ .Transaction‬يُمكِن‪www‬ك محاول‪www‬ة تنفي‪www‬ذ ذل‪www‬ك أو االطالع عىل الح‪www‬ل في المل‪www‬ف‬

‫‪( RedisIndex.java‬انتبه إىل أن اسمه في المستودع ‪ JedisIndex.java‬والله أعلم)‪.‬‬

‫‪ 15.3‬تحليل أداء عملية الفهرسة‬


‫ص‪w‬م ْمناها؟ فك‪w‬ر قب‪w‬ل أن‬
‫ّ‬ ‫م‪w‬ا ال‪w‬زمن ال‪w‬ذي تس‪w‬تغرقه فهرس‪w‬ة ص‪w‬فحةٍ عن‪w‬د اس‪w‬تخدام هياك‪ww‬ل البيان‪w‬ات ال‪w‬تي‬

‫تكمل القراءة‪.‬‬

‫ً‬
‫صفحة‪ ،‬فإننا نمرّ عبر شجرة ‪ ،DOM‬ونع‪ww‬ثر عىل الكائن‪ww‬ات ال‪ww‬تي تنتمي إىل الن‪ww‬وع ‪،TextNode‬‬ ‫لكي نُفهرِّس‬

‫بحث‪ .‬تَستغرِق كل تلك العمليات زمنًا يتناسب مع عدد الكلمات الموجودة‬


‫ٍ‬ ‫ون ُ ِّ‬
‫قسم السالسل النصية إىل كلمات‬

‫في الصفحة‪.‬‬

‫بحث ضمن الصفحة‪ ،‬وهو ما يَس‪ww‬تغرِق زمنً‪ww‬ا ثاب ًت‪ww‬ا‪ ،‬م‪ww‬ا‬


‫ٍ‬ ‫نزيد العدّاد ضمن خريطة النوع ‪ HashMap‬لكل كلمة‬

‫يَجعَ ل الصنف ‪ TermCounter‬يَستغرِق زمنًا يتناسب مع عدد الكلمات الموجودة في الصفحة‪.‬‬

‫كائن آخرَ منها‪ .‬يس‪ww‬تغرِق ذل‪ww‬ك‬


‫ٍ‬ ‫تتطلَّب إضافة كائن الصنف ‪ TermCounter‬إىل قاعدة بيانات ‪ Redis‬حذف‬

‫خط ًّيا مع عدد كلمات البحث‪ .‬بعد ذلك‪ ،‬علينا أن نُن ِّفذ التالي من أجل كل كلمة‪:‬‬
‫زمنًا يتناسب ّ‬

‫كائن من النوع ‪.URLSet‬‬


‫ٍ‬ ‫‪ .1‬نضيف عنصرًا إىل‬

‫كائن من النوع ‪.TermCounter‬‬


‫ٍ‬ ‫‪ .2‬نضيف عنص ًرا إىل‬

‫‪w‬ائن من الن‪ww‬وع‬
‫‪w‬وب إلض‪ww‬افة ك‪ٍ w‬‬
‫الكلي المطل‪ُ w‬‬
‫ُّ‬ ‫تَستغرِق العمليتان السابقتان زمنًا ثاب ًت‪ww‬ا‪ ،‬وبالت‪ww‬الي‪ ،‬يك‪ww‬ون ال‪ww‬زمن‬

‫‪ TermCounter‬خط ًيا مع عدد كلمات البحث الفريدة‪.‬‬

‫نستخلص مما س‪ww‬بق أنّ زمن تنفي‪ww‬ذ الص‪ww‬نف ‪ TermCounter‬يتناس‪ww‬ب م‪ww‬ع ع‪ww‬دد الكلم‪ww‬ات الموج‪ww‬ودة في‬

‫كائن ينتمي إىل النوع ‪ TermCounter‬إىل قاعدة بيانات ‪ Redis‬تتطلَّب زمنًا يتناس‪ww‬ب م‪ww‬ع‬
‫ٍ‬ ‫الصفحة‪ ،‬وأن إضافة‬

‫عدد الكلمات الفريدة‪.‬‬

‫ً‬
‫عادة عدد كلمات البحث الفريدة‪ ،‬فإن التعقيد يتناسب‬ ‫ولما كان عدد الكلمات الموجودة في الصفحة يتجاوز‬
‫ّ‬
‫ٌ‬
‫صفحة ما نظرًيً‪ww‬ا عىل جمي‪ww‬ع كلم‪ww‬ات البحث‬ ‫طردًا مع عدد الكلمات الموجودة في الصفحة‪ ،‬ومع ذلك‪ ،‬قد تحتوي‬
‫َّ‬
‫نتوقع ح‪ww‬دوث تل‪ww‬ك‬ ‫الموجودة في الفهرس‪ ،‬وعليه‪ ،‬ينتمي األداء في أسوأ الحاالت إىل المجموعة )‪ ،O(M‬ولكننا ال‬

‫الحالة عمل ًّيا‪.‬‬

‫‪141‬‬
‫هياكل البيانات للمبرمجين‬ ‫الزحف عىل ويكيبيديا‬

‫يتضح من التحليل السابق أنه ما يزال من الممكن تحسين أداء الخوارزمية‪ ،‬فمثاًل يُمكِنن‪w‬ا أن نتجنَّب فهرس‪w‬ة‬
‫ِّ‬

‫كبيرة من الذاكرة كما أنها تستغرِق وق ًت‪ww‬ا ط‪ww‬وياًل ؛ فهي تَظه ‪َ w‬ر تقريبً‪ww‬ا‬
‫ً‬ ‫ً‬
‫مساحة‬ ‫الكلمات الشائعة جدًا‪ ،‬ألنها أواًل تحتل‬
‫ً‬
‫كبيرة فهي ال تساعد عىل تحدي‪ww‬د‬ ‫ً‬
‫أهمية‬ ‫كائنات النوعين ‪ URLSet‬و ‪ ،TermCounter‬كما أنها ال تحمل‬
‫ِ‬ ‫جميع‬ ‫في‬
‫ِ‬
‫َ‬
‫ذات صلةٍ بكلمات البحث‪.‬‬ ‫حتمل أن تكون‬
‫الصفحات التي يُ َ‬

‫الكلمات الش‪ww‬ائعةِ ال‪ww‬تي يطل‪ww‬ق عليه‪ww‬ا اس‪ww‬م الكلم‪ww‬ات المهمل‪ww‬ة ‪stop‬‬


‫ِ‬ ‫َ‬
‫فهرسة‬ ‫تتجنَّب غالبية محركات البحث‬

‫‪ words‬ضمن هذا السياق‪.‬‬

‫‪ 15.4‬التنقل في مخطط ‪graph‬‬


‫ً‬ ‫إذا أكملت تمرين الفصل السابع كل الطرق تؤدي إىل روما‪ ،‬فل‪ww‬ديك بالفع‪ww‬ل برن‪ww‬امجٌ يق‪ww‬رأ ص‪w‬‬
‫‪w‬فحة من موق‪ww‬ع‬

‫رابط فيها‪ ،‬ويَستخدِمه لتحميل الصفحة التالية‪ ،‬ثم يك‪ww‬رر العملي‪ww‬ة‪ .‬يُع ‪ّ w‬د ه‪ww‬ذا البرن‪ww‬امج‬
‫ٍ‬ ‫ويكيبيديا‪ ،‬ويبحث عن أول‬
‫ً‬
‫فعادة عندما يُذكرُ مص‪ww‬طلح "زاح‪ww‬ف إن‪ww‬ترنت" ‪ ،Web crawler‬فالمقص‪ww‬ود برن‪ww‬امج‬ ‫زاحف من نوع خاص‪،‬‬
‫ٍ‬ ‫بمنزلةِ‬

‫يُن ِّفذ ما يلي‪:‬‬

‫حمل صفحة بداية معينة‪ ،‬ويُفهرِس محتوياتها‪.‬‬


‫يُ ِّ‬ ‫•‬

‫يبحث عن جميع الروابط الموجودة في تلك الصفحة ويضيفها إىل تجميعة ‪.collection‬‬ ‫•‬

‫روابط جديدة‪.‬‬
‫َ‬ ‫حمل كاًّل منها‪ ،‬ويُفهرِسه‪ ،‬ويضيف أثناء ذلك‬
‫يمرّ عبر تلك الروابط‪ ،‬ويُ ِّ‬ ‫•‬

‫إذا عَ َثر عىل ُمح ّد ِد موار َد ُموح ٍد فهرَ َ‬


‫سه من قبل‪ ،‬فإنه يتخطاه‪.‬‬ ‫•‬

‫يُمكِننا أن نتص ّور شبكة اإلنترنت كما لو ك‪ww‬انت ش‪ww‬عبة أو مخط‪ww‬ط ‪ .graph‬تُم ِث‪ww‬ل ك‪ww‬ل ص‪ww‬فحة إن‪ww‬ترنت عق‪ww‬د ًة‬

‫‪ node‬في تلك الشعبة‪ ،‬ويُمثِل كل رابط ضلعً ا ُموجّهً ا ‪ directed edge‬من عقد ٍة إىل عقد ٍة أخرى‪ .‬يُمكِنك ق‪ww‬راءة‬

‫المزيد عن الشعب إذا لم تكن عىل معرفةٍ بها‪.‬‬

‫بنا ًء عىل هذا التصور‪ ،‬يستطيع الزاحف أن يبدأ من عقد ٍة مع ّينةٍ ‪ ،‬ويتنقل عبر أو يجت‪w‬از الش‪ww‬عبة‪ ،‬وذل‪ww‬ك بزي‪w‬ارة‬

‫العقد التي يُمكِنه الوصول إليها مر ًة واحد ًة فقط‪.‬‬

‫تُح ‪w‬دِّد التجميع‪ww‬ة ال‪ww‬تي سنَس‪ww‬تخدِمها لتخ‪ww‬زين مح‪ww‬ددات الم‪ww‬وارد المح‪ww‬ددة ن‪ww‬وع االجتي‪ww‬از أو التنق‪ww‬ل ال‪ww‬ذي‬

‫يُن ِّفذه الزاحف‪:‬‬

‫إذا كانت التجميعة رتاًل ‪ ،queue‬أي تتب‪ww‬ع أس‪ww‬لوب "ال‪ww‬داخل أواًل ‪ ،‬يخ‪ww‬رج أواًل " ‪ ،FIFO‬ف‪ww‬إن الزاح‪ww‬ف يُن ِّفذ‬ ‫•‬

‫السعة أواًل ‪.breadth-first‬‬


‫اجتياز ّ‬

‫مكدسا ‪ ،stack‬أي تتبع أسلوب "الداخل آخرًا‪ ،‬يخ‪ww‬رج أواًل " ‪ ،LIFO‬ف‪ww‬إن الزاح‪ww‬ف يُن ِّفذ‬
‫ً‬ ‫إذا كانت التجميعة‬ ‫•‬

‫اجتياز العمق أواًل ‪.depth-first‬‬

‫‪142‬‬
‫هياكل البيانات للمبرمجين‬ ‫الزحف عىل ويكيبيديا‬

‫ات لعناص‪w‬ر التجميع‪ww‬ة‪ .‬عىل س‪ww‬بيل المث‪ww‬ال‪ ،‬ق‪w‬د ن‪w‬رغب في إعط‪w‬اء أولوي‪w‬ة أعىل‬
‫من الممكن تحديد أولويّ ٍ‬ ‫•‬

‫للصفحات التي لم تُفه َرس منذ فترة طويلة‪.‬‬

‫‪ 15.5‬تمرين ‪12‬‬
‫واآلن‪ ،‬عليك كتابة الزاحف‪ ،‬ستجد ملفات الشيفرة التالية الخاصة بالتمرين في مستودع الكتاب‪:‬‬

‫‪ :WikiCrawler.java‬يحتوي عىل شيفرة مبدئ ّية للزاحف‪.‬‬ ‫•‬

‫‪ :WikiCrawlerTest.java‬يحتوي عىل اختبارات وحدة للصنف ‪.WikiCrawler‬‬ ‫•‬

‫تمرين الفصل المشار إليه باألعىل‪.‬‬


‫ِ‬ ‫‪ :JedisIndex.java‬يحتوي عىل حلِّ‬ ‫•‬

‫ستحتاج ً‬
‫أيضا إىل األصناف المساعدة التالية التي استخدَمناها في تمارين الفصول السابقة‪:‬‬

‫‪JedisMaker.java‬‬ ‫•‬

‫‪WikiFetcher.java‬‬ ‫•‬

‫‪TermCounter.java‬‬ ‫•‬

‫‪WikiNodeIterable.java‬‬ ‫•‬

‫ٍّ‬
‫ملف يحتوي عىل بيانات خادم ‪ Redis‬قبل تنفي‪ww‬ذ الص‪ww‬نف ‪ .JedisMaker‬إذا أكملت‬ ‫سيتع ّين عليك توفير‬

‫تمرين الفصل المشار إليه باألعىل‪ ،‬فقد جهّ زت كل شيء بالفعل‪ ،‬أما إذا لم تكمله‪ ،‬فستجد التعليم‪ww‬ات الض‪ww‬رورية‬

‫إلتمام ذلك في نفس الفصل‪.‬‬

‫ن ِّفذ األمر ‪ ant build‬لتصريف ملفات الشيفرة‪ ،‬ثم ن ِّفذ األمر ‪ ant JedisMaker‬لكي تتأ ّكد من أنه مهيا ٌ‬

‫لالتصال مع خادم ‪ Redis‬الخاص بك‪.‬‬

‫واآلن‪ ،‬ن ِّفذ األمر ‪ .ant WikiCrawlerTest‬ستجد أن االختبارات قد فش‪w‬لت؛ ألن م‪w‬ا ي‪w‬زال علي‪w‬ك إتم‪w‬ام‬

‫بعض العمل أواًل ‪.‬‬

‫انظر إىل بداية تعريف الصنف ‪:WikiCrawler‬‬

‫{ ‪public class WikiCrawler‬‬

‫;‪public final String source‬‬


‫;‪private JedisIndex index‬‬
‫;)(>‪private Queue<String> queue = new LinkedList<String‬‬
‫;)(‪final static WikiFetcher wf = new WikiFetcher‬‬

‫‪143‬‬
‫هياكل البيانات للمبرمجين‬ ‫الزحف عىل ويكيبيديا‬

‫{ )‪public WikiCrawler(String source, JedisIndex index‬‬


‫;‪this.source = source‬‬
‫;‪this.index = index‬‬
‫;)‪queue.offer(source‬‬
‫}‬

‫{ )(‪public int queueSize‬‬


‫;)(‪return queue.size‬‬
‫}‬

‫يحتوي هذا الصنف عىل متغيرات النسخ ‪ instance variables‬التالية‪:‬‬

‫‪ُ :source‬محدّد الموارد الموحد الذي ستبدأ منه‪.‬‬ ‫•‬

‫‪ :index‬مفهرِس ‪-‬من النوع ‪ -JedisIndex‬ينبغي أن تُخزَّن النتائج فيه‪.‬‬ ‫•‬

‫‪ :queue‬عبارة عن قائمة من النوع ‪ .LinkedList‬يُفترَض أن تُخزَّن فيه‪ww‬ا ك‪ww‬لُّ مح‪ww‬ددات الم‪ww‬وارد ال‪ww‬تي‬ ‫•‬

‫عثرت عليها‪ ،‬ولكن لم تُفهرِسها بعد‪.‬‬

‫‪ :wf‬عبارة عن كائن من النوع ‪ .WikiFetcher‬عليك أن تَستخ ِدمه لقراءة صفحات اإلنترنت وتحليلها‪.‬‬ ‫•‬

‫عليك اآلن أن تكمل التابع ‪ .crawl‬انظر إىل بصمته‪:‬‬

‫}{ ‪public String crawl(boolean testing) throws IOException‬‬

‫ً‬
‫مس‪ww‬اوية للقيم‪ww‬ة ‪ true‬إذا ك‪ww‬ان مس‪ww‬تدعيه ه‪ww‬و الص‪ww‬نف‬ ‫ينبغي أن تك‪ww‬ون قيم‪ww‬ة المعام‪ww‬ل ‪testing‬‬
‫ً‬
‫مساوية للقيمة ‪ false‬في الحاالت األخرى‪.‬‬ ‫‪ ،WikiCrawlerTest‬وأن تكون‬

‫ً‬
‫مساوية للقيمة ‪ ،true‬يُفت َرض أن يُن ِّفذ التابع ‪ crawl‬ما يلي‪:‬‬ ‫عندما تكون قيمة المعامل ‪testing‬‬

‫يختار ُمح ّد َد موارد موحدًا من الرتل وف ًقا ألسلوب "الداخل أواًل ‪ ،‬يخرج أواًل " ثم يحذفه منه‪.‬‬ ‫•‬

‫يقرأ محتويات تل‪ww‬ك الص‪ww‬فحة باس‪ww‬تخدام الت‪ww‬ابع ‪ WikiFetcher.readWikipedia‬ال‪ww‬ذي يعتم‪ww‬د في‬ ‫•‬

‫‪w‬خ ُمخزّن‪ww‬ةٍ مؤق ًت‪ww‬ا في المس‪ww‬تودع به‪ww‬دف االختب‪ww‬ار (لكي نتجنَّب أ َّ‬
‫ي مش‪ww‬كالت‬ ‫قراءته للص‪ww‬فحات عىل نس‪ٍ w‬‬
‫النسخ الموجود ُة في موقع ويكيبيديا)‪.‬‬
‫ُ‬ ‫ممكنة في حال تغ ّيرت‬

‫يُفهرِس الصفحة بغض النظر عما إذا كانت قد ُفهر ِ َ‬


‫ست من قبل‪.‬‬ ‫•‬

‫ينبغي أن يجد كل الروابط الداخلية الموجودة في الص‪ww‬فحة ويض‪ww‬يفها إىل الرت‪ww‬ل بنفس ت‪ww‬رتيب ظهوره‪ww‬ا‪.‬‬ ‫•‬

‫قص د بالروابط الداخلية تلك الروابط التي تشير إىل صفحات أخرى ضمن موقع ويكيبيديا‪.‬‬
‫يُ َ‬

‫ينبغي أن يعيد ُمحدّد الموارد الخاص بالصفحة التي فهرسها‪.‬‬ ‫•‬

‫‪144‬‬
‫هياكل البيانات للمبرمجين‬ ‫الزحف عىل ويكيبيديا‬

‫ً‬
‫مساوية للقيمة ‪ ،false‬يُفترَض له أن يُن ِّفذ ما يلي‪:‬‬ ‫في المقابل‪ ،‬عندما تكون قيمة المعامل ‪testing‬‬

‫يختار ُمحدّد موارد موحدًا من الرتل وف ًقا ألسلوب "الداخل أواًل ‪ ،‬يخرج أواًل " ثم يحذفه منه‪.‬‬ ‫•‬

‫سا بالفعل‪ ،‬ال ينبغي أن يعيد فهرسته‪ ،‬وعليه أن يعيد القيمة ‪.null‬‬
‫إذا كان محدّد الموارد المختار مفه َر ً‬ ‫•‬

‫إذا لم يُفه‪wwwwww‬رَس من قب‪wwwwww‬ل‪ ،‬فينبغي أن يق‪wwwwww‬رأ محتوي‪wwwwww‬ات الص‪wwwwww‬فحة باس‪wwwwww‬تخدام الت‪wwwwww‬ابع‪.‬‬ ‫•‬

‫‪ WikiFetcher.fetchWikipedia‬الذي يعتمد في قراءته للصفحات عىل شبكة اإلنترنت‪.‬‬

‫‪w‬اص‬
‫روابط موجود ٍة فيه‪ww‬ا إىل الرت‪ww‬ل‪ ،‬ويعي‪ww‬د ُمح‪w‬دّد الم‪ww‬وارد الخ‪َّ w‬‬
‫َ‬ ‫ي‬
‫ينبغي أن يُفهرِس الصفحة‪ ،‬ويضيف أ َّ‬ ‫•‬

‫بالصفحة التي فهرسها‪.‬‬

‫حمل الصنف ‪ WikiCrawlerTest‬رتاًل يحتوي عىل ‪ 200‬رابط‪ ،‬ثم يَس‪ww‬تدعِ ي الت‪ww‬ابع ‪ crawl‬ثالث م ‪w‬رّات‪،‬‬
‫يُ ِّ‬
‫َ‬
‫القيمة المعادة والطولَ الجديد للرتل‪.‬‬ ‫ويفحص في كلِّ استدعاء‬

‫إذا أكملت زاحف اإلنترنت الخاص بك بشكل صحيح‪ ،‬فينبغي أن تنجح االختبارات‪.‬‬

‫وفقك الله!‬

‫‪145‬‬
‫‪ .16‬البحث المنطقي ‪Boolean Search‬‬

‫سنشرح في هذا الفصل حل التمرين التالي من الفصل السابق‪ ،‬ثم نن ّفذ ش‪ww‬يفرة ت‪ww‬دمج مجموع‪ً w‬‬
‫‪w‬ة من نت‪ww‬ائج‬

‫البحث وترتِّبها بحسب مدى ارتباطها بكلمات البحث‪.‬‬

‫‪ 16.1‬الزاحف ‪crawler‬‬
‫لنمرّ أواًل عىل حل تمرين الفصل المشار إليه‪ .‬كنا قد و َّفرنا الشيفرة المبدئية للص‪ww‬نف ‪ WikiCrawler‬وك‪ww‬ان‬

‫المعرَّفة في ذلك الصنف‪:‬‬


‫المطلوب منك هو إكمال التابع ‪ .crawl‬انظر الحقول ُ‬

‫{ ‪public class WikiCrawler‬‬


‫يشير إلى المكان الذي بدأنا منه ‪//‬‬
‫;‪private final String source‬‬

‫المفهرس الذي سنخزن فيه النتائج ‪//‬‬


‫ِ‬
‫;‪private JedisIndex index‬‬

‫رتل محددات الموارد الموحدة المطلوب فهرستها ‪//‬‬


‫;)(>‪private Queue<String> queue = new LinkedList<String‬‬

‫َ‬
‫ستخدم لقراءة الصفحات من موقع ويكيبيديا ‪//‬‬‫ُي‬
‫;)(‪final static WikiFetcher wf = new WikiFetcher‬‬
‫}‬

‫نشئ كائنًا من الن‪ww‬وع ‪ ،WikiCrawler‬علين‪ww‬ا أن نُم‪w‬رِّر قيم‪ww‬تي ‪ source‬و ‪ .index‬يحت‪ww‬وي المتغ‪ww‬ير‬


‫عندما ن ُ ِ‬

‫‪ queue‬مبدئ ًيا عىل عنصر واحد فقط هو ‪.source‬‬


‫هياكل البيانات للمبرمجين‬ ‫البحث المنطقي ‪Boolean Search‬‬

‫الحِ ظ أن الرتل ‪ُ queue‬من َّفذ باستخدام قائمةٍ من الن‪ww‬وع ‪ ،LinkedList‬وبالت‪ww‬الي‪ ،‬تس‪ww‬تغرق عملي‪ww‬ة إض‪ww‬افة‬
‫ً‬
‫قائم‪w‬ة من الن‪w‬وع ‪ LinkedList‬إىل متغ‪w‬ير من‬ ‫العناصر إىل نهايته ‪-‬وحذفها من بدايته‪ -‬زمنًا ثابتًا‪ ،‬وألنن‪w‬ا أس‪w‬ندنا‬

‫المعرَّفة بالواجهة ‪ ،Queue‬أي سنَس‪ww‬تخدِم الت‪ww‬ابع ‪offer‬‬


‫النوع ‪ ،Queue‬أصبح استخدامنا له مقتصرًا عىل التوابع ُ‬
‫إلضافة العناصر و ‪ poll‬لحذفها منه‪.‬‬

‫انظر تنفيذنا للتابع ‪:WikiCrawler.crawl‬‬

‫{ ‪public String crawl(boolean testing) throws IOException‬‬


‫{ ))(‪if (queue.isEmpty‬‬
‫;‪return null‬‬
‫}‬
‫;)(‪String url = queue.poll‬‬
‫;)‪System.out.println("Crawling " + url‬‬

‫{ ))‪if (testing==false && index.isIndexed(url‬‬


‫;)"‪System.out.println("Already indexed.‬‬
‫;‪return null‬‬
‫}‬

‫;‪Elements paragraphs‬‬
‫{ )‪if (testing‬‬
‫;)‪paragraphs = wf.readWikipedia(url‬‬
‫{ ‪} else‬‬
‫;)‪paragraphs = wf.fetchWikipedia(url‬‬
‫}‬
‫;)‪index.indexPage(url, paragraphs‬‬
‫;)‪queueInternalLinks(paragraphs‬‬
‫;‪return url‬‬
‫}‬

‫السبب وراء التعقيد الموجود في التابع السابق هو تسهيل عملي‪ww‬ة اختب‪ww‬اره‪ .‬تُ ِّ‬
‫وض ‪w‬ح النق‪ww‬اط التالي‪ww‬ة المنط‪ww‬ق‬

‫المبني عليه التابع‪:‬‬

‫ً‬
‫فارغا‪ ،‬يعيد التابع القيمة الفارغة ‪ null‬لكي يشير إىل أنه لم يُفهرِس أي صفحة‪.‬‬ ‫إذا كان الرتل‬ ‫•‬

‫إذا لم يكن فار ًغا‪ ،‬فإنه يقرأ محدد الموارد الموحد ‪ URL‬التالي ويَحذِفه من الرتل‪.‬‬ ‫•‬

‫‪147‬‬
‫هياكل البيانات للمبرمجين‬ ‫البحث المنطقي ‪Boolean Search‬‬

‫سا بالفع‪ww‬ل‪ ،‬ال يُفهرِّس‪ww‬ه الت‪ww‬ابع م‪ww‬رة أخ‪ww‬رى إال إذا ك‪ww‬ان في وض‪ww‬ع‬
‫إذا كان محدد الموارد قيد االختيار ُمفهرَ ً‬ ‫•‬

‫االختبار‪.‬‬

‫يقرأ التابع بعد ذلك محتويات الصفحة‪ :‬إذا كان التابع في وضع االختبار‪ ،‬فإن‪ww‬ه يقرؤه‪ww‬ا من مل‪ww‬ف‪ ،‬وإن لم‬ ‫•‬

‫يكن كذلك‪ ،‬فإنه يقرؤها من شبكة اإلنترنت‪.‬‬

‫يُفهرِس الصفحة‪.‬‬ ‫•‬

‫يُحلِّل الصفحة ويضيف الروابط الداخلية الموجودة فيها إىل الرتل‪.‬‬ ‫•‬

‫يعيد في النهاية ُمحدّد موارد الصفحة التي فهرَسها للتو‪.‬‬ ‫•‬

‫كنا قد عرضنا تنفي ًذا للت‪ww‬ابع ‪ Index.indexPage‬في نفس الفص‪ww‬ل المش‪ww‬ار إلي‪ww‬ه في األعىل‪ ،‬أي أن الت‪ww‬ابع‬

‫الوحيد الجديد هو ‪.WikiCrawler.queueInternalLinks‬‬

‫كتبنا نسختين من ذلك التابع بمعامالت ‪ parameters‬مختلفة‪ :‬تَستق ِبل األوىل كائنًا من النوع ‪Elements‬‬

‫يتضمن شجرة ‪ DOM‬واحد ًة لكل فقرة‪ ،‬بينما تَستق ِبل الثانية كائنًا من النوع ‪ Element‬يُمثِل فقرة واحدة‪.‬‬
‫َّ‬

‫تمرّ النسخة األوىل عبر الفقرات‪ ،‬في حين تُن ِّفذ النسخة الثانية العمل الفعلي‪.‬‬

‫{ )‪void queueInternalLinks(Elements paragraphs‬‬


‫{ )‪for (Element paragraph: paragraphs‬‬
‫;)‪queueInternalLinks(paragraph‬‬
‫}‬
‫}‬

‫{ )‪private void queueInternalLinks(Element paragraph‬‬


‫;)"]‪Elements elts = paragraph.select("a[href‬‬
‫{ )‪for (Element elt: elts‬‬
‫;)"‪String relURL = elt.attr("href‬‬

‫{ ))"‪if (relURL.startsWith("/wiki/‬‬
‫;)"‪String absURL = elt.attr("abs:href‬‬
‫;)‪queue.offer(absURL‬‬
‫}‬
‫}‬
‫}‬

‫لكي نُح‪w‬دِّد م‪ww‬ا إذا ك‪ww‬ان ُمح‪w‬دّد م‪ww‬وارد موح‪ww‬د معين ه‪ww‬و ُمح‪w‬دّد داخلي‪ ،‬فإنن‪ww‬ا نفحص م‪ww‬ا إذا ك‪ww‬ان يب‪w‬دأ بكلمة‬

‫يتضمن ذلك بعض الصفحات التي ال نرغب في فهرستها مثل بعض الص‪ww‬فحات الوص‪ww‬فية لموق‪ww‬ع‬
‫َّ‬ ‫"‪ ."/wiki/‬قد‬

‫‪148‬‬
‫هياكل البيانات للمبرمجين‬ ‫البحث المنطقي ‪Boolean Search‬‬

‫ويكيبيديا‪ ،‬كما قد يستثني ذلك بعض الصفحات التي نريدها مثل روابط الصفحات المكتوب‪ww‬ة بلغ‪w‬ات أخ‪w‬رى غ‪ww‬ير‬

‫اإلنجليزية‪ ،‬ومع ذلك‪ ،‬تُع ّد تلك الطريقة جيدة بالقدر الكافي كبداية‪.‬‬

‫يتضمن هذا التمرين الكثير‪ ،‬فهو فرصة فقط لتجميع األجزاء الصغيرة معً ا‪.‬‬
‫َّ‬ ‫ال‬

‫‪ 16.2‬اسرتجاع البيانات‬
‫سننتقل اآلن إىل المرحلة التالية من المشروع‪ ،‬وهي تنفيذ أداة بحث تتك ّون مما يلي‪:‬‬

‫‪ .1‬واجهة تُمكِّن ُ‬
‫المستخدمين من إدخال كلمات البحث ومشاهدة النتائج‪.‬‬

‫تتضمنها‪.‬‬
‫َّ‬ ‫‪ .2‬طريقة الستقبال كلمات البحث وإعادة الصفحات التي‬

‫‪ .3‬طريقة لدمج نتائج البحث العائدة من عدة كلمات بحث‪.‬‬

‫‪ .4‬خوارزمية تُصن ِّف نتائج البحث وتُرتِّبها‪.‬‬

‫يُطلَق عىل تلك العمليات وما يشابهها اسم استرجاع المعلومات ‪.Information retrieval‬‬

‫ً‬
‫بسيطة من الخطوة رقم ‪ ،2‬وسنُر ِّكز في هذا التم‪ww‬رين عىل الخط‪ww‬وتين ‪ 3‬و ‪ .4‬ق‪ww‬د ت‪ww‬رغب‬ ‫ً‬
‫نسخة‬ ‫أنشأنا بالفعل‬

‫مهتما ببناء تطبيقات الويب‪.‬‬


‫ً‬ ‫بالعمل ً‬
‫أيضا عىل الخطوة رقم ‪ 1‬إذا كنت‬

‫‪ 16.3‬البحث المنطقي‪/‬الثنائي ‪Boolean search‬‬


‫تستطيع معظم محركات البحث أن تُن ِّفذ بح ًثا منطق ًيا‪ ،‬بمعنى أن بإمكانه‪ww‬ا دمج نت‪ww‬ائج البحث الخاص‪ww‬ة بع‪ww‬دة‬
‫ً‬
‫أمثلة عىل ذلك‪:‬‬ ‫كلمات باستخدام المنطق الثنائي‪ ،‬ولنأخذ‬

‫تعيد عملية البحث عن "‪ "java AND programming‬الصفحات التي تحتوي عىل الكلم‪ww‬تين "‪"java‬‬ ‫•‬

‫و "‪ "programming‬فقط‪.‬‬

‫تعيد عملية البحث عن "‪ "java OR programming‬الص‪ww‬فحات ال‪ww‬تي تحت‪ww‬وي عىل إح‪ww‬دى الكلم‪ww‬تين‬ ‫•‬

‫وليس بالضرورة كلتيهما‪.‬‬

‫تعيد عملية البحث عن "‪ "java -indonesia‬الصفحات التي تحتوي عىل كلمة "‪ "java‬وال تحت‪ww‬وي عىل‬ ‫•‬

‫كلمة "‪."indonesia‬‬

‫يُطلَق عىل تلك التعبيرات‪ ،‬أي تلك التي تحتوي عىل كلمات بحث وعمليات‪ ،‬اسم "استعالمات ‪."queries‬‬

‫عندما تُطبَّق تلك العمليات عىل نتائج البحث‪ ،‬ف‪ww‬إن الكلم‪ww‬ات "‪ "AND‬و "‪ "OR‬و "‪ "-‬تقاب‪ww‬ل في الرياض‪ِ w‬‬
‫‪w‬يات‬

‫عمليات "التقاطع" و "االتحاد" و "الفرق" عىل الترتيب‪ .‬لنفترض مثاًل أن‪:‬‬

‫‪ s1‬يمثل مجموعة الصفحات التي تحتوي عىل كلمة "‪،"java‬‬ ‫•‬

‫‪149‬‬
‫هياكل البيانات للمبرمجين‬ ‫البحث المنطقي ‪Boolean Search‬‬

‫‪ s2‬يمثل مجموعة الصفحات التي تحتوي عىل كلمة "‪،"programming‬‬ ‫•‬

‫‪ s3‬يمثل مجموعة الصفحات التي تحتوي عىل كلمة "‪،"indonesia‬‬ ‫•‬

‫في تلك الحالة‪:‬‬

‫يُمثِل التقاطع بين ‪ s1‬و ‪ s2‬مجموعة الصفحات التي تحتوي عىل الكلمتين "‪"java‬‬ ‫•‬

‫و "‪ "programming‬معً ا‪.‬‬

‫يُمثِل االتحاد بين ‪ s1‬و ‪ s2‬مجموعة الصفحات التي تحتوي عىل كلمة "‪ "java‬أو كلمة‬ ‫•‬

‫"‪."programming‬‬

‫يُمثِل الفرق بين ‪ s1‬و ‪ s3‬مجموعة الصفحات التي تحتوي عىل كلمة "‪ "java‬وال تحتوي عىل كلمة‬ ‫•‬

‫"‪."indonesia‬‬

‫ستكتب في القسم التالي تابعً ا يُن ِّفذ تلك العمليات‪.‬‬

‫‪ 16.4‬تمرين ‪13‬‬
‫ستجد ملفات شيفرة هذا التمرين في مستودع الكتاب‪:‬‬

‫‪ :WikiSearch.java‬يُعرِّف كائنًا يحتوي عىل نتائج البحث ويُطبِّق العمليات عليها‪.‬‬ ‫•‬

‫‪ :WikiSearchTest.java‬يحتوي عىل شيفرة اختبار للصنف‪.WikiSearch‬‬ ‫•‬

‫المعرَّف بالنوع ‪.java.util.Collections‬‬ ‫‪ :Card.java‬يُ ِّ‬


‫وضح طريقة استخدام التابع ‪ُ sort‬‬ ‫•‬

‫ستجد ً‬
‫أيضا بعض األصناف المساعدة التي استخدَمناها من قبل هذا الكتاب‪.‬‬

‫انظر بداية تعريف الصنف ‪:WikiSearch‬‬

‫{ ‪public class WikiSearch‬‬

‫يربط ُم ّ‬
‫حددات الموارد التي تحتوي على الكلمة بدرجة الارتباط ‪//‬‬
‫;‪private Map<String, Integer> map‬‬

‫{ )‪public WikiSearch(Map<String, Integer> map‬‬


‫;‪this.map = map‬‬
‫}‬

‫{ )‪public Integer getRelevance(String url‬‬


‫;)‪Integer relevance = map.get(url‬‬

‫‪150‬‬
‫هياكل البيانات للمبرمجين‬ ‫البحث المنطقي ‪Boolean Search‬‬

‫;‪return relevance==null ? 0: relevance‬‬


‫}‬
‫}‬

‫يحتوي كائن النوع ‪ WikiSearch‬عىل خريطة ‪ map‬تربط ُمحدّدات الموارد الموحدة ‪ URLs‬بدرج‪ww‬ة االرتب‪ww‬اط‬

‫‪ ،relevance score‬والتي تُمثِل ‪-‬ضمن سياق استرجاع البيانات‪ -‬عددًا يشير إىل المدى الذي يستوفي به ُمحدِّد‬

‫المستخدِم‪ .‬تتو ّفر الكثير من الطرائ‪ww‬ق لحس‪ww‬اب درج‪ww‬ة االرتب‪ww‬اط‪ ،‬ولكنه‪ww‬ا تعتم‪ww‬د في‬
‫الموارد االستعالم الذي يدخله ُ‬
‫الغالب عىل "تردد الكلمة" أي عدد مرات ظهوره‪ww‬ا في الص‪ww‬فحة‪ .‬يُع ‪ّ w‬د ‪ TF-IDF‬واح ‪w‬دًا من أك‪ww‬ثر درج‪ww‬ات االرتب‪ww‬اط‬

‫شيو ًعا‪ ،‬وتُمثِل األحرف اختصا ًرا لعبارة ت‪w‬ردد المص‪ww‬طلح ‪ - term frequency‬معك‪ww‬وس ت‪w‬ردد المس‪ww‬تند ‪inverse‬‬

‫‪.document frequency‬‬

‫إذا احتوى االستعالم عىل كلمة بحث واحدة‪ ،‬فإن درجة االرتباط لصفحة معينة تس‪ww‬اوي ت‪ww‬ردّد الكلم‪ww‬ة‪ ،‬أي‬ ‫•‬

‫عدد مرات ظهورها في تلك الصفحة‪.‬‬

‫بالنسبة لالستعالمات التي تحتوي عىل عدة كلم‪w‬ات‪ ،‬تك‪w‬ون درج‪w‬ة االرتب‪w‬اط لص‪w‬فحة معين‪w‬ة هي حاص‪w‬ل‬ ‫•‬

‫مجموع تردد الكلمات‪ ،‬أي عدد مرات ظهور أي كلمة منها‪.‬‬

‫واآلن وقد أصبحت مستعدًا لبدء التمرين‪ ،‬ن ِّفذ األمر ‪ ant build‬لتصريف ملف‪w‬ات الش‪w‬يفرة‪ ،‬ثم ن ِّفذ األم‪w‬ر‬

‫‪ .ant WikiSearchTest‬ستفشل االختبارات كالعادة ألن ما يزال عليك إكمال بعض العمل‪.‬‬

‫أكم‪ww‬ل متن ك‪ww‬لٍّ من التواب‪ww‬ع ‪ and‬و ‪ or‬و ‪ minus‬في المل‪ww‬ف ‪ WikiSearch.java‬لكي تتمكّن من اجتي‪ww‬از‬

‫االختبارات المرتبطة بتلك التوابع‪ .‬ال تقلق بشأن التابع ‪ ،testSort‬فسنعود إليه الح ًقا‪.‬‬

‫يُمكِنك أن تُن ِّفذ ‪ WikiSearchTest‬بدون اِستخدَام ‪ Jedis‬ألنه ال يعتمد عىل فهرس قاع‪ww‬دة بيان‪ww‬ات ‪Redis‬‬

‫الخاصة بك‪ ،‬ولكن‪ ،‬إذا أردت أن تَستخِ دم الفهرس لالستعالم ‪ ،query‬فال ب ُ ّد أن تو ِّفر بيانات الخادم في ملف‪ ،‬كما‬

‫أوضحنا في الفصل ‪ 14‬حفظ البيانات عبر ‪.Redis‬‬

‫ن ِّفذ األم‪w‬ر ‪ ant JedisMaker‬لكي تتأ ّك‪ww‬د من قدرت‪w‬ه عىل االتص‪ww‬ال بخ‪w‬ادم ‪ ،Redis‬ثم ن ِّفذ ‪WikiSearch‬‬

‫الذي يَطبَع نتائج االستعالمات الثالثة التالية‪:‬‬

‫"‪"java‬‬ ‫•‬

‫"‪"programming‬‬ ‫•‬

‫"‪"java AND programming‬‬ ‫•‬

‫لن تكون النتائج ُمرتَّبة في البداية ألن التابع ‪ WikiSearch.sort‬ما يزال غير مكتمل‪.‬‬

‫‪151‬‬
‫هياكل البيانات للمبرمجين‬ ‫البحث المنطقي ‪Boolean Search‬‬

‫أكمل متن التابع ‪ sort‬لكي تُص ِبح النتائج ُمرتَّبة تصاعديًا بحسب درجة االرتب‪w‬اط‪ .‬يُمكِن‪ww‬ك االس‪w‬تعانة بالت‪w‬ابع‬

‫المعرَّف بالنوع ‪ java.util.Collections‬حيث يُمكِنه ترتيب أي نوع قائم‪ww‬ة ‪ .List‬يُمِ كن‪ww‬ك االطالع‬
‫‪ُ sort‬‬
‫عىل توثيق النوع ‪.List‬‬

‫تتو َّفر نسختان من التابع ‪:sort‬‬

‫ً‬
‫قائمة وتُرتِّب عناصرها باستخدام التابع ‪ ،compareTo‬ول‪ww‬ذلك ينبغي أن‬ ‫نسخة أحادية المعامل تَستق ِبل‬ ‫•‬

‫تكون العناصر من النوع ‪.Comparable‬‬

‫نسخة ثنائية المعام‪ww‬ل تَس‪ww‬تق ِبل قائم‪ً w‬‬


‫‪w‬ة من أي ن‪ww‬وع وكائنً‪ww‬ا من الن‪ww‬وع ‪ ،Comparator‬ويُس‪ww‬تخدَم الت‪ww‬ابع‬ ‫•‬

‫المعرَّف ضمن الكائن لموازنة العناصر‪.‬‬


‫‪ُ compare‬‬

‫سنتحدث عن الواجهتين ‪ Comparable‬و ‪ Comparator‬في القسم التالي إن لم تكن عىل معرفة بهما‪.‬‬

‫‪ 16.5‬الواجهتان ‪ Comparable‬و ‪Comparator‬‬


‫يتضمن مستودع الكتاب الصنف ‪ Card‬الذي يحتوي عىل طريقتين لترتيب قائمة كائنات من الن‪ww‬وع ‪.Card‬‬
‫َّ‬
‫انظر إىل بداية تعريف الصنف‪:‬‬

‫{ >‪public class Card implements Comparable<Card‬‬

‫;‪private final int rank‬‬


‫;‪private final int suit‬‬

‫{ )‪public Card(int rank, int suit‬‬


‫;‪this.rank = rank‬‬
‫;‪this.suit = suit‬‬
‫}‬

‫تحتوي كائنات الصنف ‪ Card‬عىل الحقلين ‪ rank‬و ‪ suit‬من النوع العددي الصحيح‪ .‬يُن ِّفذ الص‪ww‬نف ‪Card‬‬

‫الواجهة >‪ Comparable<Card‬مما يَعنِي أنه بالضرورة يُو ِّفر تنفي ًذا للتابع ‪:compareTo‬‬

‫{ )‪public int compareTo(Card that‬‬


‫{ )‪if (this.suit < that.suit‬‬
‫;‪return -1‬‬
‫}‬
‫{ )‪if (this.suit > that.suit‬‬
‫;‪return 1‬‬
‫}‬

‫‪152‬‬
‫هياكل البيانات للمبرمجين‬ ‫البحث المنطقي ‪Boolean Search‬‬

‫{ )‪if (this.rank < that.rank‬‬


‫;‪return -1‬‬
‫}‬
‫{ )‪if (this.rank > that.rank‬‬
‫;‪return 1‬‬
‫}‬
‫;‪return 0‬‬
‫}‬

‫تشير بصمة التابع ‪ compareTo‬إىل أن عليه أن يعيد عددًا سالبًا إذا كان ‪ this‬أقل من ‪ ،that‬وعددًا موجبًا‬

‫إذا كان أكبر منه‪ ،‬وصفرًا إذا كانا متساويين‪.‬‬

‫إذا اس‪ww‬تخدمت نس‪ww‬خة الت‪ww‬ابع ‪ Collections.sort‬أحادي‪ww‬ة المعام‪ww‬ل‪ ،‬فإنه‪ww‬ا ب‪ww‬دورها تَس‪ww‬تدعِ ي الت‪ww‬ابع‬

‫المعرَّف ضمن العناصر لكي تتمكّن من ترتيبها‪ .‬عىل سبيل المثال‪ ،‬تُ ِ‬
‫نش‪w‬ئ الش‪ww‬يفرة التالي‪ww‬ة قائم‪ww‬ة‬ ‫‪ُ compareTo‬‬
‫تحتوي عىل ‪ 52‬بطاقة‪:‬‬

‫{ )(‪public static List<Card> makeDeck‬‬


‫;)(>‪List<Card> cards = new ArrayList<Card‬‬
‫{ )‪for (int suit = 0; suit <= 3; suit++‬‬
‫{ )‪for (int rank = 1; rank <= 13; rank++‬‬
‫;)‪Card card = new Card(rank, suit‬‬
‫;)‪cards.add(card‬‬
‫}‬
‫}‬
‫;‪return cards‬‬
‫}‬

‫ثم تُرتِّبها كالتالي‪:‬‬

‫;)‪Collections.sort(cards‬‬

‫تُرتِّب تلك النسخة من التابع ‪ sort‬العناصر وف ًقا لما يُطلَق علي‪ww‬ه "ال‪ww‬ترتيب الط‪ww‬بيعي" ألن ال‪ww‬ترتيب ُمح‪w‬دّد‬

‫بواسطة العناصر نفسها‪.‬‬

‫أيضا أن نستعين بكائن من النوع ‪ Comparator‬لكي نَفرِض نو ًعا مختل ًفا من ال‪ww‬ترتيب‪.‬‬
‫في المقابل‪ ،‬يُمكِننا ً‬

‫المرتَبَة األقلَّ بحسب الترتيب الطبيعي للصنف ‪ ،Card‬ومع ذلك‪ ،‬فإنها‬


‫عىل سبيل المثال‪ ،‬تحتل بطاقات األص َ‬
‫أحيانًا تحتل المرتبة األكبر في بعض ألعاب البطاق‪ww‬ات‪ ،‬ول‪ww‬ذلك‪ ،‬س‪w‬نُعرِّف كائنً‪ww‬ا من الن‪ww‬وع ‪ Comparator‬يُعامِ‪ w‬ل‬

‫األص عىل أنّها البطاقة األكبر ضمن مجموعة بطاقات اللعب‪ .‬انظر إىل شيفرة ذلك النوع‪:‬‬
‫ّ‬

‫‪153‬‬
‫هياكل البيانات للمبرمجين‬ Boolean Search ‫البحث المنطقي‬

Comparator<Card> comparator = new Comparator<Card>() {


@Override
public int compare(Card card1, Card card2) {
if (card1.getSuit() < card2.getSuit()) {
return -1;
}
if (card1.getSuit() > card2.getSuit()) {
return 1;
}
int rank1 = getRankAceHigh(card1);
int rank2 = getRankAceHigh(card2);

if (rank1 < rank2) {


return -1;
}
if (rank1 > rank2) {
return 1;
}
return 0;
}

private int getRankAceHigh(Card card) {


int rank = card.getRank();
if (rank == 1) {
return 14;
} else {
return rank;
}
}
};

‫ ثم‬،‫وب‬ww‫و المطل‬ww‫ عىل النح‬compare ‫ابع‬ww‫ يُن ِّفذ الت‬anonymous ‫م‬ww‫ول االس‬ww‫ن ًفا مجه‬ww‫تُعرِّف تلك الشيفرة ص‬
ً
‫ا إذا لم تكن‬ww‫ في لغة جاف‬Anonymous Classes ‫ يُمكِنك القراءة عن األصناف مجهولة االسم‬.‫نسخة منه‬ ِ ُ‫ت‬
‫نشئ‬

.‫عىل معرفة بها‬

‫يفرة‬ww‫بين في الش‬ww‫ كما هو م‬،sort ‫ إىل التابع‬Comparator ‫يُمكِننا اآلن أن نُمرِّر ذلك الكائن المنتمي للنوع‬

:‫التالية‬

154
‫هياكل البيانات للمبرمجين‬ ‫البحث المنطقي ‪Boolean Search‬‬

‫;)‪Collections.sort(cards, comparator‬‬

‫يُعد األص البستوني وف ًقا لهذا الترتيب البطاقة األكبر ضمن مجموع‪ww‬ة بطاق‪ww‬ات اللعب‪ ،‬بينم‪ww‬ا تع‪ww‬د البطاق‪ww‬ة‬

‫ذات الرقم ‪ 2‬البطاقة األصغر‪.‬‬

‫ستجد شيفرة هذا القسم في الملف ‪ Card.java‬إن كنت تريد تجريبه‪ .‬قد ترغب ً‬
‫أيضا في كتابة ك‪ww‬ائن آخ‪ww‬ر‬

‫من الن‪ww‬وع ‪ Comparator‬يُ‪ww‬رتِّب العناص‪ww‬ر بن‪ww‬ا ًء عىل قيم‪ww‬ة ‪ rank‬أواًل ثم قيم‪ww‬ة ‪ ،suit‬وبالت‪ww‬الي‪ ،‬تص‪ww‬بح جمي‪ww‬ع‬

‫األص معً ا وجميع البطاقات الثنائية معً ا‪ ،‬وهكذا‪.‬‬


‫ّ‬ ‫بطاقات‬

‫‪ 16.6‬ملحقات‬
‫إذا تمكَّنت من كتابة الكائن الذي أشرنا إليه في األعىل‪ ،‬هاك بعض األفك‪w‬ار األخ‪w‬رى ال‪w‬تي يُمكِن‪w‬ك أن تح‪w‬اول‬

‫القيام بها‪:‬‬

‫اقرأ عن درجة االرتباط ‪ TF-IDF‬ون ِّفذها‪ .‬قد تحتاج إىل تعديل الصنف ‪ JavaIndex‬لكي تجعل‪ww‬ه يَ ِ‬
‫حس ‪w‬ب‬ ‫•‬

‫قيمة ترددات المستند أي عدد مرات ظهور كل كلمة في جميع الصفحات الموجودة بالفهرس‪.‬‬

‫بالنسبة لالستعالمات المك َّونة من أكثر من كلمةٍ واحدة‪ ،‬يُمكِنك أن تَ ِ‬


‫حسب درجة االرتباط اإلجمالية لك‪ww‬ل‬ ‫•‬
‫صفحة بحساب مجموع درجة االرتباط لجميع الكلمات‪ .‬فكر متى يُمكِن لهذه النسخة المبسطة أن تَ َ‬
‫فشل‬

‫وجرِّب بدائل أخرى‪.‬‬

‫للمس‪ww‬تخدِمين بإدخ‪ww‬ال اس‪ww‬تعالمات تحت‪ww‬وي عىل عوام‪ww‬ل ‪operators‬‬ ‫أنش ‪w‬ئ واجه‪ww‬ة ُمس‪ww‬تخدِم تَس‪َ w‬‬
‫‪w‬مح ُ‬ ‫ِ‬ ‫•‬

‫الم ْدخَلة‪ ،‬وولِّد النتائج‪ ،‬ثم رتِّبها بحس‪ww‬ب درج‪ww‬ة االرتب‪ww‬اط‪ ،‬واع‪ww‬رض ُمح‪w‬دِّدات‬
‫منطقية‪ .‬حلِّل االستعالمات ُ‬
‫أيضا أن تُولِّد مقطع شيفرة يَعرِض مكان ظه‪ww‬ور كلم‪ww‬ات البحث‬
‫الموارد التي أحرزت أعىل درجات‪ .‬حاول ً‬

‫في الصفحة‪.‬‬

‫‪155‬‬
‫‪ .17‬الرتتيب ‪Sorting‬‬

‫لدى أقسام علوم الحاسوب هوس غير طبيعي بخوارزميات الترتيب‪ ،‬فبنا ًء عىل ال‪ww‬وقت ال‪ww‬ذي يمض‪ww‬يه طلب‪ww‬ة‬

‫علوم الحاسوب في دراسة هذا الموضوع‪ ،‬قد تظن أن االختيار ما بين خوارزمي‪ww‬ات ال‪ww‬ترتيب ه‪ww‬و حج‪ww‬ر الزاوي‪ww‬ة في‬

‫هندسة البرمجيات الحديثة‪ .‬واقع األمر هو أن مطوري البرمجي‪ww‬ات ق‪ww‬د يقض‪ww‬ون س‪ww‬نوات ق‪ww‬د تص‪ww‬ل إىل مس‪ww‬ارهم‬

‫المهني بأكمل‪ww‬ه دون التفك‪ww‬ير في طريق‪ww‬ة ح‪ww‬دوث عملي‪ww‬ة ال‪ww‬ترتيب‪ ،‬فهم يَس‪ww‬تخدِمون في ك‪ww‬ل التطبيق‪ww‬ات تقريبً‪ww‬ا‬
‫ً‬
‫كافي‪ww‬ة في‬ ‫الخوارزمي‪ww‬ة متع‪ww‬ددة األغ‪ww‬راض ال‪ww‬تي تو ِّفره‪ww‬ا اللغ‪ww‬ة أو المكتب‪ww‬ة ال‪ww‬تي يس‪ww‬تخدمونها‪ ،‬وال‪ww‬تي تك‪ww‬ون‬

‫معظم الحاالت‪.‬‬

‫لذلك لو تجاهلت هذا الفصل ولم تتعلم أي ش‪ww‬يء عن خوارزمي‪ww‬ات ال‪ww‬ترتيب‪ ،‬فم‪ww‬ا ي‪ww‬زال بإمكان‪ww‬ك أن تص‪ww‬بح‬

‫ُمط ِّور برمجيات جيدًا‪ ،‬ومع ذلك‪ ،‬هناك عدة أسباب قد تدفعك لقراءته‪:‬‬

‫‪ .1‬عىل الرغم من وجود خوارزميات متعددة األغ‪w‬راض يُمكِنه‪w‬ا العم‪ww‬ل بش‪w‬كل جي‪w‬د في غالبي‪ww‬ة التطبيق‪w‬ات‪،‬‬

‫تخصص‪ww‬تان ق‪ww‬د تحت‪ww‬اج إىل معرف‪ww‬ة القلي‪ww‬ل عنهم‪ww‬ا‪ :‬ال‪ww‬ترتيب بالج‪ww‬ذر ‪radix sort‬‬
‫هنالك خوارزم ّيت‪ww‬ان ُم ّ‬
‫والترتيب بالكومة المقيدّة ‪.bounded heap sort‬‬

‫قس‪w‬م‬ ‫التعليمي األمث‪ww‬ل‪ ،‬فهي تُ ِّ‬


‫وض‪w‬ح اس‪ww‬تراتيجية " ِّ‬ ‫ّ‬ ‫‪ .2‬تُع ّد خوارزمية الترتيب بالدمج ‪ merge sort‬المثال‬

‫واغزُ ‪ "divide and conquer‬المهمة والمفيدة في تصميم الخوارزميات‪ .‬باإلضافة إىل ذلك‪ ،‬س‪ww‬تتعلم‬

‫عن ت‪ww‬رتيب نم‪ww‬و ‪ order of growth‬لم تَ‪ww‬رَ ُه من قب‪ww‬ل‪ ،‬ه‪ww‬و ت‪ww‬رتيب النم‪ww‬و "الخطي‪-‬اللوغ‪ww‬اريتمي‬
‫‪w‬ات هجين‪ً w‬‬
‫‪w‬ة‬ ‫‪ ."linearithmic‬ومن الجدير بال‪ww‬ذكر أن غالبي‪ww‬ة الخوارزمي‪ww‬ات الش‪ww‬هيرة تك‪ww‬ون ع‪ً w‬‬
‫‪w‬ادة خوارزم ّي‪ٍ w‬‬

‫بشكل أو بآخر فكرة الترتيب بالدمج‪.‬‬


‫ٍ‬ ‫وتستخدم‬

‫‪ .3‬أحد األسباب األخرى التي قد تدفعك إىل تعلم خوارزميات الترتيب هي مقابالت العم‪ww‬ل التقني‪ww‬ة‪ ،‬فع‪ww‬اد ًة‬

‫ما تُسأل خاللها عن تلك الخوارزميات‪ .‬إذا كنت تريد الحصول عىل وظيفة‪ ،‬فس ُيساعدك إظه‪ww‬ار اطالع‪ww‬ك‬

‫عىل أبجديات علوم الحاسوب‪.‬‬


‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫سنُحلِّل في هذا الفصل خوارزمية الترتيب باإلدراج ‪ ،insertion sort‬وس‪ww‬نن ِّفذ خوارزمي‪ww‬ة ال‪ww‬ترتيب بال‪ww‬دمج‪،‬‬

‫المق ّيدة‪.‬‬ ‫ً‬


‫بسيطة من خوارزمية الترتيب بالكومة ُ‬ ‫ً‬
‫نسخة‬ ‫وأخي ًرا‪ ،‬سنشرح خوارزمية الترتيب بالجذر‪ ،‬وسنكتب‬

‫‪ 17.1‬الرتتيب باإلدراج ‪Insertion sort‬‬


‫سنبدأ بخوارزمية الترتيب باإلدراج‪ ،‬ألنها بسيطة ومهمة‪ .‬عىل الرغم من أنها ليست الخوارزمية األكف‪ww‬أ إال أنه‪ww‬ا‬

‫تملك بعض الميزات المتعلقة بتحرير الذاكرة كما سنرى الح ًقا‪.‬‬

‫لن نشرح هذه الخوارزمية هنا‪ ،‬ولكن يُ ّ‬


‫فضلُ لو قرأت مقالة ويكيبيديا عن الترتيب باإلدراج ‪،Insertion Sort‬‬

‫فهي تحتوي عىل شيفرة وهمية وأمثلة متحركة‪ .‬وبعدما تفهم فكرتها العامة يمكنك متابعة القراءة هنا‪.‬‬

‫تَعرِض الشيفرة التالية تنفي ًذا بلغة جافا لخوارزمية الترتيب باإلدراج‪:‬‬

‫{ >‪public class ListSorter<T‬‬

‫)‪public void insertionSort(List<T> list, Comparator<T> comparator‬‬


‫{‬

‫{ )‪for (int i=1; i < list.size(); i++‬‬


‫;)‪T elt_i = list.get(i‬‬
‫;‪int j = i‬‬
‫{ )‪while (j > 0‬‬
‫;)‪T elt_j = list.get(j-1‬‬
‫{ )‪if (comparator.compare(elt_i, elt_j) >= 0‬‬
‫;‪break‬‬
‫}‬
‫;)‪list.set(j, elt_j‬‬
‫;‪j--‬‬
‫}‬
‫;)‪list.set(j, elt_i‬‬
‫}‬
‫}‬
‫}‬

‫كحاو لخوارزمي‪w‬ات ال‪w‬ترتيب‪ .‬نظ‪ً w‬را ألنن‪w‬ا اس‪w‬تخدمنا معام‪w‬ل ن‪w‬وع ‪type‬‬
‫ٍ‬ ‫عمل‬
‫عرَّفنا الصنف ‪ ListSorter‬ل َي َ‬
‫‪ ،parameter‬اسمه ‪ ،T‬ستتمكَّن التوابع التي سنكتبها من العمل مع قوائم تحتوي عىل أي نوع من الكائنات‪.‬‬

‫‪157‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫د من الواجه‪w‬ة ‪List‬‬
‫يستقبل التابع ‪ insertionSort‬مع‪w‬املين‪ :‬األول عب‪w‬ارة عن قائم‪w‬ة من أي ن‪w‬وع ممت‪ّ w‬‬

‫والثاني عبارة عن كائن من النوع ‪ Comparator‬بإمكانه موازن‪ww‬ة كائن‪ww‬ات الن‪ww‬وع ‪ .T‬يُ‪ww‬رتِّب ه‪ww‬ذا الت‪ww‬ابع القائم‪ww‬ة في‬

‫نفس المكان أي أنه يُعدِّل القائمة الموجودة وال يحتاج إىل حجز مساحة إضافية جديدة‪.‬‬

‫تَستدعِ ي الشيفرة التالية هذا التابع مع قائمة من النوع ‪ List‬تحتوي عىل كائنات من النوع ‪:Integer‬‬

‫(>‪List<Integer> list = new ArrayList<Integer‬‬


‫;))‪Arrays.asList(3, 5, 1, 4, 2‬‬

‫{ )(>‪Comparator<Integer> comparator = new Comparator<Integer‬‬


‫‪@Override‬‬
‫{ )‪public int compare(Integer elt1, Integer elt2‬‬
‫;)‪return elt1.compareTo(elt2‬‬
‫}‬
‫;}‬

‫;)(>‪ListSorter<Integer> sorter = new ListSorter<Integer‬‬


‫;)‪sorter.insertionSort(list, comparator‬‬
‫;)‪System.out.println(list‬‬

‫يحتوي التابع ‪ insertionSort‬عىل حلقتين متداخلتين ‪ ،nested loops‬ولذلك‪ ،‬قد تظن أن زمن تنفيذه‬

‫تربيعي‪ ،‬وهذا صحيحٌ في تلك الحالة‪ ،‬ولكن قبل أن تتوص‪ww‬ل إىل تل‪ww‬ك النتيج‪ww‬ة‪ ،‬علي‪ww‬ك أواًل أن تتأ ّك‪ww‬د من أن ع‪ww‬دد‬

‫مرات تنفي ِذ كل حلقةٍ يتناسب مع حجم المصفوفة ‪.n‬‬

‫تتكرر الحلقة الخارجية من ‪ 1‬إىل )(‪ ،list.size‬ولذلك تُع ّد خط ّي ًة بالنسبة لحجم القائم‪ww‬ة ‪ ،n‬بينم‪ww‬ا تتك‪ww‬رر‬

‫الحلقة الداخلية من ‪ i‬إىل صفر‪ ،‬لذلك هي ً‬


‫أيضا خط ّية بالنسبة لقيمة ‪ .n‬بنا ًء عىل ذلك‪ ،‬يكون ع‪ww‬دد م‪ww‬رات تنفي‪ww‬ذ‬

‫الحلقة الداخلية تربيع ًّيا‪.‬‬

‫إذا لم تكن متأ ّكدًا من ذلك‪ ،‬انظر إىل البرهان التالي‪:‬‬

‫في المرة األوىل‪ ،‬قيمة ‪ i‬تساوي ‪ ،1‬وتتكرر الحلقة الداخل ّية مر ًة واحد ًة عىل األكثر‪.‬‬ ‫•‬

‫في المرة الثانية‪ ،‬قيمة ‪ i‬تساوي ‪ ،2‬وتتكرر الحلقة الداخل ّية مرتين عىل األكثر‪.‬‬ ‫•‬

‫في المرة األخيرة‪ ،‬قيمة ‪ i‬تساوي ‪ ،n-1‬وتتكرر الحلقة الداخل ّية عددًا قدره ‪ n-1‬من المرات عىل األكثر‪.‬‬ ‫•‬

‫‪w‬اوي‬
‫وبالتالي‪ ،‬يكون عدد مرات تنفيذ الحلقة الداخلية هو مجموع المتتالية ‪ ... ،2 w،1‬ح‪ww‬تى ‪ ،n-1‬وه‪ww‬و م‪ww‬ا يُس‪ِ w‬‬
‫‪ .n(n-1)/2‬الحِ ظ أن القيمة األساسية (ذات األس األكبر) بهذا التعبير هي ‪.n2‬‬

‫يُع ّد الترتيب باإلدراج تربيع ًيا في أسوأ حالة‪ ،‬ومع ذلك‪:‬‬

‫‪158‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫‪ .1‬إذا كانت العناصر ُمرتَّ ً‬


‫بة أو شبه ُمرتَّبةٍ بالفعل‪ ،‬فإن الترتيب باإلدراج يكون ّ‬
‫خط ًّيا‪ .‬بالتحديد‪ ،‬إذا لم يكن كل‬
‫ً‬
‫مسافة أكب َر من ‪ ،k‬فإن الحلقة الداخلي‪ww‬ة لن تُن َّفذ أك‪ww‬ثر من ع‪ww‬د ٍد ق‪ww‬دره ‪k‬‬ ‫عنصر ٍ أبع َد من موضعه الصحيح‬

‫من المرات‪ ،‬ويكون زمن التنفيذ الكلي هو )‪.O(kn‬‬

‫‪ .2‬نظ ًرا ألن تنفيذ تلك الخوارزمية بسيط‪ ،‬فإن تكلفته منخفضة‪ ،‬أي عىل الرغم من أن زمن التنفي‪ww‬ذ يس‪ww‬اوي‬

‫‪ ،a n2‬إال أن المعامل ‪ a‬قد يكون صغيرًا‪.‬‬

‫ولذلك‪ ،‬إذا عرفنا أن المصفوفة شبه ُمرتَّبة أو إذا لم تكن كبير ًة جدًا‪ ،‬فقد يكون الترتيب ب‪ww‬اإلدراج خي‪ww‬ا ًرا جي ‪w‬دًا‪،‬‬

‫ولكن بالنسبة للمصفوفات الكبيرة‪ ،‬فهنالك خيارات أفضل بكثير‪.‬‬

‫‪ 17.2‬تمرين ‪14‬‬
‫تُع ّد خوارزمية الترتيب بالدمج ‪ merge sort‬واحد ًة من ضمن مجموعةٍ من الخوارزمي‪ww‬ات ال‪ww‬تي يتف‪ww‬وق زمن‬

‫التربيعي‪ .‬ننصحك قبل المتابعة بقراءة مقالة ويكيبي‪ww‬ديا عن ال‪ww‬ترتيب بال‪ww‬دمج ‪.merge sort‬‬
‫ّ‬ ‫تنفيذها عىل الزمن‬

‫بعد أن تفهم الفكرة العامة للخوارزمية‪ ،‬يُمكِنك العودة الختبار فهمك بكتابة تنفي ٍذ لها‪.‬‬

‫ستجد ملفات الشيفرة التالية الخاصة بالتمرين في مستودع الكتاب‪:‬‬

‫‪ListSorter.java‬‬ ‫•‬

‫‪ListSorterTest.java‬‬ ‫•‬

‫ن ِّفذ األمر ‪ ant build‬لتصريف ملفات الشيفرة ثم ن ِّفذ األمر ‪ .ant ListSorterTest‬سيفشل االختبار‬

‫كالعادة ألن ما يزال عليك إكمال بعض الشيفرة‪.‬‬

‫ستجد ضمن الملف ‪ ListSorter.java‬شيفر ًة مبدئ ّي ًة للتابعين ‪ mergeSortInPlace‬و ‪:mergeSort‬‬

‫>‪public void mergeSortInPlace(List<T> list, Comparator<T‬‬


‫{ )‪comparator‬‬
‫;)‪List<T> sorted = mergeSortHelper(list, comparator‬‬
‫;)(‪list.clear‬‬
‫;)‪list.addAll(sorted‬‬
‫}‬

‫)‪private List<T> mergeSort(List<T> list, Comparator<T> comparator‬‬


‫{‬
‫!‪// TODO: fill this in‬‬
‫;‪return null‬‬
‫}‬

‫‪159‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫واجهات مختلفة‪ .‬يَس‪ww‬تق ِبل الت‪ww‬ابع ‪ mergeSort‬قائم‪ً w‬‬


‫‪w‬ة ويعي‪ww‬د‬ ‫ٍ‬ ‫يقوم التابعان بنفس الشيء‪ ،‬ولكنهما يوفران‬
‫ً‬
‫جدي‪www‬دة تحت‪www‬وي عىل نفس العناص‪www‬ر بع‪www‬د ترتيبه‪www‬ا ترتيبً‪www‬ا تص‪www‬اعديًا‪ .‬في المقاب‪www‬ل‪ ،‬يُع‪www‬دِّل الت‪www‬ابع‬ ‫ً‬
‫قائم‪www‬ة‬

‫‪ mergeSortInPlace‬القائمة ذاتها وال يعيد أيّة قيمة‪.‬‬

‫عليك إكم‪ww‬ال الت‪ww‬ابع ‪ .mergeSort‬ويمكن‪ww‬ك ب‪ww‬داًل من كتاب‪ww‬ة نس‪ww‬خة تعاودي‪ww‬ة ‪ recursive‬بالكام‪ww‬ل‪ ،‬أن تتب‪ww‬ع‬

‫الطريقة التالية‪:‬‬

‫قسم القائمة إىل نصفين‪.‬‬


‫ِّ‬ ‫‪.1‬‬

‫‪ .2‬رتِّب النصفين باستخدام التابع ‪ Collections.sort‬أو التابع ‪.insertionSort‬‬

‫المرتَّبين إىل قائمة واحدة ُمرتَّبة‪.‬‬


‫‪ .3‬ادمج النصفين ُ‬

‫سيعطيك هذا التمرين الفرصة لتنقيح شيفرة الدمج دون التعامل مع تعقيدات التوابع التعاودية‪.‬‬

‫واآلن أضف حالة أساسية ‪ .base case‬إذا كان لديك قائمة تحتوي عىل عنصر واحد فقط‪ ،‬يُمكِنك أن تعيدها‬

‫مباشر ًة ألنها نوعً ا ما ُمرتَّبة بالفعل‪ ،‬وإذا كان طول القائمة أقل من قيمة معينة‪ ،‬يُمكِنك أن تُرتِّبها باستخدام الت‪ww‬ابع‬

‫‪ Collections.sort‬أو التابع ‪ .insertionSort‬اختبر الحالة األساسية قبل إكمال القراءة‪.‬‬

‫أخيرًا‪ ،‬عدِّل الحل واجعله يُن ِّفذ استدعاءين تعاوديّين لترتيب نصفي المصفوفة‪ .‬إذا عدلته بالش‪ww‬كل الص‪ww‬حيح‪،‬‬

‫ينبغي أن ينجح االختباران ‪ testMergeSort‬و ‪.testMergeSortInPlace‬‬

‫‪ 17.3‬تحليل أداء خوارزمية الرتتيب بالدمج‬


‫لكي نصنف زمن تنفيذ خوارزمية الترتيب بالدمج‪ ،‬علينا أن نفكر بمستويات التعاود وبكمية العمل المطلوب‬

‫في كل مستوى‪ .‬لنفترض أننا سنبدأ بقائمةٍ تحتوي عىل عد ٍد قدره ‪ n‬من العناصر‪ .‬وفيما يلي خطوات الخوارزمية‪:‬‬

‫‪ .1‬ن ُ ِ‬
‫نشئ مصفوفتين وننسخ نصف العناصر إليهما‪.‬‬

‫‪ .2‬نُرتِّب النصفين‪.‬‬

‫‪ .3‬ندمج النصفين‪.‬‬

‫‪160‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫خط ّية‪ .‬بالمثل‪ ،‬تنسخ الخطوة الثالثة كل عنصر مر ًة واح‪ww‬د ًة‬


‫تنسخ الخطوة األوىل كل عنصر مر ًة واحد ًة‪ ،‬أي أنها ّ‬
‫خط ّية كذلك‪ .‬علينا اآلن أن نُحدِّد تعقيد الخطوة الثانية‪ .‬ستس‪ww‬اعدنا عىل ذل‪ww‬ك الص‪ww‬ورة التالي‪ww‬ة ال‪ww‬تي‬
‫فقط‪ ،‬أي أنها ّ‬
‫تَعرِض مستويات التعاود‪.‬‬

‫في المستوى األعىل‪ ،‬سيكون لدينا قائمة واحدة ُمك َّونة من عد ٍد قدره ‪ n‬من العناصر‪ .‬للتبسيط‪ ،‬سنفترض أن‬

‫‪ n‬عبارة عن قيمة مرفوعة لألس ‪ ،2‬وبالتالي‪ ،‬سيكون لدينا في المستوى التالي قائمت‪ww‬ان تحتوي‪ww‬ان عىل ع‪ww‬دد ‪n/2‬‬

‫ثم في المستوى التالي‪ ،‬سيكون لدينا ‪ 4‬قوائم تحتوي عىل عدد قدره ‪ n/4‬من العناصر‪ ،‬وهكذا ح‪ww‬تى‬
‫من العناصر‪ّ .‬‬
‫نصل إىل عدد ‪ n‬من القوائم تحتوي جميعها عىل عنصر واحد فقط‪.‬‬

‫لدينا إ ًذا عدد قدره ‪ n‬من العناصر في كل مستوى‪ .‬أثناء نزولنا في المستويات‪ّ ،‬‬
‫قسمنا المص‪ww‬فوفات في ك‪ww‬ل‬

‫مستوى إىل نصفين‪ ،‬وهو ما يستغرق زمنًا يتناسب مع ‪ n‬في كل مستوى‪ ،‬وأثناء صعودنا لألعىل‪ ،‬علينا أن ن‪ww‬دمج‬

‫عددًا من العناصر مجموعه ‪ n‬وهو ما يستغرق زمنًا خط ًيا ً‬


‫أيضا‪.‬‬

‫‪161‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫إذا كان عدد المس‪ww‬تويات يس‪ww‬اوي ‪ ،h‬ف‪ww‬إن العم‪ww‬ل اإلجم‪ww‬الي المطل‪ww‬وب يس‪ww‬اوي )‪ ،O(nh‬واآلن‪ ،‬كم ه‪ww‬و ع‪ww‬دد‬

‫المستويات؟ يُمكِننا أن نفكر في ذلك بطريقتين‪:‬‬

‫‪ .1‬كم عدد المرات التي سنضطر خاللها لتقسيم ‪ n‬إىل نصفين حتى نصل إىل ‪.1‬‬

‫‪ .2‬أو كم عدد المرات التي سنضطرّ خاللها لمضاعفة العدد ‪ 1‬قبل أن نصل إىل ‪.n‬‬

‫يُمكِننا طرح السؤال الثاني بطريقة أخرى‪" :‬ما هي قيمة األس المرفوع للعدد ‪ 2‬لكي نحصل عىل ‪n‬؟"‪.‬‬

‫‪2h = n‬‬

‫بحساب لوغاريتم أساس ‪ 2‬لكال الطرفين‪ ،‬نحصل عىل التالي‪:‬‬

‫‪h = log2 n‬‬

‫أي أن الزمن الكلي يساوي ))‪ .O(n log(n‬الح‪w‬ظ أنن‪ww‬ا تجاهلن‪ww‬ا قيم‪ww‬ة أس‪w‬اس اللوغ‪w‬اريتم ألن اختالف أس‪w‬اس‬

‫اللوغاريتم يؤثر فقط بعامل ثابت‪ ،‬أي أن جميع اللوغاريتمات لها نفس ترتيب النمو ‪.order of growth‬‬

‫يُطلَق أحيانًا عىل الخوارزميات التي تنتمي إىل ))‪ O(n log(n‬اسم "خطي‪-‬لوغاريتمي ‪ ،"linearithmic‬ولكن‬

‫في العادة نقول "‪."n log n‬‬

‫في الواقع‪ ،‬يُع ّد ))‪ O(n log(n‬الحد األدنى من الناحية النظرية لخوارزميات الترتيب التي تَعتم‪ww‬د عىل موازن‪ww‬ة‬

‫العناصر مع بعض‪ww‬ها البعض‪ .‬يع‪ww‬ني ذل‪ww‬ك أن‪ww‬ه ال توج‪ww‬د خوارزمي‪ww‬ة ت‪ww‬رتيب بالموازن‪ww‬ة ذات ت‪ww‬رتيب نم‪ٍّ w‬و أفض‪ww‬لَ من‬

‫‪.n log n‬‬

‫ترتيب ال تعتمد عىل الموازنة وتستغرق زمنًا خط ًيا‪.‬‬


‫ٍ‬ ‫ولكن كما سنرى في القسم التالي‪ ،‬هناك خوارزميات‬

‫‪ 17.4‬خوارزمية الرتتيب بالجذر ‪Radix sort‬‬


‫إن واجهنا سؤااًل عن أكفأ طريقة لترتيب ملي‪ww‬ون ع‪ww‬دد ص‪ww‬حيح من ن‪w‬وع ‪ 32‬بت فلن تك‪ww‬ون خوارزمي‪ww‬ة ت‪ww‬رتيب‬

‫الفقاعات الطريقة األفضل‪ ،‬فخوارزمية ترتيب الفقاعات ‪ bubble sort‬صحيحٌ أنها بسيطة وسهلة الفهم‪ ،‬لكنّه‪ww‬ا‬

‫تستغرق زمنًا تربيع ًيا‪ ،‬كما أن أداءها ليس جيدًا بالموازنة مع خوارزميات الترتيب التربيعية األخرى‪.‬‬

‫ربما خوارزمية الترتيب بالجذر ‪ radix sort‬هي اإلجابة األدق عن السؤال‪ ،‬فهي خوارزمية ترتيب غ‪ww‬ير مبن ّي‪ww‬ة‬

‫عىل الموازنة‪ ،‬كما أنها تَ َ‬


‫عمل بنجاح عندما يكون حجم العناص‪ww‬ر مق ّي‪ w‬دًا كع‪ww‬دد ص‪ww‬حيح من ن‪ww‬وع ‪ 32‬بت أو سلس‪ww‬لة‬

‫نصية ُمك َّونة من ‪ 20‬محر ًفا‪.‬‬

‫ّسا ‪ stack‬من البطاقات‪ ،‬وكل واح‪ww‬دة منه‪ww‬ا تحت‪ww‬وي عىل كلم‪ww‬ة‬


‫لكي نفهم طريقة عملها‪ ،‬لنتخيل أن لدينا مكد ً‬
‫ُمك َّونة من ثالثة أحرف‪ .‬ها هي الطريقة التي يُمكِن أن نرتب بها تلك البطاقات‪:‬‬

‫‪162‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫‪ .1‬مرّ عبر البطاقات وقسمها إىل مجموعات بن‪ww‬ا ًء عىل الح‪ww‬رف األول‪ ،‬أي ينبغي أن تك‪ww‬ون الكلم‪ww‬ات البادئ‪ww‬ة‬

‫بالحرف ‪ a‬ضمن مجموعة واحدة‪ ،‬يليها الكلمات التي تبدأ بحرف ‪ ،b‬وهكذا‪.‬‬

‫قسم كل مجموعة مرة أخرى بنا ًء عىل الحرف الثاني‪ ،‬بحيث تصبح الكلم‪ww‬ات البادئ‪ww‬ة ب‪ww‬الحرفين ‪ aa‬معً ‪ w‬ا‪،‬‬
‫ِّ‬ ‫‪.2‬‬
‫يليها الكلمات التي تبدأ بالحرفين ‪ ،ab‬وهكذا‪ .‬لن تكون كل المجموعات ممل‪ً w‬‬
‫‪w‬وءة بالتأكي‪ww‬د‪ ،‬ولكن ال ب‪ww‬أس‬

‫بذلك‪.‬‬

‫قسم كل مجموعة مرة أخرى بحسب الحرف الثالث‪.‬‬


‫ِّ‬ ‫‪.3‬‬

‫واآلن‪ ،‬أصبحت كل مجموعة ُمك َّونة من عنصر واحد فقط‪ ،‬كما أصبحت المجموعات ُمرتَّب‪ً w‬‬
‫‪w‬ة ترتيبً‪ww‬ا تص‪ww‬اعديًا‪.‬‬

‫تَعرِض الصورة التالية مثااًل عن الكلمات المك َّونة من ثالثة أحرف‪.‬‬

‫المرتَّبة‪ ،‬بينما يَعرِض الص‪w‬ف الث‪w‬اني ش‪w‬كل المجموع‪w‬ات بع‪w‬د اجتيازه‪w‬ا أو‬
‫يَعرِض الصف األول الكلمات غير ُ‬
‫التنقل فيها للمرة األوىل‪ .‬تبدأ كلمات كل مجموعة بنفس الحرف‪.‬‬

‫بعد اجتياز الكلمات للمرة الثانية‪ ،‬تبدأ كلمات كل مجموع‪ww‬ة بنفس الح‪ww‬رفين األول‪ww‬يين‪ ،‬وبع‪ww‬د اجتيازه‪ww‬ا للم‪ww‬رة‬

‫الثالثة‪ ،‬سيكون هنالك كلمة واحدة فقط في كل مجموعة‪ ،‬وستكون المجموعات ُمرتَّبة‪.‬‬

‫أثناء كل اجتياز‪ ،‬نمرّ عبر العناصر ونضيفها إىل المجموعات‪ .‬يُع ّد ك‪ww‬ل اجتي‪ww‬از منه‪ww‬ا خط ًي‪ww‬ا طالم‪ww‬ا ك‪ww‬انت تل‪ww‬ك‬

‫المجموعات تَ َ‬
‫سمح بإضافة العناصر إليها بزمن خطي‪.‬‬

‫تعتمد عدد مرات االجتياز أو التنقل ‪-‬التي سنطلق عليها ‪ -w‬عىل عرض الكلمات‪ ،‬ولكن‪ww‬ه ال يعتم‪ww‬د عىل ع‪ww‬دد‬

‫الكلمات ‪ ،n‬وبالتالي‪ ،‬يكون ترتيب النمو )‪ O(wn‬وهو خطي بالنسبة لقيمة ‪.n‬‬

‫تتو َّفر نسخ أخرى من خوارزمية الترتيب بالجذر‪ ،‬ويُمكِن تنفيذ ُكلٍّ منها بطرق كثيرة‪ .‬يُمكِنك قراءة المزي‪ww‬د عن‬

‫خوارزمية الترتيب بالجذر‪ ،‬كما يُمكِنك أن تحاول كتابة تنفيذ لها‪.‬‬

‫‪163‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫‪ 17.5‬خوارزمية الرتتيب بالكومة ‪Heap sort‬‬


‫إىل جانب خوارزمية الترتيب بالجذر التي تُطبَّق عندما يكون حجم األشياء المطل‪ww‬وب ترتيبه‪ww‬ا مق ّي‪ w‬دًا‪ ،‬هنال‪ww‬ك‬

‫المق ّيدة‪ ،‬والتي تُطبَّق عندما نعم‪ww‬ل م‪ww‬ع بيان‪ٍ w‬‬


‫‪w‬ات ض‪ww‬خمةٍ‬ ‫خصصة أخرى هي خوارزمية الترتيب بالكومة ُ‬
‫خوارزمية ُم َّ‬
‫جدًا ونحتاج إىل معرفة أكبر ‪ 10‬أو أكبر عدد ‪ k‬حيث ‪ k‬قيمة أصغر بكثير من ‪.n‬‬

‫ً‬
‫خدمة عبر اإلنترنت تتعامل مع باليين المعامالت يوم ًيا‪ ،‬وأننا نريد في نهاية كل ي‪ww‬وم‬ ‫لنفترض مثاًل أننا نراقب‬

‫معرفة أكبر ‪ k‬من المعامالت (أو أبطأ أو أي معيار آخر)‪ .‬يُمكِننا مثاًل أن نُخزِّن جميع المعامالت‪ ،‬ثم نُرتِّبها في نهاية‬

‫اليوم‪ ،‬ونختار أول ‪ k‬من المعامالت‪ .‬سيستغرق ذلك زمنًا يتناسب م‪w‬ع ‪ ،n log n‬وس‪w‬يكون بطيًئا ج‪w‬دًا ألنن‪w‬ا من‬

‫المحتم‪w‬ل أال نتمكَّن من مالءم‪w‬ة باليين المع‪w‬امالت داخ‪w‬ل ذاك‪w‬رة برن‪w‬امج واح‪w‬د‪ ،‬وبالت‪w‬الي‪ ،‬ق‪w‬د نض‪w‬طرّ الس‪w‬تخدام‬

‫خوارزمية ترتيب بذاكرة خارجية (خارج النواة)‪.‬‬

‫يُمكِننا بداًل من ذلك أن نَستخدِم كومة ُمقيدّة ‪ .heap‬إليك ما سنفعله في ما تبقى من هذا الفصل‪:‬‬

‫‪ .1‬سنشرح خوارزمية الترتيب بالكومة (غير المقيدة)‪.‬‬

‫‪ .2‬ستنفذ الخوارزمية‪.‬‬

‫المقيدة ونُحلِّلها‪.‬‬
‫‪ .3‬سنشرح خوارزمية الترتيب بالكومة ُ‬

‫لكي تفهم ما يعنيه الترتيب بالكومة‪ ،‬عليك أواًل فهم ماهية الكومة‪ .‬الكومة ببس‪ww‬اطة عب‪ww‬ارة عن هيك‪ww‬ل بي‪ww‬اني‬

‫‪ data structure‬مشابه لش‪ww‬جرة البحث الثنائي‪ww‬ة ‪ .binary search tree‬تتلخص الف‪ww‬روق بينهم‪ww‬ا في النق‪ww‬اط‬

‫التالية‪:‬‬

‫تتمتع أي عقدة ‪ x‬بشجرة البحث الثنائية بـ"خاصية ‪ "BST‬أي تكون جميع عقد الشجرة الفرعية ‪subtree‬‬ ‫•‬

‫الموجود عىل يسار العقدة ‪ x‬أصغر من ‪ x‬كما تكون جميع عقد الشجرة الفرعية الموجودة عىل يمينها أكبر‬

‫من ‪.x‬‬

‫تتمتع أي عقدة ‪ x‬ضمن الكومة بـ"خاصية الكومة" أي تكون جميع عقد الش‪ww‬جرتين الفرعي‪ww‬تين للعق‪ww‬دة ‪x‬‬ ‫•‬

‫أكبر من ‪.x‬‬

‫تتشابه الكومة مع أشجار البحث الثنائية المتزنة من جه‪ww‬ة أن‪ww‬ه عن‪ww‬دما تض‪ww‬يف العناص‪ww‬ر إليه‪ww‬ا أو تح‪w‬ذفها‬ ‫•‬

‫منها‪ ،‬فإنها قد تقوم ببعض العمل اإلضافي لضمان استمرارية ات‪ww‬زان الش‪ww‬جرة‪ ،‬وبالت‪ww‬الي‪ ،‬يُمكِن تنفي‪ww‬ذها‬

‫بكفاءة باستخدام مصفوفة من العناصر‪.‬‬

‫دائما ما يكون جذر الكومة هو العنصر األصغر‪ ،‬وبالتالي‪ ،‬يُمكِننا أن نعثر علي‪ww‬ه ب‪ww‬زمن ث‪ww‬ابت‪ .‬تس‪ww‬تغرق إض‪ww‬افة‬
‫ً‬
‫دائما ما تكون متزنة‪ ،‬فإن ‪ h‬يتناس‪ww‬ب‬
‫ً‬ ‫العناصر وحذفها من الكومة زمنًا يتناسب مع طول الشجرة ‪ ،h‬وألن الكومة‬

‫مع ‪ .log n‬يُمكِنك قراءة المزيد عن الكومة لو أردت‪.‬‬

‫‪164‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫تُن ِّفذ جاف‪ww‬ا الص‪ww‬نف ‪ PriorityQueue‬باس‪ww‬تخدام كوم‪ww‬ة‪ .‬يحت‪ww‬وي ذل‪ww‬ك الص‪ww‬نف عىل التواب‪ww‬ع ُ‬
‫المعرَّف‪ww‬ة في‬

‫الواجهة ‪ Queue‬ومن بينها التابعان ‪ offer‬و ‪ poll‬اللّذان نلخص عملهما فيما يلي‪:‬‬

‫‪w‬من اس‪ww‬تيفاء "خاص‪ww‬ية الكوم‪ww‬ة"‬


‫‪ :offer .1‬يضيف عنصرًا إىل الرتل ‪ ،queue‬ويُح‪w‬دِّث الكوم‪ww‬ة بحيث يَض‪َ w‬‬
‫لجميع العقد‪ .‬الحِ ظ أنه يستغرق زمنًا يتناسب مع ‪.log n‬‬

‫‪ :poll .2‬يَح ِذف أصغر عنصر من الرتل من الجذر ويُحدِّث الكومة‪ .‬يستغرق ً‬
‫أيضا زمنًا يتناسب مع ‪.log n‬‬

‫إذا كان لديك كائن من النوع ‪ ،PriorityQueue‬تستطيع بس‪ww‬هولة ت‪ww‬رتيب تجميع‪ww‬ة عناص‪ww‬ر طوله‪ww‬ا ‪ n‬عىل‬

‫النحو التالي‪:‬‬

‫‪ .1‬أضف جميع عناصر التجميعة إىل كائن الصنف ‪ PriorityQueue‬باستخدام التابع ‪.offer‬‬

‫‪ .2‬احذف العناصر من الرتل باستخدام التابع ‪ poll‬وأضفها إىل قائمة من النوع ‪.List‬‬

‫تبق في الرتل‪ ،‬فإن العناص‪ww‬ر تُض‪ww‬اف إىل القائم‪ww‬ة ُمرتَّب‪ً w‬‬


‫‪w‬ة تص‪ww‬اعديًّا‪.‬‬ ‫نظرًا ألن التابع ‪ poll‬يعيد أصغر عنصر ُم ٍّ‬

‫يُطلَق عىل هذا النوع من الترتيب اسم الترتيب بالكومة‪.‬‬

‫رتل زمنًا يتناسب مع ‪ ،n log n‬ونفس األمر ينطبق عىل حذف‬


‫تستغرق إضافة عد ٍد قدره ‪ n‬من العناصر إىل ٍ‬
‫عد ٍد قدره ‪ n‬من العناصر منه‪ ،‬وبالتالي‪ ،‬تنتمي خوارزمية الترتيب بالكومة إىل المجموعة ))‪.O(n log(n‬‬

‫ستجد ضمن الملف ‪ ListSorter.java‬تعري ًفا مبدئ ًيا لت‪ww‬ابع اس‪ww‬مه ‪ .heapSort‬أكمل‪ww‬ه ون ِّفذ األم‪ww‬ر ‪ant‬‬

‫‪ ListSorterTest‬لكي تتأ ّكد من أنه يَ َ‬


‫عمل بشكل صحيح‪.‬‬

‫المقي ّ‬
‫دة ‪Bounded heap‬‬ ‫‪ 17.6‬الكومة ُ‬

‫تَ َ‬
‫عمل الكومة المقيدة كأي كومة عادية‪ ،‬ولكنها تكون مقيدة بعدد ‪ k‬من العناص‪ww‬ر‪ .‬إذا ك‪ww‬ان ل‪ww‬ديك ع‪ww‬دد ‪ n‬من‬

‫العناصر‪ ،‬يُمكِنك أن تحتفظ فقط بأكبر عدد ‪ k‬من العناصر باتباع التالي‪:‬‬

‫ستكون الكومة فارغة مبدئ ًيا‪ ،‬وعليك أن تُن ِّفذ التالي لكل عنصر ‪:x‬‬

‫ً‬
‫ممتلئة‪ ،‬أضف ‪ x‬إىل الكومة‪.‬‬ ‫التفريع األول‪ :‬إذا لم تكن الكومة‬ ‫•‬

‫ً‬
‫ممتلئة‪ ،‬وازن قيمة ‪ x‬م‪ww‬ع أص‪ww‬غر عنص‪ww‬ر في الكوم‪ww‬ة‪ .‬إذا ك‪ww‬انت قيم‪ww‬ة ‪x‬‬ ‫التفريع الثاني‪ :‬إذا كانت الكومة‬ ‫•‬

‫أصغر‪ ،‬فال يُمكِن أن تكون ضمن أكبر عدد ‪ k‬من العناصر‪ ،‬ولذلك‪ ،‬يُمكِنك أن تتجاهلها‪.‬‬

‫ً‬
‫ممتلئة‪ ،‬وكانت قيم‪w‬ة ‪ x‬أك‪w‬بر من قيم‪w‬ة أص‪w‬غر عنص‪w‬ر بالكوم‪w‬ة‪ ،‬اح‪w‬ذف‬ ‫التفريع الثالث‪ :‬إذا كانت الكومة‬ ‫•‬

‫أصغر عنصر‪ ،‬وأضف ‪ x‬مكانه‪.‬‬

‫بوجود أصغر عنصر أعىل الكومة‪ ،‬يُمكِننا االحتفاظ بأكبر عدد ‪ k‬من العناصر‪ .‬لنحلل اآلن أداء هذه الخوارزمي‪ww‬ة‪.‬‬

‫إننا نن ِّفذ ما يلي لكل عنصر‪:‬‬

‫‪165‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫التفريع األول‪ :‬تستغرق إضافة عنصر إىل الكومة زمنًا يتناسب مع )‪.O(log k‬‬ ‫•‬

‫التفريع الثاني‪ :‬يستغرق العثور عىل أصغر عنصر بالكومة زمنًا يتناسب مع )‪.O(1‬‬ ‫•‬

‫التفريع الثالث‪ :‬يستغرق حذف أصغر عنصر زمنًا يتناسب مع )‪ ،O(log k‬كما أن إضافة ‪ x‬تستغرق نفس‬ ‫•‬

‫مقدار الزمن‪.‬‬

‫دائما‪ ،‬ويكون الزمن اإلجم‪ww‬الي‬


‫ً‬ ‫في الحالة األسوأ‪ ،‬تكون العناصر ُمرتَّبة تصاعديًا‪ ،‬وبالتالي‪ ،‬نُن ِّفذ التفريع الثالث‬

‫ي مع ‪.n‬‬
‫خط ّ‬
‫لمعالجة عدد ‪ n‬من العناصر هو )‪ O(n log K‬أي ّ‬

‫ستجد في الملف ‪ ListSorter.java‬تعري ًفا مبدئ ًيا لتابع اس‪ww‬مه ‪ .topK‬يَس‪ww‬تق ِبل ه‪ww‬ذا الت‪ww‬ابع قائم‪ً w‬‬
‫‪w‬ة من‬

‫النوع ‪ List‬وكائنًا من النوع ‪ Comparator‬وع‪w‬ددًا ص‪w‬حيحًا ‪ ،k‬ويعي‪w‬د أك‪w‬بر ع‪w‬دد ‪ k‬من عناص‪w‬ر القائم‪w‬ة ب‪w‬ترتيب‬

‫‪ ant‬لكي تتأ ّكد من أنه يَ َ‬


‫عمل بشكل صحيح‪.‬‬ ‫تصاعدي‪ .‬أكمل متن التابع ثم ن ِّفذ األمر ‪ListSorterTest‬‬

‫‪ 17.7‬تعقيد المساحة ‪Space complexity‬‬


‫تح‪ww‬دثنا ح‪ww‬تى اآلن عن تحلي‪ww‬ل زمن التنفي‪ww‬ذ فق‪ww‬ط‪ ،‬ولكن بالنس‪ww‬بة لكث‪ww‬ير من الخوارزمي‪ww‬ات‪ ،‬ينبغي أن ن ُ‪ww‬ولِي‬

‫للمساحة التي تتطلّبها الخوارزمية بعض االهتمام‪ .‬عىل سبيل المثال‪ ،‬تحتاج خوارزمي‪ww‬ة ال‪ww‬ترتيب بال‪ww‬دمج ‪merge‬‬

‫‪ sort‬إىل إنشاء نسخ من البيانات‪ ،‬وقد كانت مساحة الذاكرة اإلجمالية التي تطلّبها تنفيذنا لتل‪ww‬ك الخوارزمي‪ww‬ة ه‪ww‬و‬

‫)‪ .O(n log n‬في الواقع‪ ،‬يُمكِننا أن نُخ ِّفض ذلك إىل )‪ O(n‬إذا نفذنا نفس الخوارزمية بطريقة أفضل‪.‬‬

‫في المقابل‪ ،‬ال تنسخ خوارزمية الترتيب باإلدراج ‪ insertion sort‬البيانات ألنها تُرتِّب العناصر في أماكنه‪ww‬ا‪،‬‬
‫ً‬
‫مؤقتة لموازنة عنصرين في كل مرة‪ ،‬كما تَستخدِم ع‪ww‬ددًا قلياًل من المتغ‪ww‬يرات المحلي‪ww‬ة ‪local‬‬ ‫متغيرات‬
‫ٍ‬ ‫وتَستخدِم‬

‫األخرى‪ ،‬ولكن المساحة التي تتطلبها ال تعتمد عىل ‪.n‬‬

‫نش ‪w‬ئ نس‪ww‬ختنا من خوارزمي‪ww‬ة ال‪ww‬ترتيب بالكوم‪ww‬ة كائنً‪ww‬ا جدي ‪w‬دًا من الن‪ww‬وع ‪ ،PriorityQueue‬وتُخ ‪w‬زِّن في‪ww‬ه‬
‫تُ ِ‬

‫العناصر‪ ،‬أي أن المساحة المطلوب‪w‬ة تنتمي إىل المجموع‪w‬ة )‪ ،O(n‬وإذا س‪w‬محنا ب‪w‬ترتيب عناص‪w‬ر القائم‪w‬ة في نفس‬

‫المكان‪ ،‬يُمكِننا أن نُخ ِّفض المساحة المطلوبة لتنفيذ خوارزمية الترتيب بالكومة إىل المجموعة )‪.O(1‬‬

‫من مميزات النسخة التي ن َّفذناها من تلك الخوارزمية هي أنها تحتاج فقط إىل مس‪ww‬احة تتناس‪ww‬ب م‪ww‬ع ‪( k‬أي‬
‫ً‬
‫وعادة ما تكون ‪ k‬أصغر بكثير من قيمة ‪.n‬‬ ‫عدد العناصر المطلوب االحتفاظ بها)‪،‬‬

‫يميل مطورو البرمجيات إىل التركيز عىل زمن التشغيل وإهمال حيز ال‪ww‬ذاكرة المطل‪ww‬وب‪ ،‬وه‪ww‬ذا في الحقيق‪ww‬ة‪،‬‬

‫مناسب لكثير من التطبيقات‪ ،‬ولكن عند التعامل مع بيانات ضخمة‪ ،‬تكون المس‪ww‬احة المطلوب‪ww‬ة بنفس الق‪ww‬در من‬

‫األهمية إن لم تكن أهم‪ ،‬كما في الحاالت التالية مثاًل ‪:‬‬

‫عم‪ w‬ل‬ ‫ً‬


‫مالئمة للبيانات‪ ،‬عاد ًة ما يزداد زمن التشغيل إىل حد كبير وقد ال يَ َ‬ ‫‪ .1‬إذا لم تكن مساحة ذاكرة البرنامج‬

‫البرنامج من األساس‪ .‬إذا اخترت خوارزمية تتطلّب حيزًا أقل من الذاكرة‪ ،‬وتَ َ‬
‫سمح بمالئمة المعالجة ضمن‬

‫‪166‬‬
‫هياكل البيانات للمبرمجين‬ ‫الترتيب ‪Sorting‬‬

‫عمل البرنامج بسرعةٍ أعىل بكثير‪ .‬باإلضافة إىل ذلك‪ ،‬تَستغِ ل البرامج ال‪ww‬تي تتطلَّب مس‪ww‬احة‬
‫الذاكرة‪ ،‬فقد يَ َ‬

‫ذاكرة أقل الذاكرة المؤقتة لوحدة المعالجة المركزية ‪ CPU caches‬بشكل أفضل وتَ َ‬
‫عمل بسرعة أكبر‪.‬‬

‫‪ .2‬في الخوادم التي تُش ِّغل برامج كثيرة في الوقت نفس‪ww‬ه‪ ،‬إذا أمكن‪ww‬ك تقلي‪ww‬ل المس‪ww‬احة ال‪ww‬تي يتطلبه‪ww‬ا ك‪ww‬ل‬

‫برنامج‪ ،‬فق‪ww‬د تتمكّن من تش‪ww‬غيل ب‪ww‬رامج أك‪ww‬ثر عىل نفس الخ‪ww‬ادم‪ ،‬مم‪ww‬ا يُقل‪ww‬ل من تكلف‪ww‬ة الطاق‪ww‬ة والعت‪ww‬اد‬

‫المطلوبة‪.‬‬

‫كانت هذه بعض األسباب التي توضح أهمية االطالع عىل متطلبات الخوارزميات المتعلقة بالذاكرة‪.‬‬

‫يمكنك التوسع أكثر في الموضوع بقراءة توثيق خوارزميات الترتيب في توثيق موسوعة حسوب‪.‬‬

‫‪167‬‬
‫أحدث إصدارات أكاديمية حسوب‬

You might also like