Professional Documents
Culture Documents
هياكل البيانات للمبرمجين أكاديمية حسوب
هياكل البيانات للمبرمجين أكاديمية حسوب
مرجع عملي إىل هياكل البيانات والخوارزميات يحتاج إليه كل مهندس برمجيات
Book Title: Think Data Structures اسم الكتاب :هياكل البيانات للمبرمجين
Editor: Jamil Bailony - Mostafa Almahmoud المحرر :جميل بيلوني – مصطفى المحمود
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).
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 تمهيد
14 المساهمون
15 المساهمة
7
هياكل البيانات للمبرمجين جدول المحتويات
8
هياكل البيانات للمبرمجين جدول المحتويات
9
هياكل البيانات للمبرمجين جدول المحتويات
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ة
نهج "من أسفل ألعىل" مفرط :تهتم كثير من كتب هياكل البيانات بطريقة عمل هياكل البيانwwات (أي •
.)interfacesيَت ِبwwع هwwذا الكتwwاب أسwwلوبًا مختل ًفwwا ،حيث يعتمwwد عىل نهج "من أعىل ألسwwفل" ،فيبwwدأ
بالواجهات ،وبالتالي يتمكَّن القراء من تعلُّم كيفية استخدام الهياكل المتاحة بإطار عمل جافا للتجميعwwات
أخيرًا ،تُقدِّم بعض الكتب هذه المادة العلمية بدون سياق واضح وبدون أي حwwافز ،فتَعwwرِض الهياكwwل البيانيwwة
ً
واحدة تلو األخرى .يحاول هذا الكتاب تنظيم الموضوعات نوعً wا مwا من خالل التركwwيز عىل تطwبيق ُمحwدّد -محwرك
بحث ،-ويَستخ ِد م هذا التطبيق هياكل البيانات بشكل مكثف ،وهو في الواقع موضوع مهم وشيق بحد ذاته.
في الحقيقة ،سيدفعنا هذا التطبيق إىل دراسة بعض الموضوعات التي ربما لن تتعرَّض لها ببعض الفصول
الدراسwwية التمهيديwwة الخاصwwة بمwwادة هياكwwل البيانwwات ،حيث سwwنتعرَّض هنwwا مثاًل ،لحفwwظ هياكwwل البيانwwات
الموضوعات ،سيطرح الكاتب المادة العلمية طرحً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
هياكل البيانات للمبرمجين تمهيد
) :Sierra and Bates, Head First Java (O’Reilly Media, 2005مناسب للقراء الذين لديهم اطالع •
ً
مألوفwwة بالنسwwبة لwwك ،ف ُيمكِنwwك قwwراءة درس مwwا المقصwwود إذا لم تكن الواجهwwات interfacesبلغwwة جافwwا
بالواجهة؟.
ً
مربكة بعض الشيء .تشير تلك الكلمة ضمن عبارة واجهة تطوير التطبيقات قد تكون كلمة واجهة interface
إمكانيات محددة.
عالو ًة عىل ذلك ،تشير تلك الكلمة بلغة جافا إىل خاصية ضمن اللغة .تُشبه تلك الخاصية األصناف وتُ ِّ
خصص
مجموعة من التوابع ،ولكي نتجنَّب الخلط بينهما ،سنَستخ ِدم كلمة واجهة بنمط الخط العادي لإلشwwارة إىل الفكwwرة
العامة للواجهة ،بينما سنَستخدِم كلمة interfaceبنمط خط الشيفرة لإلشارة إىل تلك الخاصية.
باإلضافة إىل ما سبق ،ينبغي أن تكwwون عىل علم بكwwلّ من معwwامالت األنwwواع type parametersواألنwwواع
من األفضل لو كنت قد سمعت عن أداة ،Apache Antوهي أداة بناء أوتوماتيكية للغة جافا ،كما ينبغي أن
: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اب •
ممت أمثل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ك لنسwتفيد منهمwا عمل ًّيا ونجعwwل التمwwارين
ٍ بحث
ٍ والخوارزميات إلنشاء محرك
سنبدأ بالواجهة ،Listوسنكتب صنفين ينفذ كلٌ منهما تلك الواجهة بطريقة مختلفwwة ،ثم سwwنوازن بين •
بعد ذلك ،سنقدِّم هياكل بيانات شجريّة الشكل ،ونبدأ بكتابwwة شwwيفرة التطwwبيق األول .حيث سwwيقرأ هwwذا •
صفحات من موقwwع ،Wikipediaثم يُحلِّل محتوياتهwwا ويعطي النتيجwwة عىل هيئwwة شwwجرة ،وفي
ٍ التطبيق
الشهيرة "الطريق إىل الفلسفة" الذي يمكنك معرفة الفكرة العامة عنwwه بقwwراءة المقwwال باللغwwة اإلنجليزيwwة
.Getting to Philosophy
هياكل البيانات للمبرمجين الواجهات Interfaces
المن ِّفذ لهwwا ،ثم سwwنكتب أصwwنا ًفا تُن ِّفذ تلwwك الواجهwwة
سwwنتطرق للواجهwwة Mapوصwwنف جافwwا ُ HashMap •
أخيرًا ،سنستخ ِد م تلك األصناف وبعض األصناف األخرى التي سنتناولها عبر الكتاب لتنفيذ محرك بحث •
عبر اإلنترنت .سwwيكون هwwذا المحwwرك بمنزلwwة زاحwwف crawlerيبحث عن الصwwفحات ويقرؤهwwا ،كمwwا أنwwه
س ُيفهرِس ويُخزِّن محتويات صفحات اإلنترنت بهيئةٍ تُمكِّنه من إجراء عملية البحث فيها بكفاءة ،كما أنwwه
ولنبدأ اآلن.
ArrayListأم .LinkedListفلماذا تُو ِّفر جافا تنفيذين implementationsللواجهwwة List؟ وكيwwف ينبغي
ً
مشابهة للصنفين ArrayListو ،LinkedListلكي نتمكَّن من سنن ِّفذ في التمارين القليلة األوىل أصنا ًفا
أقwwل عنwwد اسwwتخدام الصwwنف ،ArrayListوبعضwwها اآلخwwر يكwwون أسwwر ع وأصwwغر عنwwد اسwwتخدام الصwwنف
17
هياكل البيانات للمبرمجين الواجهات Interfaces
ً
قيمة من النوع .int يُو ِّفر تابعً ا اسمه compareToيَستق ِبل كائنًا كمعامل parameterويعيد •
مثال عىل ما نقول ،انظر إىل الشيفرة المصدرية للصنف java.lang.Integerفيما يلي:
ٍ وفي
الحِ ظ أنّ تنفيذ التابع 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 صنف يُن ِّفذ تلك الواجهة
ٍ فال ب ُ ّد أن يُو ِّفر أي
يو ّفر كال الصنفين ArrayListو LinkedListتلك التوابع ،وبالتالي يُمكِن التبwwديل بينهمwwا .ويَعنِي ذلwwك
18
هياكل البيانات للمبرمجين الواجهات Interfaces
يُ ِّ
وضح المثال التالي تلك الفكرة:
{ )(public ListClientExample
;)(list = new LinkedList
}
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ا
في أ ٍّ
تعديالت أخرى.
ٍ ستحتاج إليه هو تعديل الباني دون الحاجة إلجراء أي
المعلومات عنها ،يمكنك قراءة المقال البرمجة المعتمدة عىل الواجهات المتاح بنسwخته اإلنجليزيwة للتعwرف أكwثر
19
هياكل البيانات للمبرمجين الواجهات Interfaces
عىل هwwذا األسwwلوب .تجwwدر اإلشwwارة هنwwا إىل أنّ الكالم هنwwا عن الواجهwwات بمفهومهwwا العwwام وليس مقتص wرًا عىل
في أسلوب البرمجة المعتمدة عىل الواجهات ،تعتمد الشيفرة المكتوبة عىل الواجهات فقط مثwwل ،Listوال
تنفيذات مع ّينةٍ لتلك الواجهات ،مثل .ArrayListوبهذا ،ستعمل الشwwيفرة حwwتى لwwو تغ ّيwwر التنفيwwذ
ٍ تعتمد عىل
السبب يتجنَّب مطورو المكتبات تعديل الواجهات إال عند الضرورة القصوى.
1.4تمرين 1
البرمجة إىل واجهة ،ولذا فإنك لن تحتاج لتعديل أكثر من سطر ٍ واح ٍد فقط وإضافة تعليمة .import
شيفرات جافا وتشغيلها لكي تتمكَّن من حل التمارين .وقد ُطو َّرت أمثلة هذا الكتاب باس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من مستودع شيفرات الكتاب:
ست ِ
:libيحتوي عىل المكتبات الالزمة لتشغيل األمثلة (مكتبة JUnitفقط في هذا التمرين). •
بهذا التمرين:
20
هياكل البيانات للمبرمجين الواجهات Interfaces
.ListClientExample
راجع الصنف ListClientExampleوبعد أن تتأ َّكد أنwك فهمت كيwف يعمwل ،صwرِّفه وشّ wغله؛ وإذا كنت
تستخدم أداة ،Antفاذهب إىل مجلد codeون ِّفذ األمر .ant ListClientExample
سبب ظهور هذا التحذير هو أننا لم نُحدّد نوع عناصر القائمة ،وقwwد فعلنwwا ذلwwك بهwwدف تبسwwيط المثwwال ،لكن
يُمكِن حwwwwل إشwwwwكال ّية هwwwwذا التحwwwwذير بتعwwwwديل كwwwwل Listأو LinkedListإىل > List<Integerأو
،ListClientExampleويَستدعِ ي تابعه الجالب ،getListثم يَفحَص ما إذا كانت القيمة المعwwادة منwwه هي
ً
قيمة من النwwوع LinkedList َ
سيفشل هذا االختبار في البداية ألن التابع سيعيد كائن من النوع .ArrayList
فاالختبارات الجيدة ينبغي أن تتأ َّكد من تلبية الصنف الذي يجري اختباره لمتطلبات الواجهة ،ال أن تكون هذه
ً
مبنية عىل تفاصيل التنفيذ. االختبارات
،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ه
21
.2تحليل الخوارزميات
كما رأينا في الفصل السابق ،تُو ِّفر جافwwا تنفيwwذين implementationsللواجهwwة ،Listهمwwا ArrayList
و ،LinkedListحيث يكwwون النwwوع LinkedListأسwwر ع بالنسwwبة لبعض التطبيقwwات ،بينمwwا يكwwون النwwوع
لتطبيقات أخرى.
ٍ ArrayListأسر ع بالنسبة
وإذا أردنا أن نُحدِّد أيهما أفضل لالستخدام في تطبيق معين ،فيمكننا تجربة كلٍّ منهما عىل حد ٍة لwنرى الwزمن
الذي س َيستغرِقه .يُطلَق عىل هذا األسلوب اسم التشخيص ،profilingولكنّ له بعض اإلشكال ّيات:
الم ْدخَلة.
.3قد تعتمد النتائج عىل حجم المشكلة أو البيانات ُ
المشكلةِ باالستعانة بما يُعرَف باسم تحليل الخوارزميات ،الwwذي يُمكِّننwwا من
يُمكِننا معالجة بعض هذه النقاط ُ
سنضطر عندئ ٍذ لوضع بعض االفتراضات:
ّ خوارزميات دون الحاجة إىل تنفيذها فعل ًيا ،ولكننا
ٍ الموازنة بين عدة
.1فلكي نتجنَّب التفاصيل المتعلقة بعتاد الحاسوب ،سwنُحدِّد العمليwwات األساسwية الwتي تتwألف منه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ا أن
ذات زمن ثابت :تكون الخوارزمية ثابتwwة الwwزمن إذا لم يعتمwwد زمن تشwwغيلها عىل حجم المwwدخالت .عىل •
ّ
بغض النظر عن حجم المصفوفة. من عناصرها ،فإن ذلك يتطلَّب نفس عدد العمليات
خطي :تكون الخوارزمية خط ّي ًة إذا تناسب زمن تشغيلها مع حجم المدخالت .فإذا كنا نحسب
ذات زمن ّ •
حاصل مجموع العناصر الموجودة ضمن مصفوفة مثاًل ،فعلينwا أن نسwترجع قيمwة عwدد nمن العناصwر،
ذات زمن تربيعي :تكون الخوارزمية تربيعية أو من الدرجة الثانية إذا تناسب زمن تشغيلها مwwع .n2عىل •
الكلي لعمليwwات
ّ العناصر ،والتي ال ب ُ ّد من موازنة ُكلٍّ منها مع عدد n-1من العناصر األخرى ،يكون العwwدد
**/
بدل العنصرين الموجودين بالفهرس 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ع
جميع العمليات ضمن التابع 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
يُمكِننا الوصول إىل نفس النتيجة بطريقة أخرى ،وهي أن ننظر للتابع indexLowestكما لو كان حلقة تكرارٍ
ً
مجموعة من العمليwwات يكwwون ً
متداخلة ،nestedففي كل مرة نَستدعِ ي خاللها التابع ،indexLowestفإنه يُن ِّفذ
مع .n2
تنتمي جميwwع الخوارزميwwات الخط ّيwwة -الwwتي تسwwتغرِق زمنًwwا خط ًيwwا -إىل المجموعwwة ) ،O(nبينمwwا تنتمي جميwwع
الخوارزميwwات التربيعيwwة إىل المجموعwwة ) .O(n2تطلَwwق عىل تصwwنيف الخوارزميwwات بهwwذا األسwwلوب تسwwمية
25
هياكل البيانات للمبرمجين تحليل الخوارزميات
ً
خوارزمية يُو ِّفر هذا الترميز أسلوبًا سهاًل لكتابة القواعد العامة التي تسلُكها الخوارزميات في العموم .فلو ن َّفذنا
ً
خطية وتبعناها بخوارزميةٍ ثابتة الزمن عىل سبيل المثال ، ،فإن زمن التشغيل اإلجمالي يكون خط ًيا .وننبّه هنا إىل
ً
تربيعية: ً
خطية عدد nمن المراتts ،تكون النتيجة ً
عملية في المقابل ،إذا أجرينا
مجموعwwة الخوارزميwwات الwwتي ينتمي زمن تشwwغيلها إىل نفس تصwwنيف ترمwwيز ،big Oحيث تنتمي جميwwع
الخوارزميات الخطية مثاًل إىل نفس ترتيب النمو؛ وذلك ألن زمن تشغيلها ينتمي إىل المجموعة ).O(n
قص د بكلمة "ترتيب" ضمن هذا السياق "مجموعة" ،مثل اِستخدَامنا لتلك الكلمة في عبار ٍة مثل "تwwرتيب
ويُ َ
قصد بهذا أنهم مجموعة من الفرسان ،وليس طريقة ص ّفهم أو ترتيبهم ،أي يُمكِنwwك
فرسان المائدة المستديرة" .ويُ َ
أن تنظر إىل ترتيب الخوارزميات الخطية وكأنها مجموعة من الخوارزميات التي تتمتّع بكفاء ٍة عالية.
2.3تمرين 2
يشتمل التمرين التالي عىل تنفيذ الواجهة Listباستخدام مصفوفةٍ لتخزين عناصر القائمة.
ستجد الملفات التالية في مستودع الشيفرة الخاص بالكتاب -انظر القسم :-0.1
26
هياكل البيانات للمبرمجين تحليل الخوارزميات
ُ
أربعة توابعَ غير مكتملة عليwwك : MyArrayList.javaيحتوي عىل تنفيذ جزئي للواجهة ،Listفهناك •
كما سwwتجد الملwwف .build.xmlيُمكِنwwك أن تُن ِّفذ األمwwر ant MyArrayList؛ لكي تتمكَّن من تشwwغيل
اختبwارات بسwيطة.
ٍ الصwنف MyArrayList.javaوأنت مwا تwزال في المجلwد codeالwذي يحتwوي عىل عwدة
اختبارات .JUnit
ِ ويُمكِنك بداًل من ذلك أن تُن ِّفذ األمر ant MyArrayListTestلكي تُش ِّغل
عندما تُش ِّغل تلك االختبارات فسيفشل بعضها ،والسبب هو وجود توابع ينبغي عليك إكمالها .إذا نظرت إىل
ً
سريعة عىل بعض أجزاء الشwwيفرة .تحتwwوي الشwwيفرة ولكن قبل أن تبدأ في إكمال تلك التوابع ،دعنا نلق نظر ًة
{ )(public MyArrayList
;]array = (E[]) new Object[10
;size = 0
}
}
بينما يُمثِل المتغير arrayالمصفوفة التي تحتوي عىل تلك العناصر ذاتها.
مصفوفة مك َّو ً
نة من عشرة عناصر تَحمِ ل مبدئ ًّيا القيمة الفارغة ،nullكما يَضبُط قيمة المتغير ً نشئ الباني
يُ ِ
sizeإىل .0غالبًا ما يكون طول المصفوفة أكبر من قيمة المتغير ،sizeمما يَعنِي وجود أماكنَ غير ُمسwwتخدَمةٍ
في المصفوفة.
ال ي ُمكِنك إنشاء مصفوفة باستخدام معامل نوع ،type parameterوهكذا فالتعليمة التالية مثاًل لن تَ َ
عمل:
27
هياكل البيانات للمبرمجين تحليل الخوارزميات
ً
مصwwفوفة من النwwوع ،Objectثم تُحِّ wwول نوعهwwا لكي تتمكَّن من تخطِ ي تلwwك العقبwwة ،عليwwك أن تُ ِ
نش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ا.
ّ التابع زمنًا ثابتًا ،ولكنه في الحاالت التي
بسيط للغاية ويعمل كم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
المستخدَمة.
وبالتالي ال يعيد التابع قيم عناصر المصفوفة غير ُ
اقwwرأ توثيwwق setباللغwwة اإلنجليزية ،ثم أكمwwل متن التwwابع .ال ب ُ ّ wد أن ينجح االختبwwار testSetعنwwدما تُش ِّ wغل
ً MyArrayListTest
مرة أخرى.
الخطوة التالية هي إكمال التابع ،indexOfوبالمثل نحيلك إىل مقالة توثيق التwwابع List indexOfلتقرأهwwا
أواًل وذلك لتعرف ما ينبغي عليك القيام به .وأعِ ر انتباهًا لكيفية معالجته للقيمة الفارغة .null
و َّفرنا لك ً
أيضا التابع المساعد equalsللموازنة بين قيمة عنصر ضمن المصفوفة وبين قيمة معينة أخwwرى.
يعيد ذلك التابع القيمة trueإذا كانت القيمتان متساويتين كما يُعالِج القيمة الفارغة nullبشكل سwwليم .الحِ wظ
شِّ wwwwغل االختبwwwwار MyArrayListTestمwwwwرة أخwwwwرى عنwwwwدما تنتهي ،واآلن ينبغي أن ينجح االختبwwwwار
ما يزال هناك تابعwwان آخwwران عليwwك إكمالهمwwا لكي تنتهي من التمwwرين ،حيث أن التwwابع األول هwwو عبwwارة عن
مثلما سبق ،اقرأ التوثيق باللغwwة اإلنجليزية أواًل ثم ن ِّفذ التwwابع ،بعwدها شِّ wغل االختبwwارات لكي تتأ ّكwwد من أنwwك
تنفيذك سليم.
لننتقwwwل اآلن إىل التwwwابع األخwwwير :أكمwwwل متن التwwwابع .removeاقwwwرأ أواًل التوثيwwwق باللغwwwة اإلنجليزية
29
.3قائمة المصفوفة ArrayList
يضرب هذا الفصل عصفورين بحجر ٍ واحدٍ ،حيث سwwنحل فيwwه تمwwرين الفصwwل السwwابق ،وسwwنتطرق لوسwwيلة
تستغرِق كل تعليمة من تعليمات التابع getزمنًا ثابتًا ،وبنwwا ًء عىل ذلwwك يسwwتغرِق التwwابع getفي المجمwwل
زمنًا ثابتًا.
اآلن وقد صنّفنا التابع ،getيمكننا بنفس الطريقة أن نصنّف التwابع setالwذي يَسwتخدِمه .انظwر إىل تنفيwذ
;return old
}
ً
صراحة ،فهو يعتمwwد في ذلwwك عىل اسwwتدعائه للتwwابع ربما الحظت أن التابع setال يفحص نطاق المصفوفة
تَستغرق كل تعليمة من تعليمات التابع - setبما في ذلك استدعاؤه للتابع -getزمنًا ثابتًا ،وعليه يُع ّد التابع
setثابت الزمن ً
أيضا.
ولننتقل اآلن إىل بعض التوابع الخط ّية .انظر مثال ً إىل تنفيذنا للتابع :indexOf
ذلك التابع:
يستدعي الت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ر ،ومن ثم
وهكذا يتشابه تحليل التابع removeمع التابع السابق .وفيما يلي تنفيذه:
ً
بداي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ون
خاللها العنصر المطلوب حذفه واقعً ا في نهاية المصفوفة أو عىل بعد مسافةٍ ثابتةٍ من نهايتها.
32
هياكل البيانات للمبرمجين قائمة المصفوفة ArrayList
َ
النسخة ذات المعامwwل الواحwwد ) add(Eأواًل لكي تضwwع ُ
النسخة ذات المعاملين )add(int, E تستدعي
العنصر الجديد في نهاية المصفوفة ،وبعد ذلك تُحwرِّك العناصwwر األخwwرى إىل اليمين ،وتضwwع العنصwwر الجديwwد في
المكان الصحيح.
سwwwنُحلّل أواًل زمن النسwwwخة ذات المعامwwwل الواحwwwد ) add(Eقبwwwل أن ننتقwwwل إىل تحليwwwل النسwwwخة ذات
تتضح لنا هنا صعوبة تحليل زمن النسخة ذات المعامل الواحد؛ ألنه لو كانت هناك مس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ان •
َ
سبعة عناصر وهكذا دواليك. ستُخزِّن سبعُ المرات التالية •
ً
إضافة ،سنكون قد خزَّنا 16عنص ًرا ونسخنا 14عنص ًرا. بعد 16 •
يُفترَض أن تكون قد استقرأت سير العملية وحصلت عىل ما يلي :لكي نُضيف عدد مقwwداره nمن العناصwwر،
سنضطرّ إىل تخزين عدد nمن العناصر ونسخ عدد n-2من العناصر ،وبالتالي يكون عدد العمليات اإلجمwwالي هwwو
n+n-2أي .2n-2
عدد اإلضافات ،nوبذلك ستكون النتيجة هي .2-2/nالحِ ظ أنه كلما ازدادت قيمة ،nستقل قيمwwة الجwwزء الثwwاني
قد يبدو من الغريب لخوارزمية تحتاج إىل زمن خطي أحيانًا أن تكون ثابتة الزمن في المتوس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
34
هياكل البيانات للمبرمجين قائمة المصفوفة ArrayList
الحلقwwة زمنًwwا خط ًيwwا باسwwتثناء الحالwwة الwwتي نضwwيف خاللهwwا عنصwwرًا إىل نهايwwة المصwwفوفة ،وعليwwه يكwwون
3.3حجم المشكلة
ولننتقwwل اآلن إىل المثwwال األخwwير في هwwذا الفصwwل .انظwwر فيمwwا يلي إىل تنفيwwذ التwwابع removeAllضwwمن
الصنف :MyArrayList
يستدعِ ي التابع 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ا .عىل
زمنًا خط ًيا؛ أما إذا كان collectionيحتوي في العموم عىل %1من عناصر القائمة ،فwwإن التwwابع removeAll
عند الحديث عن حجم المشكلة ،ينبغي أن ننتبه إىل ماهية الحجم أو األحجام التي نحن بصwwددها .يwwبين هwwذا
المثال إحدى مشاكل تحليل الخوارزميات ،وهي االختصار المغري الناجم عن ع ّد الحلقات ،ففي حالة وجود حلقwwة
واحدة ،غالبًا ما تكون الخوارزمية خطية ،وفي حالة وجود حلقتين متداخلتين ،فغالبًا ما تكون الخوارزميwwة تربيعيwwة،
ولكن انتبه وفكر أواًل في عدد مرات تنفيذ كل حلقة ،فإذا كان عددها يتناسب مwwع nلجميwwع الحلقwwات ،فيمكنwwك
35
هياكل البيانات للمبرمجين قائمة المصفوفة ArrayList
لتخزين العناصر ،وإذا لم تكن لديك فكرة عن القوائم المترابطة ،ف ُيمكِنك القراءة عنها في مقال القوائم المترابطة،
ً
عwادة اسwwم عُقwد ،nodesحيث تحتwوي مترابطا إذا كان ُمؤل ًفا من كائنwwات يُطلَwق عليهwا
ً يُع ّد هيكل البيانات
العقد عىل مراجع referencesتشير إىل عقد أخرى .وفي القwwوائم المترابطwwة ،تحتwwوي كwwل عقwwدة عىل مرجwwع إىل
{ )(public ListNode
;this.data = null
;this.next = null
}
36
هياكل البيانات للمبرمجين قائمة المصفوفة ArrayList
ً
مجموع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ات
;node1.next = node2
;node2.next = node3
;node3.next = null
نشئ عقد ًة وتربطها في نفس الوقت .عىل سبيل المثwwال ،إذا أردت أن تضwwيف
وهناك طريقة أخرى هي أن تُ ِ
ُ
أربعة عق ٍد تحتوي عىل األعداد الصحيحة 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ر
صناديق مع أسهمٍ تُ ِّ
َ المتغيرات .تَظهَ ر المتغيرات بهيئة أسما ٍء داخل
المعرَّفة بها.
النسخ ُ
3.5تمرين 3
ستجد ملفات الشيفرة المطلوبة لهذا التمرين في مستودع الكتاب.
ً
مترابطwة لتخwزين ً
قائمwة :MyLinkedList.javaيحتوي عىل تنفيذ جزئي للواجهwة ،Listويَسwتخدِم •
العناصر.
ن ِّفذ األمر 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 MyLinkedList
;head = null
38
هياكل البيانات للمبرمجين قائمة المصفوفة ArrayList
;size = 0
}
}
بينما يشير المتغير 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ر .إذا لم تكن عىل
39
هياكل البيانات للمبرمجين قائمة المصفوفة ArrayList
يُ ِّ
وضح هذا المثال نمطين ستحتاج لمعرفتهما إلكمال حل التمرين:
.1في كثير من التوابع ،عاد ًة ما نضطرّ إىل معالجة أول عنصر ٍ في القائمة بطريقةٍ خاصة .وفي هwwذا المثwwال،
إذا كنا نضيف العنصر األول اىل القائمة ،فعلينا أن نُعدِّل قيمة ،headأما في الحاالت األخرى ،فعلينا أن
نجتاز القائمة ،حتى نصل إىل نهايتها ،ثم نضيف العقدة الجديدة.
.2يُب ّين ذلك التابع طريقة استخدام حلقة التكرار forمن أجwwل اجتيwwاز أو التنقwwل بين العقwwد الموجwwودة في
واآلن حان دورك ،أكمل متن التابع .indexOfويجب أن تقرأ مقwwال توثيwwق List indexOfلكي تعwwرف مwwا
ينبغي عليك القيام به .انتبه تحديدًا للطريقة التي يُفت َرض له معالجة القيمة الفارغة nullبها.
كما هو الحال في تمرين الفصل السابق ،و َّفرنا التwwابع المسwwاعد equalsللموازنwwة بين قيمwwة عنصwwر ٍ ضwwمن
شِّ wغل االختبwwارات مwwر ًة أخwwرى عنwwدما تنتهي .ينبغي أن ينجح االختبwwار testIndexOfوكwwذلك االختبwwارات
الممرَّر .وبالمثل ،اقرأ أواًل التوثيق List addثم ن ِّفذ التwابع ،وأخwيرًا ،شِّ wغل االختبwارات لكي
الجديدة في الفهرس ُ
تتأ ّكد من أنك ن ّفذتها بشكل سليم.
40
هياكل البيانات للمبرمجين قائمة المصفوفة ArrayList
لننتقل اآلن إىل التابع األخير :أكمل متن التابع .removeاقرأ توثيق التابع .List removeبعwwدما تنتهي من
وبالتالي ال تُكنَس المصفوفة وال يُكنَس أي من عناصwwرها حwwتى يحين موعwwد تwwدمير القائمwwة ذاتهwwا .في المقابwwل،
المسwwتخدَمة مباشً w
wرة ،وهwwو مwwا يُم ِثwwل تتقلص القائمة المترابطة عند حذف العناصر منها ،كما تُكنَس العقwwد غwwير ُ
ً
واحدة من مميزات هذا النوع من هياكل البيانات.
عندما نضبُط قيمة الرأس headبالقيمة ،nullفإننا نحذف المرجع إىل العقدة األوىل .إذا لم تكن هنwwاك أي
مراجع أخرى إىل ذلك الكائن (من المفترض أال يكون هناك أي مراجع أخرى) ،فس ُيكنَس الكائن مباشر ًة .في تلwwك
يستغرِق زمنًا ثابتًا ،ولكنك عندما تستدعيه ستُحفزّ كانس المهمالت عىل إجراء عملية تتناسب مع عدد العناصwwر،
يُع ّد هذا مثااًل عىل ما نُسميه أحيانًا بـمشكلة برمجية في األداء ،أي أن البرنامج يفعل الشيء الصحيح ،ولكنwwه
ً
خاصة في اللغات التي تُن ِّفذ أعمwwااًل الم َّ
توقع .يَص ُعب العثور عىل هذا النوع من األخطاء ال ينتمي إىل ترتيب النمو ُ
كثير ًة وراء الكواليس مثل عملية كنس المهمالت مثالً ،وتُع ّد لغة جافا واحد ًة من تلك اللغات.
41
.4القائمة المرتابطة LinkedList
سنتناول في هذا الفصل حل تمرين الفصل الثالث ،ثم نتابع مناقشة تحليل الخوارزميات.
قبل المتابعة:
المتحكِّم تُسنَد 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ا
ً
أيضا.
واآلن ،انظر إىل تنفيذ التابع addذي المعاملين ،وحاول تصنيفه قبل متابعة القراءة.
الصفر ،فإننا نضيف العقدة الجديدة إىل بدايwwة القائمwwة ،ولهwwذا علينwwا أن نُعwwالِج ذلwwك
ساوي ِّ
إذا كان indexيُ ِ
نص wل إىل العنصwwر الموجwwود في الفهwwرس
مثل حالة خاصة .وبخالف ذلك سنضطرّ إىل التنقل في القائمة إىل أن ِ
43
هياكل البيانات للمبرمجين القائمة المترابطة LinkedList
}
;return node
}
يفحص التابع getNodeما إذا كانت قيمة indexخارج النطاق المسموح به ،فإذا كانت كwwذلك ،فإنwwه يُبلِّغ
عن اعتراض exception؛ أما إذا لم تكن كذلك ،فإنه يمرّ عبر عناصر القائمة ويعيد العقدة المطلوبة.
nodeو .node.nextقد يساعدك رسم هذه العملية عىل التأكد من فهمها بوضوح.
.2تَستغرِق جميع التعليمات زمنًا ثابتًا سوا ٌء قبل استدعاء التابع getNodeأوبعد اسwwتدعائه ضwwمن التwwابع
.add
ً
نظرة عىل التابع :remove ُلق
وأخي ًرا ،لن ِ
يَستدعِ ي 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ا
الخطي:
ّ المجموعة ) O(1أو الزمن الثابت ،بينما يشير nإىل المجموعة ) O(nأو الزمن
MyLinkedList MyArrayList
n 1 ( addفي النهاية)
في حالتي إضافة عنصر أو حذفwه من نهايwة القائمwة ،فwإنّ الصwنف MyArrayListهwو أفضwلُ من نظwيره
إ ًذا ،أيهما أفضل؟ يعتمد ذلك عىل العمليات التي يُحتمل استخدامها أكثر ،وهذا السبب هو الذي يجعل جافا
45
هياكل البيانات للمبرمجين LinkedList القائمة المترابطة
Profiling التشخيص4.3
ِّغلwا أن تُشwwيفرة بإمكانهwwنف عىل شwwذا الصww يحتوي ه. في التمرين التاليProfiler ستحتاج إىل الصنف
. وتَعرِض النتائج، وتقيس زمن التشغيل لكلٍّ منها، ٍتابعً ا ما عىل مشاكلَ ذات أحجامٍ متفاوتة
ِّ ُت
:وضح الشيفرة التالية طريقة استخدام ذلك الصنف
46
هياكل البيانات للمبرمجين القائمة المترابطة LinkedList
لكي يتمكَّن الصنف Profilerمن أداء عمله ،سنحتاج أواًل إىل إنشwwاء كٍ w
wائن من النwwوع .Timeableيُwwو ِّفر
هذا الكائنُ التابعين setupو ،timeMeحيث يُن ِّفذ التابع setupكل ما ينبغي فعله قبل بwدء تشwغيل المwؤقت،
أم wا التwwابع timeMeف ُين ِّفذ العمليwwة الwwتي نحwwاول قيwwاس أدائهwwا .في هwwذا ً
قائمة فارغةّ ، نشئ
وفي هذا المثال س ُي ِ
حال لست في حاجةٍ إىل معرفة الكثwwير عنهwwا لحwwل التمwwرين التwwالي ،ويُمكِنwwك نسwwخ شwwيفرة
ٍ ولكنك عىل كل
المثال وتعديلها.
واآلن سننتقل إىل الخطوة التالية ،وهي إنشاء كائن من الصwwنف Profilerمwwع تمريwwر معwwاملين لwwه همwwا:
:endMillisعبwwارة عن قيمwwة قصwwوى بوحwwدة الميلي ثانيwwة .يwwزداد زمن التشwwغيل عنwwدما يُزيwwد التwwابع •
التابع .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ويُمثِل
4.4تفسري النتائج
َّ
نتوقع أن يسwwتغرق التwwابع addزمنًwwا ثاب ًتwwا عنwwدما بنا ًء عىل فهمنا لكيفية عمل الصwwنف ،ArrayListفإننwwا
48
هياكل البيانات للمبرمجين القائمة المترابطة LinkedList
لكي نختبر صحة تلك النظرية ،سنَعرِض تأثير زيادة حجم المشwكلة عىل زمن التشwغيل الكلّي .من المفwترض
خط مستقيمٍ عىل األقل ألحجwwام المشwwكلة problem sizeالكبwwيرة بالقwwدر الكwwافي لقيwwاس زمن
أن نحصل عىل ٍّ
ُ
كتابة دالّة هذا الخط المستقيم رياض ًّيا عىل النحو التالي: التشغيل runtimeبدقة .ويُمكِننا
runtime = a + b*n
حيث يشير aإىل إزاحةِ الخط وهو قيمة ثابتة و bإىل ميل الخط.
ً
دقيقة فسنتمكَّن بسwهولةٍ من التميwيز بين الخwط المسwتقيم والقطwع إذا كانت القياسات التي حصلنا عليها
تماما ،فقد يكون تمييز ذلك صعبًا إىل ح ٍّد ما ،وعندئ ٍ wذ يُ ّ
فض wلُ اسwwتخدام مقيwwاس ً ً
دقيقة أما إذا لم تكن
المكافئّ ،
لوغاريتمي-لوغاريتمي log-logلعرض تأثير حجم المشكلة عىل زمن التشغيل.
السبب ،لنفترض أن زمن التشغيل يتناسب مع ،nkولكننا ال نعلم قيمة األس .kيُمكِننا كتابwwة تلwwك
َ ولِنَعر ِ َف
وبالتالي يُمكِن تقريب ًّيا إهمال باقي الحدود وكتابة العالقة عىل النحو التالي:
runtime ≈ c * n^k
حيث نعني بالرمز ≈ "يساوي تقريبًا" ،فإذا حسبنا اللوغاريتم لطرفي المعادلة ،فستصبح مثل اآلتي:
تعني المعادلة السابقة أنه لو رسمنا زمن التشغيل مقابل حجم المشwwكلة nباسwwتخدام مقيwwاس لوغwwاريتمي-
بثابت -يمثل اإلزاحة -يساوي ) log(cوبميwل يسwاوي .kال يهمنwwا الثwwابت هنwwا
ٍ مستقيما
ً خطا
لوغاريتمي ،فسنرى ً
وزبدة الكالم أنه إذا كانت قيمwwة kتسwwاوي ،1فالخوارزميُ w
wة ُ وإنما يهمنا الميل ،kفهو الذي يشير إىل ترتيب النمو.
49
هياكل البيانات للمبرمجين القائمة المترابطة LinkedList
wل تقريب ًيwwا ،في حين لwwو اسwwتدعينا التwwابع wاني السwwابق ،يُمكِننwwا أن نُقwدِّر قيمwwة َ
الم ْيِ w تأملنا في الرسwwم البيّ w
إذا ّ
،plotResultsفسيحسب قيمة الميل بتطبيق طريقة المربعات الدنيا least squares fitعىل القياسwwات،
ثم يَطبَعه .و قد كانت قيمة الميل التي حصل عليها التابع:
أي تقريبًا يساوي .1إ ًذا فالزمنُ الكلي إلجراء عدد مقداره nمن اإلضwافات هwو زمن خطي ،وزمن كwwل إضwافة
4.5تمرين 4
ستجد ملفات الشيفرة المطلوبة لهذا التمرين في مستودع الكتاب.
:Profiler.java .1يحتوي عىل تنفيذ الصنف Profilerالذي شرحناه فيما سبق .ستَسwwتخدِم ذلwwك
الصنف ،ولن تحتاج لفهم طريقة عمله ،ومع ذلك يُمكِنك االطالع عىل شيفرته إن أردت.
:ProfileListAdd.java .2يحتوي عىل شيفرة تشغيل التمرين ،بمwwا في ذلwwك المثwwال العلwwوي الwwذي
شخَّصنا خالله التابع .ArrayList.addستُعدِّل هذا الملف لتُجرِي التشخيص عىل القليل من التوابwwع
األخرى.
ن ِّفذ األمر ant ProfileListAddلكي تُش ِّ wغل الملwwف .ProfileListAdd.javaينبغي أن تحصwwل
،profileArrayListAddEndفستحتاج فقwwط إىل إجwwراء القليwwل من التعwwديالت .في األخwwير ،أضwwف سwwط ًرا
50
هياكل البيانات للمبرمجين القائمة المترابطة LinkedList
بعwwwwwwwwد ذلwwwwwwwwك ،وازن ذلwwwwwwwwك األداء مwwwwwwwwع أداء الصwwwwwwwwنف .LinkedListامأل متن التwwwwwwwwابع
عنصرًا جديدًا إىل بداية القائمة .ما األداء الذي تتوقعه؟ وهل تتوافق النتائج مع تلك التوقعات؟
بينما يُض ِيف عنصرًا جديدًا إىل نهاية القائمة .ما األداء الذي تتوقعه؟ وهل تتوافق النتائج مع تلك التوقعات؟
51
.5القائمة ازدواجية الرتابط
Doubly-Linked List
سنراجع في هذا الفصل نتwائج تمwرين الفصwwل الرابع السwابق ،ثم سwنُقدِّم تنفيً wذا آخwرَ للواجهwwة ،Listوهwwو
ArrayListو LinkedListعىل أحجام مختلفة من المشكلة ،ثم عرضنا زمن التشغيل مقاب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ل
الخوارزميات.
يُشخ ِّص عملية إضافة عناصر جديدة إىل بداية قائمة من النوع .ArrayListوبنا ًء عىل تحليلنا للخوارزمية ،فقwwد
توقعنا أن يتطلَّب تنفيذ إضافة واحدة زمنًا خط ًيا بسwwبب تحريwwك العناصwwر األخwwرى إىل اليمين ،وعليwwه َّ
توقعنwwا أن ّ
انظر إىل حل التمرين الذي ستجده في مجلد الحل داخل مستودع الكتاب:
هياكل البيانات للمبرمجين Doubly-Linked List القائمة ازدواجية الترابط
ابعwwود في التwwد موجwwارق الوحيww فالف،profileArrayListAddEnd ابعwwع التwwا مwwًيتطابق هذا التابع تقريب
َ
اww كم،0 رسwwدة في الفهww لكي يضع العناصر الجديadd ثنائية المعامل من التابع ً
نسخة حيث يَستخدِم،timeMe
:)انظر إىل النتائج (حجم المشكلة عىل اليسار وزمن التشغيل بوحدة الميلي ثانية عىل اليمين
14
35
150
604
2518
11555
.problem size مقابل حجم المشكلةruntime رسما بيان ًيا لزمن التشغيل
ً تَعرِض الصورة التالية
53
هياكل البيانات للمبرمجين Doubly-Linked List القائمة ازدواجية الترابط
wل يسwwاوي
خط مستقيمٍ بميٍ w
2
ي لعدد nمن اإلضافات متناسبًا مع ،nوأن نحصل عىل ٍّ ّ
المثال أن يكون الزمنُ الكل ّ
المقدَّر 1.992تقريبًا ،وهو في الحقيقة دقيق جدًا لدرجةٍ تجعلنا ال نرغب في تزوير
،2وفي الحقيقة يساوي الميل ُ
بيانات بهذه الجودة.
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عىل األقل ،وبالتالي يتطلَّب تنفي ُذ إضافةٍ واحد ٍة زمنًا ثابتًا.
يضطر تwابع اإلضwافة إىل المwرور عwبر قائمwة العناصwر بالكامwل لكي
ّ أن يكون الصنف LinkedListأبطأ ،حيث
َّ
نتوقع أن يكون الwwزمن الكلي لعwwدد يتمكَّن من إضافة عنصر جديد إىل النهاية ،مما يَعنِي أن العملية خطية ،وعليه
في الواقع هذا ليس صحيحًا ،ويمكنك االطالع إىل الشيفرة التالية:
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ةِ
الفكرة هي أن الصنف 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ر
indexOf /
n n n
lastIndexOf
القائمة ،ويتمتعان بنفس الكفاءة فيمwwا يتعلَّق بعمليwwتي اإلضwwافة والحwwذف من نهايwwة القائمwwة ،وبالتwwالي تقتصwwر
أفضلية الصنف ArrayListعليه بعمليتي getو ،setألنهما تتطلبان زمنًا خط ًيا في القوائم المترابطة حتى لو
كانت مزدوجة.
إذا كان زمن تشغيل التطبيق الخاص بك يعتمwwد عىل الwwزمن الwwذي تتطلَّبwwه عمليتwwا getو ،setفقwwد يكwwون
التنفيذ ArrayListهو الخيار األفضل؛ أما إذا كان يَعتمِ د عىل عملية إضافة العناصر وحذفها إىل بدايwwة القائمwwة
58
هياكل البيانات للمبرمجين Doubly-Linked List القائمة ازدواجية الترابط
لو لم تكن تلك العمليات تستغرِق جزءًا كبيرًا من زمن تشغيل التطبيق الخاص بك -أي لو كwwان التطwwبيق •
يقضي غالبية زمن تشغيله في تنفيذ أشياء أخرى ،-فإن اختيارك لتنفيwwذ الواجهwwة Listغwwير مهم لتلwwك
ِ
الدرجة.
َّ
تتوقعwwه ،فبالنسwwبة ً
كبيرة بدرجة كافية ،فلربما لن تحصل عىل األداء الذي إذا لم تكن القوائم التي تُعالجها •
للمشكالت الصغيرة ،قد تكون الخوارزمية التربيعية أسر ع من الخوارزمية الخطية ،وقد تكون الخوارزميwwة
هم كثيرًا.
الخطية أسر ع من الخوارزمية ذات الزمن الثابت ،كما أن االختالف بينها في العموم ال يُ ّ
إىل جانب بعضها البعض ض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ل
.3حجم المشكلة كبير ٌ بالقدر الكافي بحيث يتمكن ترتيب النمو من توقع هيكل البيانات األنسب.
في الحقيقة ،يُمكِنك أن تتمتع بحيا ٍة مهن ّيةٍ طويلةٍ أثنwwاء عملwwك كمهنwwدس برمجيwwات دون أن تتع wرَّض لهwwذا
59
.6التنقل في الشجرة Tree Traversal
تنفي ًذا تعاوديًwwا recursiveألسwwلوب البحث بwwالعمق أواًل depth-firstوكwwذلك تنفي ً wذا تكراريًwwا ُ
للمك wدِّس stack
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ه
أو حتى عبر كل صفحات موقع ويكيبيديا ،كما أن هذا التمرين ممت ٌع نو ًعا ما.
ختصر عاد ًة إىل .HTMLعىل سبيل المثال ،انظر إىل مستند HTMLالتالي:
،Languageالتي تُ َ
><!DOCTYPE html
><html
><head
><title>This is a title</title
></head
><body
><p>Hello world!</p
></body
></html
فهي عبارة عن وسوم tagsتشير إىل الكيفية التي ستُعرَض بها تلك النصوص.
حمل الزاحف صفحة إنwترنت ،يُحلِّل محتوياتهwا المكتوبwة بلغwة HTMLليتمكَّن من اسwتخراج النص
بعد أن يُ ِّ
وإيجاد الروابط .سنَستخدِم مكتبة jsoupمفتوحة المصدر من لغة جافا إلجراء ذلك ،حيث تستطيع تلك المكتبwwة
ينتج عن تحليل مستندات 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ابنًا واحدًا هwو العقwدة < ،>titleوبالمثwwل ،تملwwك العقwدة < >bodyابنًwا واحwدًا هwو
تحتوي كلّ عقدة عىل روابط إىل عقد األبن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من القائمة؛ أما
القائمة .يُمكِنك القراءة عن أداة Web Inspectorالتي يُو ِّفرها متصفح سفاري أو كروم .Chrome
62
هياكل البيانات للمبرمجين التنقل في الشجرة Tree Traversal
تعرض الصورة السابقة لقطwwة شاشwwة لنمwwوذج DOMالخwwاص بمقالwwة ويكيبيwwديا عن لغwwة جافا ،حيث يُم ِّثلُ
العنصر المظلل أول فقرة في النص الرئيسي من المقالة .الحِ ظ أن الفقرة تقع داخل عنصwwر < >divالwwذي يملwwك
السمة ،"id="mw-content-textوالتي سنَستخ ِدمها للعثور عىل النص الرئيسي في أي مقالةٍ ن ُ ِّ
حملها.
المثال التالي:
= String url
;")"http://en.wikipedia.org/wiki/Java_(programming_language
ّ
وحلله // ِّ
حمل المستند
;)Connection conn = Jsoup.connect(url
;)(Document doc = conn.get
حمل التابع getمستند HTMLويُحلِّله ،ويعيد كائنًا من النوع Documentيُمثِل شجرة .DOM
الويب .بعد ذلك يُ ِّ
63
هياكل البيانات للمبرمجين التنقل في الشجرة Tree Traversal
ً
كثيرة جدًا لدرجة يُو ِّفر الصنف Documentتوابعً ا للتنقل عبر الشجرة واختيار العقد .في الواقع ،إنه يُو ِّفر توابع
ً
نصية من النوع ،Stringويبحث ضمن الشجرة عن عنصر ٍ ً
سلسلة :getElementByIdيستق ِبل •
Elementيُمثِل عنصر < >divذاك ،ويحتوي عىل العناصر الموجودة داخله بهيئة أبنا ٍء وأحفا ٍد وغيرها.
سلسلة نص ّي ًة من النوع ،Stringويتن ّقل عبر الشجرة ،ثم يُعيد جميع العناصر التي
ً :selectيستقبل •
يتوافق الوسم tagالخاص بها مع تلك السلسلة النصية .يعيد التابع في هذا المثال جميع وسوم
الفقرات الموجودة في الكائن .contentتكون القيمة المعادة عبارة عن كائن من النوع .Elements
األهم.
ّ إمكانيات كلٍّ منها .تجدر اإلشارة إىل أنّ األصناف Elementو Elementsو Nodeهي األصناف
ٌ
فرعية subclassesكثwwير ٌة مثwwل Element ٌ
أصناف يُمثِل الصنف Nodeعقد ًة في شجرة .DOMوتمتد منه
ً
تجميعwwة من النwwوع Collectionالwwتي د الصwwنف Elements
و TextNodeو DataNodeو .Commentيُعّ ww
64
هياكل البيانات للمبرمجين التنقل في الشجرة Tree Traversal
يُكمِ ل هذا المثال ما وصلنا إليه في المثال السابق ،فهو يختار الفقwرة األوىل في الكwائن paragraphsأواًل ،
WikiNodeIterableبح ًثا بتقنية العمق أواًل ،depth-firstويُولِّد العقد بنفس ترتيب ظهورها بالصفحة.
تَطبَع الشيفر ُة العقَ wد إذا كwانت من النwوع TextNodeوتتجاهلهwا إذا كwانت من أي نwوع آخwر ،والwتي تُمثِwل
أبناء ،فإنها ستختار االبن األول ،وتستمر في ذلك حتى تصل إىل عقد ٍة ليس لهwwا أبنwwاء ،أين تبwwدأ بwwالتراجع عنwwدها
والتحرك ألعىل إىل عقدة األب ،لتختار منها االبن التالي إن كان موجwwودًا ،وفي حالwwة عwwدم وجwwوده ،فإنهwwا تwwتراجع
للوراء مجددًا .عندما تنتهي من البحث في االبن األخير لعقدة الجذر ،فإنها تكون قد انتهت.
هناك طريقتان شائعتان لتنفيذ :DFSإما بالتعاود ،recursionأو بالتكرار .يُع ّد التنفيذ بالتعاود هwwو الطريقwwة
األبسط:
65
هياكل البيانات للمبرمجين التنقل في الشجرة Tree Traversal
،TextNodeويطبع التابع محتوياتِها ،ثم يفحص إذا كان للعقدة أي أبنwاء .فwإذا كwان لهwا أبنwاء ،فإنwه س َيسwتدعِ ي
في هذا المثالَ ،طبَعَ نا محتويات العقد التي تنتمي إىل النوع 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ريحًا
لالحتفاظ بالعقد ،وفي تلك الحالة لن نحتاج إىل التعاود ،حيث سنتمكَّن من التنقل في الشجرة عبر حلقة تكراريّة.
بيانات مشابهً ا للقائمة ،فهو عبارة عن تجميعة تتذكر ترتيب العناصر .ويتمثل الفرق بين
ٍ المكدِّس هيكل
يُع ّد ُ
المكدِّس التوابع التالية: ً
عادة ما يُو ِّفر ُ المكدِّس يو ِّفر توابعَ أقل ،وأنه
المكدّس والقائمة في أن ُ
المكدِّس.
:pushيضيف عنصرًا إىل أعىل ُ •
المكدِّس ويعيده.
:popيح ِذف العنصر الموجود أعىل ُ •
66
هياكل البيانات للمبرمجين التنقل في الشجرة Tree Traversal
" ،"LIFOوالتي تُع ّد اختصا ًرا لعبارة "الداخل آخ ًرا ،يخرج أواًل " .في المقابل ،تُع ّد األرتال queueبدياًل للمكد
ّسات،
ِ
ً
wافية عن تلwwك
wات إضw ً
واضحة بالنسبة لك ،فهمwwا ال يwwوفران أي إمكانيٍ w المكدِّسات واألرتال
قد ال تكون أهمية ُ
الموجودة في القوائم .listsبل يوفران إمكانيات أقل ،لذلك قد تتساءل لم ال نكتفي باستخدام القwwوائم؟ واإلجابwwة
.1إذا ألزمت نفسك بعدد أقل من التوابع ،أي بواجهة تطوير تطبيقات APIأصغر ،فعاد ًة ما تصبح الشwwيفرة
مقروء ًة أكثر ،كما تقل احتمالية احتوائها عىل أخطاء .عىل سwwبيل المثwwال ،إذا اسwwتخدمت قائمً w
wة لتمثيwwل
ُمكدِّس ،فقد تَحذِف -عن طريق الخطأ -عنصرًا بترتيب خاطئ .في المقابل ،إذا استخدمت واجهة تطwwوير
ً
صغيرة ،فسيكون تنفيذها بكفwwاء ٍة أسwwهل. .2إذا كانت واجهة تطوير التطبيقات التي يُو ِّفرها هيكل البيانات
المترابطة تستغرق زمنًا ثابتًا ،فإننا نكون قد حصلنا عىل تنفي ٍ wذ ذي كفwwاء ٍة عاليwwة .في المقابwwل ،يَص ُ wعب
من إجراء عمليتي اإلضافة والحذف من نهاية القائمة ألنهم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
{ ))(while (!stack.isEmpty
;)(Node node = stack.pop
{ )if (node instanceof TextNode
;)System.out.print(node
}
المك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كالتالي:
المعرَّفة بالواجهة Listال توابع الواجهة Deque؛ أمwwا إذا أسwwندته عىل
فسيكون في إمكانك استخدام التوابع ُ
النحو التالي:
69
.7كل الطرق تؤدي إىل روما
َ
زاحف إنترنت crawlerيختبر صحة فرض ّية "الطريق إىل مقالة الفلسwwفة" Getting سنبني في هذا الفصل
- to Philosophyالتي تشبه المثل الشهير كل الطرق تؤدي إىل روما -في موقع ويكيبيديا التي شرحنا معناهwwا
7.1البداية
ستجد في مستودع الكتاب ملفات الشيفرة التالية التي ستساعدك عىل بدء العمل:
:WikiNodeIterable.java .2يحتوي عىل صwنف ممتٍّ wد من النwوع Iterableبإمكانwwه المwرور عwwبر
شجرة .DOM
ً
أداة تَستخدِم مكتبة jsoupلتحميwwل الصwwفحات من صنف يُعرِّف
ٍ :WikiFetcher.java .3يحتوي عىل
موقع ويكيبيديا .ويضع الصنف حدًّا لسرعة تحميل الصفحات امتثااًل لشروط الخدمwwة في الموقwwع ،فwwإذا
طلبت أكثر من صفحة في الثانية الواحدة ،فإنه ينتظر قلياًل قبل أن يُ ِّ
حمل الصفحة التالية.
ant WikiPhilosophy
هياكل البيانات للمبرمجين كل الطرق تؤدي إىل روما
7.2الواجهتان IterablesوIterators
تناولنا في الفصل السابق تنفي ًذا تكراريًا لwwه ،وذكرنwwا وجwwه تفضwwيله عىل التنفيwwذ التعwwاودي من جهwwة سwwهولة
الخwwwwwارجي
ُّ ألwwwwwق نظwwwwwر ًة عىل محتويwwwwwات الملwwwwwف .WikiNodeIterable.javaيُن ِّفذ الصwwwwwنف
ِ
WikiNodeIterableالواجهة > ،Iterable<Nodeولذا يُمكِننا أن نَسwwتخدِمه ضwwمن حلقwwة تكwwرار loopعىل
النحو التالي:
يشير rootإىل جذر الشجرة التي ننوي اجتيازها أو التنقل فيها ،بينما يُمثِل visitالتابع الwwذي نwwرغب في
@Override
{ )(public Iterator<Node> iterator
;)return new WikiNodeIterator(root
71
هياكل البيانات للمبرمجين كل الطرق تؤدي إىل روما
}
}
Deque<Node> stack;
@Override
public boolean hasNext() {
return !stack.isEmpty();
}
@Override
public Node next() {
if (stack.isEmpty()) {
throw new NoSuchElementException();
}
َّ ا ُمww ولكنّه،يرwwد كبwwتتطابق الشيفرة السابقة مع التنفيذ التكراري ألسلوب "البحث بالعمق أواًل " إىل ح
مةw قس
72
هياكل البيانات للمبرمجين كل الطرق تؤدي إىل روما
ً
فارغا. :isEmpty .2يفحص ما إذا كان المكدس
العقدة التي سحبها .وفي حال استدعاء التابع nextفي كائن Iteratorفار ٍغ ،فإنه يُبلِّغ عن اعwwتراض
.exception
ً
فكرة غير جديرة باالهتمام. ربما تعتقد أن إعادة كتابة تابع جيد فعل ًيا باستخدام صنفين ،وأن خمسة توابع تُعَ د
استخدام النوع .Iterableيُسهِّل ذلwwك من الفصwwل بين منطwwق التنفيwwذ التكwwراري (البحث بwwالعمق أواًل ) وبين
7.3الصنف WikiFetcher
ً
كثيرة بسwرعةٍ فائقwةٍ ،ممwا قwد يwؤدي إىل انتهwاك شwروط الخدمwة صفحات
ٍ حمل
يستطيع زاحف الويب أن يُ ِّ
حمل منه تلك الصفحات .ولكي نتجنَّب ذلك ،و َّفرنا الصنف WikiFetcherالذي يقوم بما يلي:
للخادم الذي يُ ِّ
.1يُغلِّف الشيفرة التي تناولناها في الفصل السابق ،أي تلك التي تُ ِّ
حمل الصwwفحات من موقwwع ويكيبيwwديا،
نقضي بين طلبات االتصال ،فإذا لم يَكن كاف ًيا ،فإنه ينتظر حتى تمرّ فتر ٌة معقولwwة .وقwwد
الم ِ
.2يقيس الزمن ُ
افتراضي. بشكل ً
ثانية واحد ًة ضبطنا تلك الفترة لتكون
ّ ٍ
**/
ِّ
حمل صفحة محدد موارد موحد وحللها *
73
هياكل البيانات للمبرمجين كل الطرق تؤدي إىل روما
*/
public Elements fetchWikipedia(String url) throws IOException {
sleepIfNeeded();
رww وينتظ،طلب
ٍ المنقضي منذ آخر
َ َ الذي يفحص الزمنsleepIfNeeded تقع الشيفرة الجديدة ضمن التابع
. والمقدّرة بوحدة الميلي ثانيةminInterval إذا كان الزمن أقلّ من القيمة الدنيا
74
هياكل البيانات للمبرمجين كل الطرق تؤدي إىل روما
افترضنا في هذا المثال أن urlListعبwwارة عن تجميعwwة تحتwwوي عىل سالسwwلَ نص ّ wية من النwwوع String
التابع .fetchWikipedia
تَستخ ِدمه لمعالجة جميع الطلبات؛ فلو كانت لديك عدة نسwwخ instancesمن الصwwنف ،WikiFetcherفإنهwwا
لن تتمكَّن من فرض الزمن األدنى الالزم بين كل طلب والطلب الذي يليه.
أن تتجنب تلك المشكلة بجعل الصنف WikiFetcherيتبع نمط التصميم المفردة .singleton
7.4تمرين 5
بسيطا يُ ِّ
وض wح طريقwwة اسwwتخدام أجwwزا ٍء من تلwwك ً ستجد في الملف WikiPhilosophy.javaتابع main
حملها ويُحلِّلها.
.1يَستق ِبل ُمحدّد موارد موحّدًا URLلصفحةٍ من موقع ويكيبيديا ،ويُ ِّ
.4إذا كان ُم حدّد الموارد الموحد يشير إىل مقالة ويكيبيديا عن الفلسفة ،فينبغي أن ينتهي البرنwwامج مشwwي ًرا
إىل نجاحه.
ً
قائمة من النوع Listتحتوي عىل جميwع ُمحwدّدات المwwوارد الwتي زارهwا ،ويَعwwرِض نشئ البرنامج
ينبغي أن يُ ِ
75
هياكل البيانات للمبرمجين كل الطرق تؤدي إىل روما
واآلن ،ما الذي نعنيه برابط "صالح"؟ في الحقيقة لwwدينا بعض الخيwwارات ،إذ تَسwwتخدِم النسwwخ المختلفwwة من
ً
بعضا منها هنا: ً
مختلفة نَستعرِض نظرية "الوصول إىل مقالة ويكيبيديا عن الفلسفة" قواع َد
.1ينبغي أن يكwون الرابwط ضwمن المحتwوى النصwي للصwفحة وليس في شwريط التنقwل الجwانبي أو خwارجَ
الصندوق.
.3ينبغي أن تتجاهل الروابط الخارج ّية والروابط التي تشير إىل الصفحة الحالية والروابط الحمراء.
بحرف كبير.
ٍ .4ينبغي أن تتجاهل الرابط إذا كان بادًئا
ليس من الضروري أن تتقيد بكل تلك القواعد ،ولكن يُمكِنك عىل األقل معالجة األقwwواس والخطwwوط المائلwwة
إذا كنت تظن أن لديك المعلومwwات الكافيwwة لتبwwدأ ،فابwدأ اآلن ،ولكن ال بwwأس قبwwل ذلwwك بقwwراءة التلميحwwات
التالية:
.1ستحتاج إىل معالجة نوعين من العقد بينما تجتاز الشwwجرة ،همwwا الصwwنفان TextNodeو .Elementإذا
بخwwط
ٍ .2عندما تقابل كائنًا من النوع Elementيحتوي عىل رابط ،فعندها يُمكِنك اختبار ما إذا كان مكتوب ًwwا
مائل باتباع روابط عقد األب أعىل الشجرة ،فإذا وجدت بينها الوسم < >iأو الوسم < ،>emفهذا يَع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تي
ليس لها عالقة بكلمات البحث ،وفي التركيز عىل الصفحات التي تتحدث عن البرمجة بلغة جافا.
لتحقيق ذلك ،فبتو ّفر كلمة بحث معينة ،يُمكِننا المرور عبر محتويات الصفحات ،وأن نختwwار من بينهwwا تلwwك الwwتي
تحتوي عىل كلمة البحث ،ولكن زمن التشغيل في تلك الطريقة سيتناسب مع عدد الكلمات الموجودة في جميع
ً
بطيئة للغاية. الصفحات ،مما يَعنِي أن العملية ستكون
هياكل البيانات للمبرمجين المفهرس Indexer
والطريقة البديلة عن تجميعة الصفحات collectionهي :الخريطة ،mapوالتي هي عبارة عن هيكل بيانwwات
،TermCounterبحيث تربُط كل كلمة بحث بعدد مرات ظهور تلك الكلمة في كwل صwفحة ،وسwتُمثِل المفwاتيح
كلمات البحث ،بينما ستُمثِل القيم عدد مرات الظهور (أو تكرار الظهور).
) :get(keyيبحث هذا التابع عن مفتاح معين ويعيد قيمته المقابلة. •
) :put(key, valueيضيف هذا التابع زوجًا جديدًا من أزواج مفتاح/قيمة إىل خريطة من النوع ،Map •
باإلضافة إىل الخريطة 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اءة.
ِ تُو ِّفر عملية
) :add(elementيضwwيف هwwذا التwwابع عنصwرًا إىل مجموعwwة .وإذا كwwان العنصwwر موجwwودًا فعل ًيwwا ضwwمن •
تُو ِّفر جافا عدة تنفيذات للواجهة ،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
الشيفرة التالية الجزء األول من تعريف الصنف:
يربط متغير النسخة mapالكلمات بعدد مرات حدوثها ،بينما يُحدّد المتغwwير labelالمسwwتند الwwذي يحتwwوي
عىل تلك الكلمات ،وسنَستخ ِدمه لتخزين محددات الموارد الموحدة .URLs
يُع ّد الصنف HashMapأكثر تنفيذات الواجهة Mapشيوعً ا ،وسنَستخ ِدمه لتنفيذ عملية الربط ،كما سwwنتناول
عمل التابع putبمثابة تابع ُمغلِّف ،فعندما تستدعيه ،س َيستدعِ ي بدوره التwwابع ُ put
المع wرَّف في الخريطwwة يَ َ
المخزَّنة داخله.
ُ
الخريطwwwwة ،ثم يَفحَص النتيجwwwwة ،فwwwwإذا لم تكن الكلمwwwwة موجwwwwود ًة في الخريطwwwwة من قبwwwwل ،فwwwwإن التwwwwابع
79
هياكل البيانات للمبرمجين Indexer المفهرس
تق ِبلww ويَس، بسهولةincrementTermCount بتلك الطريقة عىل تعريف التابعget يُساعدنا تعريف التابع
ً
ثم نَستخدِم التابع،1 ونزيد العداد بمقدار،0 القيمةget فسيعيد،موجودة ضمن الخريطة إذا لم تكن الكلمة
ً
موجودة في الخريطة إذا كانت الكلمة، في المقابل. جديد إىل الخريطةkey-value قيمة/ إلضافة زوج مفتاحput
. ثم نُخزِّنها بحيث تَستبدِل القيمة القديمة،1 ونزيدها بمقدار، فإننا نسترجع قيمة العداد القديم، فعاًل
80
هياكل البيانات للمبرمجين المفهرس Indexer
:processElementsيَستق ِبل هذا التابع كائنًا من النwwوع Elementsالwwذي هwwو تجميعwwة من كائنwwات •
ً
عقدة تُم ِّثل عقدة جwwذر شwwجرة ،DOMويَمwرّ التwwابع عwwبر الشwwجرة ،ليعwwثر عىل :processTreeيَستق ِبل •
العقد التي تحتوي عىل نص ،ثم يَستخر ِج منها النص ويُمرِّره إىل التابع .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ة
= String url
;")"http://en.wikipedia.org/wiki/Java_(programming_language
;)(WikiFetcher wf = new WikiFetcher
;)Elements paragraphs = wf.fetchWikipedia(url
يَستخدِم هذا المثال كائنًا من النوع WikiFetcherلتحميل صwwفحةٍ من موقwwع ويكيبيwwديا ،ثم يُحلّwwل النص
يُمكِنك تشغيل الشيفرة في القسم التالي ،واختبار فهمك لها بإكمال متن التابع غير المكتمل.
8.3تمرين 6
ستجد ملفات شيفرة التمرين في مستودع الكتاب:
: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لكي تتأ َّكد من أنك قد أكملت جزء التمرين ذاك
ً
جديدة إذا كنت ترى الكلمة للمرة الأولى // ً
مجموعة أنشئ
{ )if (set == null
;)(>set = new HashSet<TermCounter
;)index.put(term, set
}
إذا كنت قد رأيت الكلمة من قبلِّ ،
عدل المجموعة الموجودة //
82
هياكل البيانات للمبرمجين المفهرس Indexer
;)set.add(tc
}
يضيف التابع 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افة
كلمةٍ جديدة.
يُعَ ّد هيكل البيانات هذا ُمعقدًا بعض الشيء .والختصاره ،يمكن القwwول أن كwwائن النwwوع Indexيحتwwوي عىل
رسما توضيح ًّيا لتلك الكائنات ،حيث يحتوي كائن الصنف Indexعىل متغير نسخة
ً تَعرِض الصورة السابقة
اسمه indexيشير إىل كائن الصنف ،Mapالذي يحتوي -في هذا المثال -عىل سلسلةٍ نص ّيةٍ واحد ٍة Javaمرتبطةٍ
83
هياكل البيانات للمبرمجين المفهرس Indexer
بمجموعةٍ من النوع Setتحتوي عىل كwwائنين من النwwوع TermCounter؛ بحيث يكwwون واحwدًا لكwwل صwwفحة قwwد
كائن من النوع TermCounterعىل متغيرَ النسخة labelالذي يُمثِل ُمح wدّد المwwوارد الموحwwد
ٍ يتضمن كلّ
َّ
يتضمن المتغي َر mapالذي يحتوي عىل الكلمات الموجودة في الصفحة ،وعدد مرات
َّ URLالخاص بالصفحة ،كما
يُ ِّ
وضح التابع printIndexطريقة قراءة هيكل البيانات ذاك:
لكل كلمة ،اطبع الصفحات التي ظهرت فيها الكلمة وعدد مرات ظهورها //
;)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ي ِّ
حمل
دورك اآلن هو إكمال التابع indexPageالذي يَستق ِبل ُمحدّد موارد موحّدًا ( URLعبارة عن سلسلةٍ نصيةٍ )،
84
هياكل البيانات للمبرمجين Indexer المفهرس
...
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ثم سننتقل إىل شرح الجداول.
الصنف :MyLinearMap
يَستخدِم هذا الصنف معاملي نوع ،type parametersحيث يشwwير المعامwwل األول Kإىل نwwوع المفwwاتيح،
بينما يشير المعامل الثاني Vإىل نوع القيم .ونظرًا ألن الصنف MyLinearMapيُن ِّفذ الواجهة ،Mapفإن عليwwه أن
تحتوي كائنات النوع MyLinearMapعىل متغير نسخةٍ instance variableوحي ٍد ،entriesوهو عبwwارة
عن قائمة من النوع ArrayListمك ّونة من كائنات تنتمي إىل النوع ،Entryحيث يحتوي كل كwwائن من النwwوع
@Override
{ )(public K getKey
;return key
}
@Override
{ )(public V getValue
;return value
}
}
9.2تمرين 7
ستجد ملفات شيفرة التمرين في مستودع الكتاب:
:MyLinearMap.javaيحتوي هذا الصنف عىل الشيفرة المبدئية للجزء األول من التمرين. •
ستجد ً
أيضا ملف البناء build.xmlفي المستودع.
ن ِّفذ األمر ant buildلكي تُصرِّف ملفات الشيفرة ،ثم ن ِّفذ األمر .ant MyLinearMapTestسwتجد أن
بعض االختبارات لم تنجح؛ والسبب هو أنه ما يزال عليك القيام ببعض العمل.
أكمل متن التابع المساعد findEntryأواًل .ال يُع ّد هذا التابع جزءًا من الواجهة ،Mapولكن بمجرد أن تكمله
المْ wدخَالت،
معين ضwwمن ُ
ٍ مفتاح
ٍ بشكل صحيح ،ستتمكَّن من استخدامه ضمن توابعَ كثيرة .يبحث هذا التابع عن
ٍ
87
هياكل البيانات للمبرمجين الواجهة Map
الم ْدخَل الذي يحتوي عىل ذلك المفتاح ،أو القيمة الفارغة nullإذا لم يكن موجودًا ،كما يوازن التwwابع
ثم يعيد إما ُ
- equalsالذي وفرناه لك -بين مفتاحين ،ويعالج القيم الفارغة nullبشكل مناسب.
Map.putباللغة اإلنجليزية أواًل لكي تَعرِف ما ينبغي أن تفعله .ويُمكِنwwك البwwدء بكتابwwة نسwwخةٍ بسwيطةٍ من التwwابع
التابع ،أما إذا كانت لديك الثقة الكافية ،فبإمكانك كتابة التابع كاماًل من البداية.
ينبغي أن ينجح االختبار containsKeyبعدما تنتهي من كتابة التابع .putاقرأ توثيwق التwابع Map.getثم
ن ِّفذه ،وش ِّغل االختبارات مر ًة أخرى .وأخي ًرا ،اقرأ توثيق التابع ،Map.removeثم ن ِّفذه.
findEntryو:equals
قد يعتمد زمن تشغيل التابع equalsعىل حجم targetوالمفاتيح ،ولكنه ال يعتمد في العمwwوم عىل عwwدد
88
هياكل البيانات للمبرمجين Map الواجهة
ذا ليسww ولكن ه،ةww ربما يحالفنا الحظ ونجد المفتاح الذي نبحث عنه في البداي،findEntry بالنسبة للتابع
.زمنًا خط ًيا
89
هياكل البيانات للمبرمجين الواجهة Map
بعدما يَستدعِ ي التابعُ putالتابعَ ،findEntryفإن كwwل شwwي ٍء آخwرَ ضwwمنَه يسwwتغرق زمنًwwا ثاب ًتwwا .الحِ wظ أن
entriesهي عبٌ w
wارة عن قائمwwةٍ من النwwوع ،ArrayListوأن إضwwافة عنصwwر إىل نهايwwة قائمwwةٍ من ذلwwك النwwوع
َل
نضطر إىل إضافة ُم ْدخ ٍ
ّ تستغرق زمنًا ثابتًا في المتوسط؛ فإذا كان المفتاح موجودًا بالفعل في الخريطة ،فإننا لن
عملية خط ّي ًة ً
أيضا. ً تزال محصلة عمليتين خطيتين
نستخلص مما سبق أن جميع التوابع األساسية ضمن ذلك الصنف خطية ،ولهذا السبب أطلقنwwا عليwwه اسwwم
.MyLinearMap
كومة القش.
wوائم قصwwيرةٍ،
سنقسمها عىل عدة قَ w
ِّ .1بداًل من أن نُخزِّن ُ
الم ْدخَالت في قائمةٍ واحد ٍة كبير ٍة من النوع ،List
وسنَستخدِم شيفرة تعمية - hash codeوسنشرح معناها في الفصل التالي -لكل مفتاح؛ وذلك لتحديد
النمو - order of growthكما سنناقش الح ًقا ،-فما تزال العمليات األساسية خط ّي ًة ،ولكن هنالك خدعة
ستُمكِّننا من تجاوز ذلك ،فإذا زِدنا عدد القwwوائم بحيث نُق ّيwwد عwwدد ُ
المْ wدخَالت الموجwwودة في كwwل قائمwwة،
ثابت .سنناقش تفاصيل ذلك في تمرين الفصل التالي ،ولكن قبwwل أن
ٍ زمن
ٍ فسنحصل عىل خريطةٍ ذات
سنتناول حل هذا التمرين ونُحلِّل أداء التوابع األساسية للواجهة Mapفي الفصل التالي ،وسنُقدِّم ً
أيضا تنفي ًذا
أكثر كفاءة.
90
.10التعمية Hashing
،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ع
واآلن ،س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
الفرعية ُ
وعليه ،ال يُمكِننا أن نفترض وجود ذلك التنفيذ فعل ًيا.
عددًا صحيحًا يُعرَف باسم شيفرة التعمية .hash codeاألهم من ذلك هwwو أننwwا عنwwدما نقابwwل نفس الكwwائن مwwر ًة
معين ،فإننا سنحصل عىل نفس شيفرة التعمية إذا أردنا استرجاعه.
ٍ مفتاح
ٍ
الخاصة بالكائن .يختلف تنفيذ هذا التابع باختالف نوع الكائن ،وسنرى مثااًل عىل ذلك الح ًقا.
معين:
ٍ لمفتاح
ٍ الخريطة الفرع ّي َة المناسبة
َ يختار التابعُ المساع ُد التالي
92
هياكل البيانات للمبرمجين التعمية Hashing
إذا كان keyيساوي ،nullفإننا سنختار الخريطة الفرعيwwة الموجwwودة في الفهwwرس 0عشwwوائ ًيا .وفيمwwا عwwدا
من أنه ال يحتوي عىل قيمة سالبة ،ثم نَستخدِم عامwwل بwwاقي القسwwمة \%لكي نحصwwل عىل قيمwwةٍ واقعwwةٍ بين 0و
حتما ،ألنwwه
ً نحصل عىل نفس الخريطة التي حصلنا عليها عندما أضفنا ذلك المفتاح .نقول هنا أنه يُفترَض وليس
ٌ
بسيطة للغاية ،حيث يَستدعِ ي التابع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من ُ
شيفرة تعميته ،والتي تستغرق بعض الوقت ،ثم سنبحث في الخريطة الفرعية المقابلة.
93
هياكل البيانات للمبرمجين التعمية Hashing
قابل للتعديل immutable؛ أما إذا كان قاباًل للتعديل ،فاألمر يحتاج إىل بعض التفكير.
ٍ نوعً ا ما إذا كان الكائن غيرَ
كمثال عىل الكائنات غwير القابلwة للتعwديل ،سwنُعرِّف الصwنف ،SillyStringحيث يُغلِّف ذلwك الصwنف
في الواقع ،هذا الصنف ليس ذا فائد ٍة كبيرةٍ ،ولهذا السبب سميناه ،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ة،
المعرَّف فيهما.
كائنان من النوع SillyStringإذا تساوى متغير النسخة ُ innerString
الشرط التالي:
َ تُح ِّقق دال ّ ُة التعمية السابقة
إذا احتوى كائنان من النوع SillyStringعىل سالسلَ نص ّيةٍ متساوية ،فإنهما يحصالن عىل نفس شيفرة
التعمية.
تَ َ
عمل الشيفرة السابقة بشكل صحيح ،ولكنها ليست بالكفاءة المطلوبة؛ فهي تعيwwد شwwيفرة التعميwwة نفسwwها
لعدد كبير من السالسل النصية المختلفة؛ فمثاًل لو تك َّونت سلسلتان من نفس األحرف مهما كان ترتيبها ،فإنهمwwا
ستحصالن عىل نفس شيفرة التعمية ،بwwل حwwتى لwwو لم تتكونwwا من نفس األحwwرف ،فقwwد يكwwون حاصwwل المجمwwوع
كائنات كثير ٌة عىل نفس شيفرة التعمية ،فإنها ستُخزَّن في نفس الخريطwwة الفرعيwwة ،وإذا احتwwوت
ٌ إذا حصلت
أي قيمةٍ ضمن النطاق المسموح به متساوية .يُمكنك قراءة المزيد عن التصميم الج ّيد لدوال التعمية لو أردت.
المعرَّف فيه لتجعله يشير إىل سلسلةٍ نص ّيةٍ مختلفةٍ من النوع ،Stringكمwwا أنwwك ال تسwwتطيع
ُ innerString
دائما.
ً أن تُعدِّل السلسلة النص ّي َة التي يشير إليها ،وبالتالي ستكون للكائن نفس شيفرة التعمية
ولكن ،ماذا يحدث لو كان الكائن قاباًل للتعديل؟ انظر إىل تعريف الصwwنف SillyArrayالمطَ w
wابق للصwwنف
95
هياكل البيانات للمبرمجين Hashing التعمية
@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;
}
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ترَض أن •
تُكملها.
:Profiler.javaيحتوي عىل شيفر ٍة لقياس األداء ورسم تأثير حجم المشكلة عىل زمن التشغيل. •
antلكي تُصwwwwwرِّف ملفwwwwwات الشwwwwwيفرة ،ثم األمر كالعwwwwwادة ،عليwwwwwك أن تُن ِّفذ األمwwwwwر build
.ant MyBetterMapTestستفشل العديد من االختبارات؛ وذلك ألنه ما يزال عليك إكمال بعض التوابع.
97
هياكل البيانات للمبرمجين التعمية Hashing
راجwwwwع تنفيwwwwذ التwwwwابعين putو getمن ذات الفصwwwwول المشwwwwار إليهwwwwا في األعىل ،ثم أكمwwwwل متن
أكم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انظر
الم ْد َخلات المسموح بها في كل خريطة فرعية قبل إعادة حساب شيفرات التعمية //
متوسط عدد ُ
هياكل البيانات للمبرمجين الواجهة HashMap
@Override
{ )public V put(K key, V value
;)V oldValue = super.put(key, value
تأ ّكد مما إذا كان عدد العناصر في الخريطة الفرعية قد تجاوز الحد الأقصى //
{ )if (size() > maps.size() * FACTOR
;)(rehash
}
;return oldValue
}
}
المعرَّفة في الص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
الم ْدخَالت الموجودة في الجدول .بعد ذلك ،عليه أن يضwبُط حجم إ ًذا أكمل متن التابع rehashبحيث يُ ِّ
جمع ُ
المْ wدخَالت إليwwه مwwر ًة أخwwرى .و َّفرنwwا تwwابعين مسwwاعدين همwwا MyBetterMap.makeMaps
الجwwدول ،ويضwwيف ُ
و .MyLinearMap.getEntriesينبغي أن يُضاعِ ف حلك عدد الخرائط kفي كل مرة يُستد َعى فيها التابع.
100
هياكل البيانات للمبرمجين الواجهة HashMap
َ
شيفرة التعمية للمفتاح ،وهو ما يَستغِ رق زمنًا ثابتًا ،ثم يَسwتدعِ ي تابعً wا عىل خريطwةٍ فرع ّيwةٍ ، تابع
ٍ حسب كلّ
يَ ِ
وهو ما يَستغِ رق ً
أيضا زمنًا ثابتًا.
ربما األمورُ جيد ٌة حتى اآلن ،ولكن ما يزال من الصعب تحليل أداء التابع األساسي اآلخر ،putفهwwو يَسwwتغرِق
زمنًا ثابتًا إذا لم يضطرّ الستدعاء التابع ،rehashويَستغرِق زمنًا خط ًيا إذا اضطرّ لذلك .بتلك الطريقة ،يكwwون هwذا
التابع مشابهً ا للتابع ArrayList.addالذي حللنا أداءه في الفصل الثالث قائمة المصفوفة .ArrayList
االس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اب
عمل واحدة.
ٍ لمفتاح وإضافته لخريطةٍ فرع ّيةٍ ،وسيكون ذلك بمنزلةِ وحد ِة
ٍ شيفرة التعمية
101
هياكل البيانات للمبرمجين الواجهة HashMap
عمل واحwwدةٍ،
ٍ واآلن ،أصبح حجم الجدول ،4وبالتالي ،س ُين ِّفذ التابع putعند استدعائه في المرة التالية وحدة
ولكن ،في المرة التالية التي سيضطرّ خاللها الستدعاء ،rehashفإنه س ُين ِّفذ 4وحدات عمٍ w
wل لحسwwاب شwwيفرات
إذا أنزلنا األبراج كما تقترح األسهم ،سيمأل كلّ واح ٍد منها الفراغ الموجود قبل الwwبرج التwwالي ،وسنحصwwل عىل
وضح الرسم البياني مدى أهمية مضاعفة عدد الخرائط الفرعية kعندما نعيد حساب شيفرات التعمية؛ فلwwو
يُ ِ
ً
ثابتة إىل kبداًل من مضwwاعفتها ،سwwتكون األبwwراج قريبwwة جwدًا من بعضwwها ،وسwwتتراكم فwwوق بعضwwها، ً
قيمة أضفنا
في المتوسط ،وهذا أمر ٌ رائ ٌع بحق ،فأداء تلك العمليات هو نفسه تقريبًا بغض النظر عن حجم الجدول.
الزمن ،ولكن الحواسيب الحقيقية أعق ُد من ذلك بكثير ،فتبلغ أقصى سرعتها عنwwدما تتعامwwل مwwع هياكwwل بيانwwات
ُوضع في الذاكرة المخبئية ،cacheوتكون أبطأ قلياًل عندما ال يتناسwwب حجم هياكwwل البيانwwات
صغيرة بما يكفي لت َ
مع الذاكرة المخبئية ولكن مع إمكانية وضعها في الذاكرة ،وتكون أبطwwأ بكثwwير ٍ إذا لم يتناسwwب حجم الهياكwwل حwwتى
مع الذاكرة.
102
هياكل البيانات للمبرمجين الواجهة HashMap
باإلضافة إىل ما سبق ،فإن بعض التوابع التي كانت تَسwwتغرِق زمنًwwا ثاب ًتwwا في الصwwنف MyLinearMapقwwد
يضطرّ التابع clearلتفريغ جميع الخرائط الفرع ّيةِ التي يتناسب عددها مع ،nوبالتwwالي ،هwwذا التwwابعُ ّ
خطيٌّ.
لحسن الحظ ،ال يُستخدَم هذا التابع كثيرًا ،ولذا فما يزال هذا التنفيذ مقبواًل في غالبية التطبيقات.
ن ِّفذ األمر 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طرّ إىل
عندما ّ
شغلنا تلك الشيفرة ،وجدنا أن الميلَ يساوي 1.7تقريبًا ،ممwwا يشwwير إىل أن ذلwwك التنفيwwذ ال يسwwتغرق
ق باألداء. ِّ
برمجي ُمتعل ٍ
ٍّ زمنًا ثابتًا .في الحقيقة ،هو يحتوي عىل خطٍأ
ّ
نتوقwwع قبwwل أن عليك أن تعثر عىل ذلك الخطِأ وتصلحَه وتتأ َّكد من أن التابع putيستغرق زمنًا ثابتًا كمwwا كنwا
103
هياكل البيانات للمبرمجين الواجهة HashMap
فيما يلي:
خط ًّيا ً
أيضا ألنه يَستخدِم التابع sizeكما هو ُمب َّينٌ في الشيفرة التالية: يجعل ذلك التابع ّ put
َ
ثابت الزمن. خط ًّيا ،فإننا نهدر كل ما فعلناه لجعل التابع put
إذا تركنا التابع ّ size
ستجد الحل في مستودع الكتاب في الملف .MyFixedHashMap.javaانظر إىل بداية تعريف الصنف:
104
هياكل البيانات للمبرمجين الواجهة HashMap
بداًل من تعديل الصنف ،MyHashMapعرَّفنا صن ًفا جديwدًا يمتّ wد منwwه ،وأضwwفنا إليwwه متغwwير النسwwخة ،size
كانت التعديالت عىل التابعين removeو putأعقد قلياًل ؛ ألننا عندما نستدعي نسخها في الصwwنف األعىل،
فإننا ال نستطيع معرفة ما إذا كان حجم الخرائط الفرع ّية قد تغ ّير أم ال .تُ ِّ
وضح الشيفرة التالية الطريقة الwwتي حاولنwwا
يَستخدِم التابعُ removeالتابعَ chooseMapلكي يعثر عىل الخريطة المناسبة ،ثم يَطرَ ح حجمها .بعwwد ذلwwك،
يَستدعِ ي تابع الخريطة الفرعية removeالذي قد يُغ ّير حجم الخريطة ،حيث يعتمد ذلك عىل ما إذا كwwان قwwد وجwwد
المفتاح فيها أم ال ،ثم يضيف الحجم الجديد للخريطة الفرعية إىل ،sizeوبالتالي تصبح القيمة النهائية صحيحة.
105
هياكل البيانات للمبرمجين الواجهة HashMap
;)(rehash
}
;return oldValue
}
واجهنا نفس المشكلة هنا :عندما استدعينا تابع الخريطة الفرعية ،putفإننا ال نعرف مwwا إذا كwwان قwwد أضwwاف
مدخاًل جديدًا أم ال ،ولذلك استخدمنا نفس الحل ،أي بطرح الحجم القديم ،ثم إضافة الحجم الجديد.
بسيطا:
ً واآلن ،أصبح تنفيذ التابع size
يعتمد بعضها عىل بعض .انظر إىل العالقات بين تلك األصناف:
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خةٍ •
بأسهم صلبة.
107
.12الواجهة TreeMap
سwwwwنناقش في هwwwwذا الفصwwwwل تنفيً wwwwذا جديwwwwدًا للواجهwwwwة Mapيُعwwwwرَف باسwwwwم شwwwwجرة البحث الثنائية
.binary search treeيشيع استخدام هذا التنفيذ عند الحاجة إىل االحتفاظ بترتيب العناصر.
يشيع استخدام الصنف HashMapبفضل كفاءته العالية ،ولكنه مع ذلك ليس التنفيذ الوحيد للواجهwwة ،Map
.1قد تستغرق عملية حساب شيفرة التعمية زمنًا طوياًل .فعىل الwwرغم من أن عمليwwات الصwwنف HashMap
وإعادة حساب شيفرات التعمية للمفاتيح .بالنسwwبة لبعض التطبيقwwات ،قwwد يكwwون الحفwwاظ عىل تwwرتيب
من الصعب حل كل تلك المشكالت في الوقت نفسه ،ومع ذلك ،تُو ِّفر جافا التنفيذ TreeMapالwwذي يُعwwالِج
ً
بعضا منها:
الصنف دال ّ َة تعميةٍ ،وبالتالي ،يتجنَّب الزمن اإلضافي الالزم لحساب شيفرات التعميwwة،
ُ .1ال يَستخدِم ذلك
خطي.
وبزمن ّ
ٍ ن
لترتيب مع ّي ٍ
ٍ وف ًقا
.3يتناسب زمن تنفيذ غالب ّية توابع الصنف TreeMapمع ) ،log(nوالwwتي رغم أنهwwا ليسwwت بكفwwاءة الwwزمن
ً
جيدة جدًا. الثابت ،ولكنها ما تزال
سنشرح طريقة عمل أشجار البحث الثنائية في القسم التالي ثم سنستخدِمها لتنفيذ الواجهwwة ،Mapوأخwwيرًا،
ٌ
ابنة يسرى ،فال ب ُ ّد أن تكون قيم جميع المفاتيح الموجودة في الشجرة الفرعية أب عقد ٌة
.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ا
المثال ،إذا كنت تبحث عن مفتاح targetقيمتwwه تسwwاوي 4في الرسwwمة السwwابقة ،فعليwwك أن تبwwدأ من عقwwدة
الجذر التي تحتوي عىل المفتاح ،8وألن المفتاح المطلوب أقلَّ من ،8فسwwتذهب إىل اليسwwار ،وألنwwه أكwwبر من ،3
فستذهب إىل اليمين ،وألنه أقل من ،6فستذهب إىل اليسار ،ثم ستعثر عىل المفتاح الذي تبحث عنه.
يتناسب عدد الموازنات المطلوبة في العموم مع ارتفاع الشجرة وليس مع عدد المفاتيح الموجودة فيها.
بارتفwاع قصwير ٍ
ٍ ما الذي نستنتجه من ذلك بخصوص العالقة بين ارتفاع الشwجرة hوعwدد العقwد n؟ إذا بwدأنا
إذا كان ارتفاع الشجرة hيساوي ،1فإن عدد العقد nضمن تلك الشجرة يساوي .1 •
وإذا كان ارتفاع الشجرة hيساوي ،4يُمكِننا أن نضيف ما يصل إىل ثماني عق ٍد أخwwرى ،وبالتwwالي ،يصwwبح •
110
هياكل البيانات للمبرمجين الواجهة TreeMap
n = 2h - 1
بتطبيق لوغاريتم األساس 2عىل طرفي المعادلة السابقة ،نحصل عىل التالي:
log2 n ≈ h
بحث ثنائ ّيwwةٍ مwwع ) .log(nيُعّ wد ذلwwك صwwحيحًا سwwوا ٌء
ٍ مفتاح ضمن شwwجر ِة
ٍ البحث عن
ِ وبالتالي ،يتناسب زمنُ
أكانت الشجر ُة ممتلئة كلّ ًّيا أم جزئ ًيا ،ولكنه ليس صحيحًا في المطلق ،وهو ما سنراه الح ًقا.
يُطلَق عىل الخوارزميات التي تَستغرِق زمنًا يتناسب مwع ) log(nاسwم "خوارزميwwة لوغاريتميwة" ،وتنتمي إىل
12.3تمرين 10
بحث ثنائ ّيةٍ .
ٍ ستكتب في هذا التمرين تنفي ًذا للواجهة Mapباستخدام شجر ِة
يحتفظ متغ ّيرُ النسخةِ sizeبعwدد المفwاتيح بينمwwا يحتwوي rootعىل مرجwwع referenceيشwير إىل عقwwدة
ً
فارغwwة ،يحتwwوي rootعىل القيمwwة nullوتكwwون قيمwwة size الخاصwwةِ بالشwwجرة .إذا كwwانت الشwwجرة
ّ الجwwذر
ً
مساوية للصفر.
111
هياكل البيانات للمبرمجين الواجهة TreeMap
تحتوي كل عقwwد ٍة عىل زوج مفتwwاح/قيمwwة وعىل مراجwwعَ تشwwير إىل العقwwد األبنwwاء leftو .rightقwwد تكwwون
قد تظن للوهلة األوىل أن التابع clearيستغرق زمنًا ثابتًا ،ولكن فكر بالتالي :عندما تُضبَط قيمwwة rootإىل
.antقد تفشل بعض ن ِّفذ األمر ant buildلتصريف ملفات الشيفرة ،ثم ن ِّفذ األمر MyTreeMapTest
االختبارات ألنّ هناك بعض التوابع التي ينبغي عليك إكمالها أواًل .
112
هياكل البيانات للمبرمجين الواجهة TreeMap
و َّفرنا تصو ًرا مبدئ ًيا للتابعين getو .containsKeyيَستخدِم كالهما التابع ُ findNode
المعرَّف باسwwتخدام
)"@SuppressWarnings("unchecked
;Comparable<? super K> k = (Comparable<? super K>) target
يشير المعامل targetإىل المفتاح الذي نبحث عنه .إذا كwwانت قيمwwة targetتسwاوي ،nullيُبلِّغ التwwابع
اعتراض .exceptionفي الواقع ،بإمكان بعض تنفيwwذات الواجهwwة Mapمعالجwwة الحwwاالت الwwتي
ٍ findNodeعن
ي من أصwwنافه األعىل
كwwائن من النwwوع Kأو أ ٍّ
ٍ > Comparable<? super Kلكي يُصِ wwبح قwwاباًل للموازنwwة مwwع
َ
احتراف التعامل مع نظام األنواع في لغة جافا ،ف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ب زمن
ً
مبدئية تعالج الحاالت البسيطة فقط: واآلن ،أكمل متنَ التابع .putو َّفرنا له شيفر ًة
إذا حاولت اسwwتخدَام القيمwwة الفارغwwة nullكمفتwwاح ،سُ wيبلّغ putعن اعwwتراض .إذا كwwانت الشwwجرة فارغً w
wة، ٍ
المعرَّف فيها. ً
جديدة ،ويُه ِّيُئ المتغير ُ root ً
عقدة نشئ التابع put
س ُي ِ
أكمل متنَ التابع putHelperواجعله يبحث ضمن الشجرة وف ًقا لما يلي:
.1إذا كان المفتاح keyموجودًا بالفعل ضمن الشجرة ،عليه أن يَستبدِل القيمة الجديwدة بالقيمwة القديمwwة،
ثم يعيدها.
ً
جديwدة ،ثم يضwيفها إىل المكwان نشwئ عقً w
wدة .2إذا لم يكن المفتاح keyموجwودًا في الشwجرة ،فعليwwه أن يُ ِ
114
هياكل البيانات للمبرمجين الواجهة TreeMap
ينبغي أن يَستغرِق التابع putزمنًا يتناسب مع ارتفاع الشwwجرة hوليس مwwع عwwدد العناصwwر .nسwwيكون من
ً
واحدة فقط ،ولكن إذا كان البحث فيها مwرّتين أسwwهلَ بالنسwwبة لwwك ،فال بwwأس. األفضل لو بحثت في الشجرة ً
مرة
وأخي ًرا ،عليك أن تُكمِ ل متن التابع .keySetيعيد ذلك التابع -وف ًقا لـللتوثيق (باللغwwة اإلنجليزية) -قيمwwة من
ولكنه ال يحافظ عىل ترتيب المفاتيح .في المقابwwل ،يتwwو َّفر التنفيwwذ LinkedHashSetالwwذي يحافwwظ عىل تwwرتيب
المفاتيح.
ً
قيمة من النوع LinkedHashSetويعيدها كما يلي: نشئ التابع keySet
يُ ِ
ي.
بترتيب تصاعد ّ
ٍ ُ
يضيف المفاتيح من الشجرة إىل المجموعة set عليك أن تُكمِ ل هذا التابع بحيث تجعلُه
ُ
قراءة بعض تابع مساع ٍد ،وقد ترغب بجعله تعاوديًّا ،recursiveكما قد يساعدك عىل الحلِّ
ٍ قد تحتاج إىل كتابةٍ
ينبغي أن تنجح جميع االختبارات بعد أن تنتهي من إكمال هذا التابع .س wنَعرِض حwwل هwwذا التمwwرين ونفحص
115
.13شجرة البحث الثنائي Binary Search Tree
13.1الصنف MyTreeMap
و َّفرنا في الفصل المشار إليها تصّ wو ًرا مبwwدئ ًيا للصwwنف ،MyTreeMapوتركنwwا للقwwارئ مهمwwة إكمwwال توابعwwه.
الم ِّ
صرف // هذا ما ُيسعد ُ
)"@SuppressWarnings("unchecked
;Comparable<? super K> k = (Comparable<? super K>) target
;node = node.left
)else if (cmp > 0
;node = node.right
else
;return node
}
;return null
}
المعدّل .privateيُمثِل المعامل targetالمفتاحَ الذي نبحث عنه .كنا قwwد شwwرحنا الجwwزء األول من
باستخدام ُ
هذا التابع في الفصل المشار إليه:
يُجرَى البحث عىل النحو التالي :نضwwبط متغwwير الحلقwwة nodeإىل عقwwدة الجwwذر ،وفي كwwلّ تكwwرارٍ ،نwwوازن بين
المفتاح targetوقيمة .node.keyإذا كان targetأصغ َر من مفتاح العقدة الحال ّية ،سننتقل إىل عقدة االبن
اليسرى ،أما إذا كان أكبرَ منه ،سننتقل إىل عقدة االبن اليمنى ،وإذا كانا متساويين ،سنعيد العقدة الحال ّية.
إذا وصلنا إىل قاع الشجرة دون أن نعثر عىل المفتاح المطلوب ،فهذا يَعنِي أنه غير موجود فيها ،وسwنعيد في
الشجرة وليس مع عدد العقد الموجودة فيها؛ وذلك ألننا غير مضطرّين للبحث في كامل الشwwجرة ،ولكن بالنسwwبة
للتابع ،containsValueفإننا سنضطرّ للبحث بالقيم وليس المفwwاتيح ،وألن خاصwwية BSTال تُطبَّق عىل القيم،
117
هياكل البيانات للمبرمجين شجرة البحث الثنائي Binary Search Tree
ً
مساوية للقيمة الفارغة ،null تفحص تعليمة ifاألوىل الحالة األساسية للتعاود :إذا كانت قيمة node •
فإن التابع وصل إىل قاع الشجرة دون إيجاد القيمwwة المطلوبwwة ،targetويعيwwد عنwwدها القيمwwة .false
انتبه ،يعني ذلك أن القيمة targetغير موجود ٍة في واح ٍد فقط من مسwwارات الشwwجرة ال في مسwwارات
تفحص تعليمة ifالثانية ما إذا كان التابع قد وجد القيمة المطلوبة ،وفي تلك الحالة ،يعيد التابع القيمة •
تَستدعِ ي الحالة الشرطية الثالثة التابعَ تعاوديًwwا لكي يبحث عن نفس القيمwwة ،أي ،targetفي الشwwجرة •
الفرعية اليسرى .إذا وجدها فيها ،فإنwwه يعيwwد القيمwwة trueمباشً w
wرة دون أن يحwwاول البحث في الشwwجرة
تبحث الحالة الشرطية الرابعة عن القيمة المطلوبة في الشwwجرة الفرعيwwة اليمwwنى .إذا وجwwدها فيهwwا ،فإنwwه •
يمرّ التابع السابق عبر كل عقد ٍة من الشجرة ،ولهذا ،يَستغرِق زمنًا يتناسب مع عدد العقد.
118
هياكل البيانات للمبرمجين Binary Search Tree شجرة البحث الثنائي
ونwwدما ال يكwwة عنww والثاني،ةww وينبغي عندها أن يَستب ِدله ويعيد القيمة القديم،عطى موجودًا في الشجرة بالفعل
َ الم
ُ
.نشئ عقد ًة جديد ًة ثم يضعها في المكان الصحيح
ِ ُ وعندها ينبغي أن ي،موجودًا
: انظر إىل شيفرته فيما يلي.putHelper وكان المطلوب هو إكمال متن التابع
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ود
ً
فارغة ،نَستدعِ ي التابع تعاوديًّا للبحث في الشجرة الفرعية اليسرى. إن لم تكن الشجرة •
في المقابل ،إذا تح ّقق الشرط ،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ة Mapالمفwwاتيح وف ًقwwا أل ّ
ي تwwرتيب ،ولكن ألن هwwذا
120
هياكل البيانات للمبرمجين شجرة البحث الثنائي Binary Search Tree
ً
قيمة من النوع LinkedHashSetفي التابع .keySetيُن ِّفذ ذلwwك النwwوع الواجهwwة Set كما ترى فقد أنشأنا
ويحافظ عىل ترتيب العناصر (بخالف معظم تنفيذات تلك الواجهwwة) .نَسwwتدعِ ي بعwwد ذلwwك التwwابع addInOrder
للتنقل في الشجرة.
َّ
تتوقع -للتنقwwل في الشwwجرة يشير المعامل األول nodeمبدئ ًّيا إىل جذر الشجرة ،ونَستخدِمه -كما يُفترَض أن
ي شي ٍء ٌ
فارغة ،وعندها يعود التابع دون إضافة أ ّ َ
الفرعية َ
الشجرة ً
فارغة ،يَعنِي ذلك أن إذا كانت العقدة node
ً
فارغة ،نقوم بما يلي: إىل المجموعة ،setأما إذا لم تكن
.2نضيف .node.key
ت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حنا في
نفترض اآلن أن التابعين 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ف الصwwنف UUIDضwwمن حزمwwة ،java.utilويُمكِنwwه أن يُولِّد ُمعwwرِّف هويwwةٍ فريwwدًا عموم ًيwwا
َ
ذات فائد ٍة كبwwير ٍة في مخت ِلwwف أنwwواع بأسلوب عشوائي .تُع ّد تلك ُ
المعرّفات ٍ universally unique identifier
التطبيقات ،ولكننا سنَستخدِمها في هذا المثال كطريقةٍ سهلةٍ لتوليد سالسلَ نص ّيةٍ عشوائ ّيةٍ .
الخرج التالي:
أضفنا ً
أيضا قيمة اللوغاريتم لألساس 2إىل الخريطة لكي نرى طول الشجرة إذا كانت ممتلئة .تشwwير النتيجwwة
122
هياكل البيانات للمبرمجين شجرة البحث الثنائي Binary Search Tree
في الواقع ،ارتفاع شجرة السالسل النصية العشوائية الفعلي هو ،33وهو أكبر من الحد األدنى النظwwري ولكن
عقدة ،سنضطرّ إلجwwراء 33موازنً w
wة ،أي ً مفتاح ضمن تجميعةٍ مكونةٍ من 16,384 ليس بشكل كبير .لكي نعثر عىل
ٍ
أسر ع بـً 500
مرة تقريبًا من البحث الخطي .linear search
يُع ّد هذا األداء نموذج ًيا للسالسل النص ّية العشوائ ّية والمفاتيح األخرى التي ال تضwwاف وف ًقwwا أل ّ
ي تwwرتيب .رغم
أي أقل بكثير من ،nحيث تنمو قيمة ) log(nببط ٍء مع زيwwادة قيمwwة nلدرجwwةٍ يَصwwعُ ب معهwwا التميwwيز بين الwwزمن
َ
سبعة أضعاف زمن التشغيل في الحالة السwwابقة .إذا كنت تتسwwاءل عن يتجاوز زمن التشغيل في هذه الحالة
النهائي .16384
ّ ً
نظرة عىل ارتفاع الشجر ِة السبب ،فألق
123
هياكل البيانات للمبرمجين شجرة البحث الثنائي Binary Search Tree
عمل بها التابع ،putفقد تفهم مwwا يحwwدث :ففي كwwل مwwر ٍة نضwwيف فيهwwا
إذا أمعنت النظر في الطريقة التي يَ َ
دائم wا الختيwwار الشwwجرة
ً مفتاحً ا جديدًا ،فإنه يكون أكبر من جميع المفاتيح الموجودة في الشجرة ،وبالتالي ،نضwwطرّ
دائما العقدة الجديدة كعقدة ابن يمنى للعقدة الواقعة عىل أقصى اليمين .نحصل بwwذلك
ً الفرعية اليمنى ،ونضيف
يتناس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دد
دائما.
ً يُمكِننا أن نتجنّب إضافة المفاتيح إىل الخريطة بالترتيب ،ولكن هذا الحل ليس ممكنًا •
ما إذا كانت الشجرة قد أصبحت غير متزنة ،وعندها ،يعيد ترتيب العق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ل في
صفحات أخرى.
ٍ روابط إىل
الفهرسة :indexingوتن ّف ُذ من خالل هيكل بيانات data structureبإمكانه البحث عن كلمwwة والعثwwور •
البحث.
وإذا كنت قد أكملت تمرين الفصل السابع كل الطwwرق تwwؤدي إىل روما ،فقwwد ن َّفذت بالفعwwل زاح ًفwwا يَت ِبwwع أول
بالترتيب.
ً
فرصة أكبر التخwاذ القwرارات المتعلقwwة بالتصwwميم. شيفرة مبدئ ّي ًة أقصر في هذه التمارين ،وسنعطيك
ً سنُو ِّفر
وتجدر اإلشارة إىل أن هذه التمارين ذات نهايات مفتوحة ،أي سنطرح عليك فقط بعض األهداف البسwwيطة الwwتي
المفهرِس.
واآلن ،سنبدأ بالنسخة الجديدة من ُ
يُخزَّن هيكال البيانات في ذاكرة التطبيق ،ولذا يتالشيان بمجرد انتهاء البرنامج .توص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عاد ًة ما تكون عمليتا قراءة هياكل البيانات الضخمة (مثل مفهرس الويب) وكتابتها عمليتين بطيئتين.
نحو غير متوقع (نتيجة النقطاع الكهرباء مثاًل ) ،سنفقد جميع التغييرات التي
ٍ .3إذا انتهى برنامجٌ معينٌ عىل
وهناك طريقة أخرى لحفظ البيانات وهي قواعد البيانات .إذ تُع ّد قواع ُد البيانات البديلَ األفضwwلَ ،فهي تُwwو ِّفر
ٌ
قادرة عىل قراءة جز ٍء من قاعدة البيانات أو كتابته دون الحاجwwة إىل قwwراءة قاعwwدة ً
مستمرة ،كما أنها تخزين َ
مساحة
ٍ
البيانات أو كتابتها بالكامل.
تتو َّفر الكثير من أنواع نظم إدارة قواعد البيانات ،DBMSويتمتع ك ٌّ
ل منها بإمكانيات مختلفة .ويُمكِنك قwwراءة
مقارنة بين أنظمة إدارة قواعد البيانات العالقية واالطالع عىل سلسلة تصميم قواعد البيانات.
تُو ِّفر قاعدة بيانات - Redisالتي سنَستخدِمها في هwwذا التمwwرين -هياكwwل بيانwwات مسwwتمرة مشً w
wابهة لهياكwwل
127
هياكل البيانات للمبرمجين حفظ البيانات عبر Redis
تُع ّد Redisقاعدة بيانات من نوع زوج مفتاح/قيمة ،ويَعwwني ذلwwك أن هياكwwل البيانwwات (القيم) الwwتي تُخزِّنهwwا
تكون ُمعرَّ ً
فة بواسطة سالسل نصية فريدة (مفاتيح) .تلعب المفاتيح في قاعدة بيانwwات Redisنفس الwwدور الwwذي
ً
أمثلة عىل ذلك الح ًقا. تلعبه المراجع referencesفي لغة جافا ،أي أنّها تُعرِّف هوية الكائنات .سنرى
قاموس عن بعد ،"REmote DIctionary Serverولنتمكن من استخدامها ،علينا أن نُشِّ wغل خwادم Redisفي
مكان ما ثم ن َ ِ
تصل به عبر عميل .Redisمن الممكن إعداد ذلك الخادم بأكثرَ من طريقةٍ ،كما يُمكِننwwا االختيwwار من ٍ
بين العديد من برامج العمالء ،وسنَستخدِم في هذا التمرين ما يلي:
التمرين.
أنشئ حسابًا عىل موقع ،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ة ُ
من أحرف وأعدا ٍد في المنتصف .ستحتاج تلك المعلومات في الخطوة التالية.
128
هياكل البيانات للمبرمجين حفظ البيانات عبر Redis
:JedisMaker.javaيحتwwوي عىل بعض األمثلwwة عىل االتصwwال بخwwادم Redisوتشwwغيل بعض توابwwع •
.Jedis
:WikiFetcher.javaيحتوي عىل شيفر ٍة تقرأ صwwفحات إنwwترنت وتُحلِّلهwwا باسwwتخدام مكتبwwة .jsoup •
ستجد ً
أيضا الملفات التالية التي كتبناها في نفس تلك التمارين:
ً
خريطة تربُط كلمات البحث بعدد مرات حدوثها. :TermCounter.javaيُمثِل •
إذا تمكَّنت من كتابة نسخك الخاصة من تلك الملفwwات ،يُمكِنwwك اسwwتخدامها لهwwذا التمwwرين .إذا لم تكن قwwد
أكملت تلك التمارين أو أكملتها ولكنك غير متأ ّكد ممwwا إذا كwwانت تَ َ
عم wل عىل النحwwو الصwwحيح ،يُمكِنwwك أن تَ َ
نسwخ
واآلن ،ستكون خطوتك األوىل هي استخدام عميل Jedisلالتصال بخادم Redisالخاص بك .يُ ِّ
وضح الصwwنف
wجل ٍّ
ملف ،ثم يتصwwل بwwه ،ويُسِ w RedisMaker.javaطريقة القيام بذلك :عليه أواًل أن يقرأ معلومات الخادم من
دخوله باستخدام كلمة المرور ،وأخي ًرا ،يُعيد كائنًا من النوع Jedisالذي يُمكِن استخدامه لتنفيذ عمليات .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 بالتابع
// 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
يَعرِض المثال أنواع البيانات والتوابع التي ستحتاج إليها غالبًا في هذا التمرين .ينبغي أن تحصل عىل الخرج
تَ َ
عمل Redisكخريطةٍ تربط مفاتيحَ (سالسل نص ّية) بقيم .قد تنتمي تلك القيم إىل مجموعwwة أنwwواع مختلفwwة
من البيانات .يُع ّد النوع stringواحدًا من أبسط األنواع التي تُو ِّفرهwwا قاعwwدة بيانwwات .Redisالحwwظ أننwwا سwwنكتب
سنَستخدِم التابع jedis.setإلضافة سلسلةٍ نص ّيةٍ من النوع stringإىل قاعwدة البيانwات .قwد تجwد ذلwك
;)"jedis.set("mykey", "myvalue
;)"String value = jedis.get("mykey
wكل مشwwابهٍ للنwwوع > Set<Stringفي جافwwا .إذا أردت أن تُو ِّفر Redisهيكل البيانات setالwwذي يَ َ
عم wل بشٍ w
حwا يُحwدّد هويwة تلwك المجموعwة ،ثم اِسwتخدِم التwابع
تضيف عنص ًرا جديدًا إىل مجموعةٍ من النوع ،setاختر مفتا ً
jedis.saddكما يلي:
الحِ ظ أنه ليس من الضروري إنشاء المجموعة بخطwwو ٍة منفصwwلةٍ ،حيث تُنشwwؤها Redisإن لم تكن موجwwود ًة.
ً
مجموعة من النوع setاسمها mysetتحتوي عىل ثالثة عناصر. تُ ِ
نشئ Redisفي هذا المثال
يفحص التابع jedis.sismemberما إذا كان عنصر معين موجودًا في مجموعة من النwwوع .setتَسwwتغرِق
131
هياكل البيانات للمبرمجين حفظ البيانات عبر Redis
أخيرًا ،تُو ِّفر Redisالهيكل hashالذي يشwwبه النwوع > Map<String, Stringفي جافwwا .يضwwيف التwwابع
تنتمي المفاتيح والقيم إىل النوع ،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ات Redisبعض التوابwwع الخاصwwة الwwتي تعامwwل القيم وكأنهwwا أعwwداد مثwwل التwwابع .hincrbyانظwwر إىل
المثال التالي:
يسترجع هذا التابع المفتاح ،myhashويحصل عىل القيمة الحالية المرتبطة بالحقل ( word2أو عىل الصwwفر
إذا لم يكن الحقل موجودًا) ،ثم يزيدها بمقدار ،1ويكتبها مر ًة أخرى في الجدول.
132
هياكل البيانات للمبرمجين حفظ البيانات عبر Redis
14.5تمرين 11
فهرس قادرٍ عىل تخزين النتwwائج في قاعwwدة
ٍ بوصولك إىل هنا ،أصبح لديك كل المعلومات الضرورية إلنشاء ُم
بيانات .Redis
واآلن ،ن ِّفذ األمر .ant JedisIndexTestستفشل بعض االختبارات ألنه ما يزال أمامنا بعض العمل.
ُمحدّد مwwوارد URLباإلضwwافة إىل كwwائن من النwwوع Elementsيحتwwوي عىل عناصwwر الصwwفحة المطلwwوب
فهرستها.
محدد موار َد يحتوي عىل تلك الكلمة بعدد مرات ظهورها في تلك الصفحة.
;")"http://en.wikipedia.org/wiki/Java_(programming_language
;)Elements paragraphs = wf.readWikipedia(url1
إذا بحثن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بكلمة " ،"URLSet:وبالتالي ،لكي نحصل عىل محددات الموارد الموحwدة الwwتي تحتwwوي
يُمثِل النوع TermCounterجدواًل من النوع hashفي قاعwwدة بيانwwات .Redisيرب ُwط هwwذا الجwدول كwwل •
نبحث فيها.
14.6المزيد من االقرتاحات
أصبح لديك اآلن كل المعلومات الضرورية لحل التمرين ،لwwذا يُمكِنwwك أن تبwwدأ اآلن إذا أردت ،ولكن مwwا يwwزال
ً
مساعدة أقلّ في هwwذا التمwwرين ،ونwwترك لwwك حريّwwة أكwwبر في اتخwwاذ بعض القwwرارات المتعلقwwة سنو ِّفر لك •
البيانات قد تتغير في كل مر ٍة تُش ِّغل فيها البرنامج .فإذا تسبَّبت بخطٍأ في قاعدة البيانات ،سيكون عليك
إصالحه أو البدء من جديد .ولكي نُبقِي األمwwور تحت السwwيطرة ،و ّفرنwwا لwwك التوابwwع deleteURLSetsو
المطلوب ،ثم ير ّد برسالة .إذا ن َّفذت الكثير من العمليات الصغيرة ،فستحتاج إىل وقت طويل لمعالجتها،
ولهwwذا ،من األفضwwل تجميwwع متتاليwwة من العمليwwات ضwwمن معاملwwة واحwwدة من النwwوع Transaction
لتحسين األداء.
عىل سبيل المثال ،انظر إىل تلك النسخة البسيطة من التابع :deleteAllKeys
134
هياكل البيانات للمبرمجين حفظ البيانات عبر Redis
صفحات ،فقد يَستغرِق تنفيwwذ ذلwwك التwwابع وق ًتwwا طwwوياًل .بwwداًل من ذلwwك ،يُمكِنwwك أن
ٍ المفهرِس يحتوي عىل بضع
ُ
كائن من النوع Transactionعىل النحو التالي:
ٍ تُسرِّ ع تلك العملية باستخدام
يعيد التابع jedis.multiكائنًا من النوع .Transactionيُwwو ِّفر هwwذا الكwwائن جميwwع التوابwwع المتاحwwة في
المخزَّنة إىل الخادم في نفس الوقت ،وهو ما يكون أسر ع عاد ًة.
العمليات ُ
ِ جميعَ
ال تتابع القراءة قبل أن تُش ِّغل شيفرة االختبار ،وتُج wرِّب بعض أوامwwر Redisالبسwwيطة ،وتكتب بعض التوابwwع
إذا لم تتمكّن من متابعة الحل فعاًل ،إليك بعض التوابع التي قد ترغب في العمل عليها:
**/
ً
موحدا إلى المجموعة الخاصة بكلمة * أضف محدد موارد
*/
135
هياكل البيانات للمبرمجين حفظ البيانات عبر Redis
**/
ابحث عن كلمة وأعد مجموعة ُم ِّ
حددات الموارد الموحدة التي تحتوي عليها *
*/
}{ )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 اإلنترنت ،ثم سنبني
بحث كائنٌ من النوع URLSetهو عبwwار ٌة عن مجموعwwة Setفي قاعwwدة بيانwwات Redis
ٍ س ُيقابِل كلَّ كلمة •
تحتوي عىل ُمحدِّدات الموارد الموحدة URLsالتي تحتوي عىل تلك الكلمة.
س ُيقابِل كل ُمحدّد موارد موحد كائنًا من النوع TermCounterيُم ّثwwل جwwدول Hashفي قاعwwدة بيانwwات •
يُمكِنwwك مراجعwwة أنwwواع البيانwwات الwwتي ناقشwwناها في الفصwwل المشwwار إليwwه ،كمwwا يُمكِنwwك قwwراءة المزيwwد عن
ُ
الصنف المذكور التابع ،termCounterKeyوالذي يستقبل ُمح wدّد مwwوارد موح wدًا ويعيwwد مفتwwاح كما يُعرِّف
يَستق ِبل تابع الفهرسة indexPageمحد َد موارد موح wدًا وكائنًwwا من النwwوع Elementsيحتwwوي عىل شwwجرة
.1ن ُ ِ
نشئ كائنًا من النوع TermCounterيُمثِل محتويات الصفحة باستخدام شيفرة تمرين الفصل المشwwار
إليه باألعىل.
تُ ِّ
وضح الشيفرة التالية طريقة إضافة كائنات النوع TermCounterإىل قاعدة بيانات :Redis
ً
مسبقا ،احذف الجدول القديم // إذا كانت الصفحة مفهرسة
;)t.del(hashname
ً
جديدا إلى الفهرس // ً
وعنصرا ً
جديدا في كائن الصنف TermCounter ً
مدخلا أضف
138
هياكل البيانات للمبرمجين الزحف عىل ويكيبيديا
ً
معاملة من النوع Transactionلتجميع العمليات ،ثم يرسلها جميعً wا إىل الخwwادم عىل يَستخدم هذا التابع
خطوة واحدة .تُع ّد تلك الطريقة أسر ع بكثير من إرسال متتاليةٍ من العمليات الصغيرة.
يمرّ التابع عبر العناصر الموجودة في كائن الصنف ،TermCounterويُن ِّفذ التالي من أجل كل عنصر ٍ منها:
كائن من النوع - TermCounterأو ينشئه إن لم يجده -في قاعدة بيانات ،Redisثم يضwwيف
ٍ .1يبحث عن
كائن من النوع - URLSetأو ينشئه إن لم يجwwده -في قاعwwدة بيانwwات ،Redisثم يضwwيف إليwwه
ٍ .2يبحث عن
إذا كنا قد فه َرسنا تلك الصفحة من قبل ،علينا أن نحذف كائن الصنف TermCounterالقديم الذي يمثلهwwا
139
هياكل البيانات للمبرمجين الزحف عىل ويكيبيديا
ً
مجموعة من النوع Setتحتوي عىل محwwددات المwwوارد الموحwwدة بحث ويعيد
ٍ :getURLsيَستق ِبل كلمة •
:getCountيَستق ِبل محدد موارد موحدًا URIوكلمwwة بحث ،ويعيwwد عwwدد مwwرات ظهwwور الكلمwwة بمحwwدد •
الممرَّر.
الموارد ُ
المفهرِس.
صم ّمنا بها ُ
ّ ً
نتيجة للطريقة التي تَ َ
عمل تلك التوابع بكفاء ٍة
الوقت الذي سيستغرقه البحث عن كلمةٍ معينةٍ ؟ فكر قبل أن تكمل القراءة.
سنُن ِّفذ التابع getCountsللبحث عن كلمةٍ ،يُن ِّفذ ذلك التابع ما يلي:
ً
خريطة من النوع .HashMap نشئ
.1يُ ِ
يستغرق التابع getURLsزمنًا يتناسب مع عدد محددات الموارد الموحدة الwwتي تحتwwوي عىل كلمwwة البحث.
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ف
تكمل القراءة.
ً
صفحة ،فإننا نمرّ عبر شجرة ،DOMونعwwثر عىل الكائنwwات الwwتي تنتمي إىل النwwوع ،TextNode لكي نُفهرِّس
في الصفحة.
خط ًّيا مع عدد كلمات البحث .بعد ذلك ،علينا أن نُن ِّفذ التالي من أجل كل كلمة:
زمنًا يتناسب ّ
wائن من النwwوع
wوب إلضwwافة كٍ w
الكلي المطلُ w
ُّ تَستغرِق العمليتان السابقتان زمنًا ثاب ًتwwا ،وبالتwwالي ،يكwwون الwwزمن
نستخلص مما س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ة .يُع ّ 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ف يُن ِّفذ •
مكدسا ،stackأي تتبع أسلوب "الداخل آخرًا ،يخwwرج أواًل " ،LIFOفwwإن الزاحwwف يُن ِّفذ
ً إذا كانت التجميعة •
142
هياكل البيانات للمبرمجين الزحف عىل ويكيبيديا
ات لعناصwر التجميعwwة .عىل سwwبيل المثwwال ،قwد نwرغب في إعطwاء أولويwة أعىل
من الممكن تحديد أولويّ ٍ •
15.5تمرين 12
واآلن ،عليك كتابة الزاحف ،ستجد ملفات الشيفرة التالية الخاصة بالتمرين في مستودع الكتاب:
ستحتاج ً
أيضا إىل األصناف المساعدة التالية التي استخدَمناها في تمارين الفصول السابقة:
JedisMaker.java •
WikiFetcher.java •
TermCounter.java •
WikiNodeIterable.java •
ٍّ
ملف يحتوي عىل بيانات خادم Redisقبل تنفيwwذ الصwwنف .JedisMakerإذا أكملت سيتع ّين عليك توفير
تمرين الفصل المشار إليه باألعىل ،فقد جهّ زت كل شيء بالفعل ،أما إذا لم تكمله ،فستجد التعليمwwات الضwwرورية
ن ِّفذ األمر ant buildلتصريف ملفات الشيفرة ،ثم ن ِّفذ األمر ant JedisMakerلكي تتأ ّكد من أنه مهيا ٌ
واآلن ،ن ِّفذ األمر .ant WikiCrawlerTestستجد أن االختبارات قد فشwلت؛ ألن مwا يwزال عليwك إتمwام
143
هياكل البيانات للمبرمجين الزحف عىل ويكيبيديا
:queueعبارة عن قائمة من النوع .LinkedListيُفترَض أن تُخزَّن فيهwwا كwwلُّ محwwددات المwwوارد الwwتي •
:wfعبارة عن كائن من النوع .WikiFetcherعليك أن تَستخ ِدمه لقراءة صفحات اإلنترنت وتحليلها. •
ً
مس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ابع. •
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ان
َ
ستخدم لقراءة الصفحات من موقع ويكيبيديا //ُي
;)(final static WikiFetcher wf = new WikiFetcher
}
الحِ ظ أن الرتل ُ queueمن َّفذ باستخدام قائمةٍ من النwwوع ،LinkedListوبالتwwالي ،تسwwتغرق عمليwwة إضwwافة
ً
قائمwة من النwوع LinkedListإىل متغwير من العناصر إىل نهايته -وحذفها من بدايته -زمنًا ثابتًا ،وألننwا أسwندنا
;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ابع
كتبنا نسختين من ذلك التابع بمعامالت parametersمختلفة :تَستق ِبل األوىل كائنًا من النوع Elements
يتضمن شجرة DOMواحد ًة لكل فقرة ،بينما تَستق ِبل الثانية كائنًا من النوع Elementيُمثِل فقرة واحدة.
َّ
تمرّ النسخة األوىل عبر الفقرات ،في حين تُن ِّفذ النسخة الثانية العمل الفعلي.
{ ))"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طريقة الستقبال كلمات البحث وإعادة الصفحات التي
يُطلَق عىل تلك العمليات وما يشابهها اسم استرجاع المعلومات .Information retrieval
ً
بسيطة من الخطوة رقم ،2وسنُر ِّكز في هذا التمwwرين عىل الخطwwوتين 3و .4ق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يات
149
هياكل البيانات للمبرمجين البحث المنطقي Boolean Search
يُمثِل التقاطع بين s1و s2مجموعة الصفحات التي تحتوي عىل الكلمتين ""java •
يُمثِل االتحاد بين s1و s2مجموعة الصفحات التي تحتوي عىل كلمة " "javaأو كلمة •
"."programming
يُمثِل الفرق بين s1و s3مجموعة الصفحات التي تحتوي عىل كلمة " "javaوال تحتوي عىل كلمة •
"."indonesia
16.4تمرين 13
ستجد ملفات شيفرة هذا التمرين في مستودع الكتاب:
:WikiSearch.javaيُعرِّف كائنًا يحتوي عىل نتائج البحث ويُطبِّق العمليات عليها. •
ستجد ً
أيضا بعض األصناف المساعدة التي استخدَمناها من قبل هذا الكتاب.
يربط ُم ّ
حددات الموارد التي تحتوي على الكلمة بدرجة الارتباط //
;private Map<String, Integer> map
150
هياكل البيانات للمبرمجين البحث المنطقي Boolean Search
يحتوي كائن النوع 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فال ب ُ ّد أن تو ِّفر بيانات الخادم في ملف ،كما
ن ِّفذ األمwر ant JedisMakerلكي تتأ ّكwwد من قدرتwه عىل االتصwwال بخwادم ،Redisثم ن ِّفذ WikiSearch
""java •
""programming •
لن تكون النتائج ُمرتَّبة في البداية ألن التابع WikiSearch.sortما يزال غير مكتمل.
151
هياكل البيانات للمبرمجين البحث المنطقي Boolean Search
أكمل متن التابع sortلكي تُص ِبح النتائج ُمرتَّبة تصاعديًا بحسب درجة االرتبwاط .يُمكِنwwك االسwتعانة بالتwابع
المعرَّف بالنوع java.util.Collectionsحيث يُمكِنه ترتيب أي نوع قائمwwة .Listيُمِ كنwwك االطالع
ُ sort
عىل توثيق النوع .List
ً
قائمة وتُرتِّب عناصرها باستخدام التابع ،compareToولwwذلك ينبغي أن نسخة أحادية المعامل تَستق ِبل •
سنتحدث عن الواجهتين Comparableو Comparatorفي القسم التالي إن لم تكن عىل معرفة بهما.
تحتوي كائنات الصنف Cardعىل الحقلين rankو suitمن النوع العددي الصحيح .يُن ِّفذ الصwwنف Card
الواجهة > Comparable<Cardمما يَعنِي أنه بالضرورة يُو ِّفر تنفي ًذا للتابع :compareTo
152
هياكل البيانات للمبرمجين البحث المنطقي Boolean Search
تشير بصمة التابع compareToإىل أن عليه أن يعيد عددًا سالبًا إذا كان thisأقل من ،thatوعددًا موجبًا
إذا اسwwتخدمت نسwwخة التwwابع Collections.sortأحاديwwة المعامwwل ،فإنهwwا بwwدورها تَسwwتدعِ ي التwwابع
المعرَّف ضمن العناصر لكي تتمكّن من ترتيبها .عىل سبيل المثال ،تُ ِ
نشwئ الشwwيفرة التاليwwة قائمwwة ُ compareTo
تحتوي عىل 52بطاقة:
;)Collections.sort(cards
تُرتِّب تلك النسخة من التابع sortالعناصر وف ًقا لما يُطلَق عليwwه "الwwترتيب الطwwبيعي" ألن الwwترتيب ُمحwدّد
أيضا أن نستعين بكائن من النوع Comparatorلكي نَفرِض نو ًعا مختل ًفا من الwwترتيب.
في المقابل ،يُمكِننا ً
األص عىل أنّها البطاقة األكبر ضمن مجموعة بطاقات اللعب .انظر إىل شيفرة ذلك النوع:
ّ
153
هياكل البيانات للمبرمجين Boolean Search البحث المنطقي
ثم،وب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ة
ستجد شيفرة هذا القسم في الملف 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ل 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
واغزُ "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ك
سنُحلِّل في هذا الفصل خوارزمية الترتيب باإلدراج ،insertion sortوسwwنن ِّفذ خوارزميwwة الwwترتيب بالwwدمج،
تملك بعض الميزات المتعلقة بتحرير الذاكرة كما سنرى الح ًقا.
فهي تحتوي عىل شيفرة وهمية وأمثلة متحركة .وبعدما تفهم فكرتها العامة يمكنك متابعة القراءة هنا.
تَعرِض الشيفرة التالية تنفي ًذا بلغة جافا لخوارزمية الترتيب باإلدراج:
كحاو لخوارزمي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
يحتوي التابع insertionSortعىل حلقتين متداخلتين ،nested loopsولذلك ،قد تظن أن زمن تنفيذه
تربيعي ،وهذا صحيحٌ في تلك الحالة ،ولكن قبل أن تتوصwwل إىل تلwwك النتيجwwة ،عليwwك أواًل أن تتأ ّكwwد من أن عwwدد
تتكرر الحلقة الخارجية من 1إىل )( ،list.sizeولذلك تُع ّد خط ّي ًة بالنسبة لحجم القائمwwة ،nبينم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
.2نظ ًرا ألن تنفيذ تلك الخوارزمية بسيط ،فإن تكلفته منخفضة ،أي عىل الرغم من أن زمن التنفيwwذ يسwwاوي
ولذلك ،إذا عرفنا أن المصفوفة شبه ُمرتَّبة أو إذا لم تكن كبير ًة جدًا ،فقد يكون الترتيب بwwاإلدراج خيwwا ًرا جي wدًا،
17.2تمرين 14
تُع ّد خوارزمية الترتيب بالدمج merge sortواحد ًة من ضمن مجموعةٍ من الخوارزميwwات الwwتي يتفwwوق زمن
التربيعي .ننصحك قبل المتابعة بقراءة مقالة ويكيبيwwديا عن الwwترتيب بالwwدمج .merge sort
ّ تنفيذها عىل الزمن
بعد أن تفهم الفكرة العامة للخوارزمية ،يُمكِنك العودة الختبار فهمك بكتابة تنفي ٍذ لها.
ListSorter.java •
ListSorterTest.java •
ن ِّفذ األمر ant buildلتصريف ملفات الشيفرة ثم ن ِّفذ األمر .ant ListSorterTestسيفشل االختبار
159
هياكل البيانات للمبرمجين الترتيب Sorting
عليك إكمwwال التwwابع .mergeSortويمكنwwك بwwداًل من كتابwwة نسwwخة تعاوديwwة recursiveبالكامwwل ،أن تتبwwع
الطريقة التالية:
سيعطيك هذا التمرين الفرصة لتنقيح شيفرة الدمج دون التعامل مع تعقيدات التوابع التعاودية.
واآلن أضف حالة أساسية .base caseإذا كان لديك قائمة تحتوي عىل عنصر واحد فقط ،يُمكِنك أن تعيدها
مباشر ًة ألنها نوعً ا ما ُمرتَّبة بالفعل ،وإذا كان طول القائمة أقل من قيمة معينة ،يُمكِنك أن تُرتِّبها باستخدام التwwابع
أخيرًا ،عدِّل الحل واجعله يُن ِّفذ استدعاءين تعاوديّين لترتيب نصفي المصفوفة .إذا عدلته بالشwwكل الصwwحيح،
في كل مستوى .لنفترض أننا سنبدأ بقائمةٍ تحتوي عىل عد ٍد قدره nمن العناصر .وفيما يلي خطوات الخوارزمية:
.1ن ُ ِ
نشئ مصفوفتين وننسخ نصف العناصر إليهما.
.2نُرتِّب النصفين.
.3ندمج النصفين.
160
هياكل البيانات للمبرمجين الترتيب Sorting
في المستوى األعىل ،سيكون لدينا قائمة واحدة ُمك َّونة من عد ٍد قدره nمن العناصر .للتبسيط ،سنفترض أن
nعبارة عن قيمة مرفوعة لألس ،2وبالتالي ،سيكون لدينا في المستوى التالي قائمتwwان تحتويwwان عىل عwwدد n/2
ثم في المستوى التالي ،سيكون لدينا 4قوائم تحتوي عىل عدد قدره n/4من العناصر ،وهكذا حwwتى
من العناصرّ .
نصل إىل عدد nمن القوائم تحتوي جميعها عىل عنصر واحد فقط.
لدينا إ ًذا عدد قدره nمن العناصر في كل مستوى .أثناء نزولنا في المستوياتّ ،
قسمنا المصwwفوفات في كwwل
مستوى إىل نصفين ،وهو ما يستغرق زمنًا يتناسب مع nفي كل مستوى ،وأثناء صعودنا لألعىل ،علينا أن نwwدمج
161
هياكل البيانات للمبرمجين الترتيب Sorting
إذا كان عدد المسwwتويات يسwwاوي ،hفwwإن العمwwل اإلجمwwالي المطلwwوب يسwwاوي ) ،O(nhواآلن ،كم هwwو عwwدد
.1كم عدد المرات التي سنضطر خاللها لتقسيم nإىل نصفين حتى نصل إىل .1
.2أو كم عدد المرات التي سنضطرّ خاللها لمضاعفة العدد 1قبل أن نصل إىل .n
يُمكِننا طرح السؤال الثاني بطريقة أخرى" :ما هي قيمة األس المرفوع للعدد 2لكي نحصل عىل n؟".
2h = n
أي أن الزمن الكلي يساوي )) .O(n log(nالحwظ أننwwا تجاهلنwwا قيمwwة أسwاس اللوغwاريتم ألن اختالف أسwاس
اللوغاريتم يؤثر فقط بعامل ثابت ،أي أن جميع اللوغاريتمات لها نفس ترتيب النمو .order of growth
يُطلَق أحيانًا عىل الخوارزميات التي تنتمي إىل )) O(n log(nاسم "خطي-لوغاريتمي ،"linearithmicولكن
في الواقع ،يُع ّد )) O(n log(nالحد األدنى من الناحية النظرية لخوارزميات الترتيب التي تَعتمwwد عىل موازنwwة
العناصر مع بعضwwها البعض .يعwwني ذلwwك أنwwه ال توجwwد خوارزميwwة تwwرتيب بالموازنwwة ذات تwwرتيب نمٍّ wو أفضwwلَ من
الفقاعات الطريقة األفضل ،فخوارزمية ترتيب الفقاعات bubble sortصحيحٌ أنها بسيطة وسهلة الفهم ،لكنّهwwا
تستغرق زمنًا تربيع ًيا ،كما أن أداءها ليس جيدًا بالموازنة مع خوارزميات الترتيب التربيعية األخرى.
ربما خوارزمية الترتيب بالجذر radix sortهي اإلجابة األدق عن السؤال ،فهي خوارزمية ترتيب غwwير مبن ّيwwة
162
هياكل البيانات للمبرمجين الترتيب Sorting
.1مرّ عبر البطاقات وقسمها إىل مجموعات بنwwا ًء عىل الحwwرف األول ،أي ينبغي أن تكwwون الكلمwwات البادئwwة
بالحرف aضمن مجموعة واحدة ،يليها الكلمات التي تبدأ بحرف ،bوهكذا.
قسم كل مجموعة مرة أخرى بنا ًء عىل الحرف الثاني ،بحيث تصبح الكلمwwات البادئwwة بwwالحرفين aaمعً wا،
ِّ .2
يليها الكلمات التي تبدأ بالحرفين ،abوهكذا .لن تكون كل المجموعات مملً w
wوءة بالتأكيwwد ،ولكن ال بwwأس
بذلك.
واآلن ،أصبحت كل مجموعة ُمك َّونة من عنصر واحد فقط ،كما أصبحت المجموعات ُمرتَّبً 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
ً
خدمة عبر اإلنترنت تتعامل مع باليين المعامالت يوم ًيا ،وأننا نريد في نهاية كل يwwوم لنفترض مثاًل أننا نراقب
معرفة أكبر kمن المعامالت (أو أبطأ أو أي معيار آخر) .يُمكِننا مثاًل أن نُخزِّن جميع المعامالت ،ثم نُرتِّبها في نهاية
اليوم ،ونختار أول kمن المعامالت .سيستغرق ذلك زمنًا يتناسب مwع ،n log nوسwيكون بطيًئا جwدًا ألننwا من
المحتمwل أال نتمكَّن من مالءمwة باليين المعwامالت داخwل ذاكwرة برنwامج واحwد ،وبالتwالي ،قwد نضwطرّ السwتخدام
يُمكِننا بداًل من ذلك أن نَستخدِم كومة ُمقيدّة .heapإليك ما سنفعله في ما تبقى من هذا الفصل:
.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وألن الكومة
164
هياكل البيانات للمبرمجين الترتيب Sorting
تُن ِّفذ جافwwا الصwwنف PriorityQueueباسwwتخدام كومwwة .يحتwwوي ذلwwك الصwwنف عىل التوابwwع ُ
المعرَّفwwة في
الواجهة Queueومن بينها التابعان offerو pollاللّذان نلخص عملهما فيما يلي:
:poll .2يَح ِذف أصغر عنصر من الرتل من الجذر ويُحدِّث الكومة .يستغرق ً
أيضا زمنًا يتناسب مع .log n
إذا كان لديك كائن من النوع ،PriorityQueueتستطيع بسwwهولة تwwرتيب تجميعwwة عناصwwر طولهwwا nعىل
النحو التالي:
.1أضف جميع عناصر التجميعة إىل كائن الصنف PriorityQueueباستخدام التابع .offer
.2احذف العناصر من الرتل باستخدام التابع pollوأضفها إىل قائمة من النوع .List
ستجد ضمن الملف ListSorter.javaتعري ًفا مبدئ ًيا لتwwابع اسwwمه .heapSortأكملwwه ون ِّفذ األمwwر ant
المقي ّ
دة 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ذف التفريع الثالث :إذا كانت الكومة •
بوجود أصغر عنصر أعىل الكومة ،يُمكِننا االحتفاظ بأكبر عدد kمن العناصر .لنحلل اآلن أداء هذه الخوارزميwwة.
165
هياكل البيانات للمبرمجين الترتيب Sorting
التفريع األول :تستغرق إضافة عنصر إىل الكومة زمنًا يتناسب مع ).O(log k •
التفريع الثاني :يستغرق العثور عىل أصغر عنصر بالكومة زمنًا يتناسب مع ).O(1 •
التفريع الثالث :يستغرق حذف أصغر عنصر زمنًا يتناسب مع ) ،O(log kكما أن إضافة xتستغرق نفس •
مقدار الزمن.
ي مع .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ترتيب
للمساحة التي تتطلّبها الخوارزمية بعض االهتمام .عىل سبيل المثال ،تحتاج خوارزميwwة الwwترتيب بالwwدمج merge
sortإىل إنشاء نسخ من البيانات ،وقد كانت مساحة الذاكرة اإلجمالية التي تطلّبها تنفيذنا لتلwwك الخوارزميwwة هwwو
) .O(n log nفي الواقع ،يُمكِننا أن نُخ ِّفض ذلك إىل ) O(nإذا نفذنا نفس الخوارزمية بطريقة أفضل.
في المقابل ،ال تنسخ خوارزمية الترتيب باإلدراج insertion sortالبيانات ألنها تُرتِّب العناصر في أماكنهwwا،
ً
مؤقتة لموازنة عنصرين في كل مرة ،كما تَستخدِم عwwددًا قلياًل من المتغwwيرات المحليwwة local متغيرات
ٍ وتَستخدِم
نش 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در من
البرنامج من األساس .إذا اخترت خوارزمية تتطلّب حيزًا أقل من الذاكرة ،وتَ َ
سمح بمالئمة المعالجة ضمن
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
أحدث إصدارات أكاديمية حسوب