الطوابير )(Queues
إنشاء نظام طابور
نظام الطابور يشبه ذلك الخاص بالمكدّسات .يوجد اختلاف بسيط في كون أن العناصر تخرج في الإتجاه
المعاكس ،لا يوجد شيء صعب إن كنت قد فهمت المكدّسات.
سننشئ هيكل Elementو هيكل تحكّم : Queue
;1 typedef struct Element Element
2 struct Element
{3
;4 int number
5 ;Element *next
;} 6
;7 typedef struct Queue Queue
8 struct Queue
{9
10 ;Element *first
;} 11
تماما كالمكدّسات ،كل عنصر من الطابور سيكون من نوع . Elementبالإستعانة بالمؤش ّر first
سنتوف ّر دائما ًعلى العنصر الأول و يمكننا من خلاله الصعود إلى آخر عنصر.
إضافة عنصر إلى الطابور )(enqueuing
الدالة التي تضيف عُنص ُرا ً إلى الطابور تسمّى دالة ”الإلحاق” ) .(enqueuingتوجد حالتان :
• إما أن الطابور فارغ ،في هذه الحالة يجب أن ننشئ الطابور بجعل المؤش ّر firstيؤش ّر نحو العنصر الجديد
الذي نحن بصدد انشائه.
• إما أن الطابور غير فارغ ،في هذه الحالة يجب أن نتق ّدم في الطابور إنطلاقا ًمن العنصر الأول حتى نصل
إلى آخر عنصر .نضيف العنصر الجديد بعد آخر عنصر.
إليك ما يمكننا فعله عمليا ً:
)1 void enqueue(Queue *q, int newNumber
{2
3 ;))Element *new = malloc(sizeof(*new
4
)if (new == NULL || new== NULL
{5
;)6 exit(EXIT_FAILURE
}7
;8 new−>number = newNumber
;9 new−>next = NULL
501
10 (Stacks and Queues) المكدّسات و الطوابير.2.الفصل د
11
12 if (q−>first != NULL) // The queue is not empty
13 {
14
15 // We move to the end of the queue
16
17 Element *currentElement = q−>first;
18
19 while (currentElement−>next != NULL)
20 {
21
22 currentElement= currentElement−>next;
23 }
24 } currentElement−>next = new;
}
else /* The queue is empty, it’s our first element */
{
q−>first = new;
}
الاختلاف. ك ّل منهما يجب أن تتم دراستها على حدى،ترى في هذه الشفرة المصدر ية تحليل كلتا الحالتين
هو أنه يجب التموضع في نهاية الطابور لوضع العنصر، و الذي يضيف لمسة صعوبة صغيرة،مقارنة ً بالمكدّسات
. هذا ما يمكنك ملاحظته، كافية للقيام باللازمwhile لـكن لابأس فحلقة.الجديد
(dequeuing) إزالة عنصر من الطابور
، بامتلاكنا مؤش ّرا نحو أول عنصر من الطابور.( تشابه كثيرا ًعملية إلغاء التكديسdequeuing) عملية إلغاء الإلحاق
.يكفي أن ننزعه و أن ن ُرجع قيمته
1 dequeue(Queue *queue)
2{
3 if (queue == NULL)
4{
5 exit(EXIT_FAILURE);
6}
7 int dequeuedNumber = 0;
8 // We verify if there’s something to dequeue
9 if (queue−>first != NULL)
10 {
11 Element *dequeuedElement = queue−>first;
12
dequeuedNumber = dequeuedElement−>number;
13 queue−>first = dequeuedElement−>next;
14 free(dequeuedElement);
15 }
16 return dequeuedNumber;
17 }
502
مل ّخص
حان دورك !
تبقى دالة إظهار محتوى الطابور displayQueueو عملها مشابه لما قمنا به مع المكدّسات .سيسمح لك هذا
بالتأكد من سلامة عمل الطابور.
قم بعد ذلك بكتابة mainمن أجل تجريب البرنامج .يجدر بنتيجة البرنامج أن تشبه هذه :
Queue’s state :
4 8 15 16 23 42
I dequeue 4
I dequeue 8
Queue’s state :
15 16 23 42
يجدر أن تكون قادرا ً على إنشاء مكتبة الطوابير الخاصة بك ،بملفات queue.hو queue.cمثلا ً.
أقترح عليك تنز يل مشروع التحكّم في الطوابير كاملا ًإن أردت .إنه يتضمّن الدالة : displayQueue
http://www.siteduzero.com/uploads/fr/ftp/mateo21/c/files.zip
مل ّخص
• تسمح المكدّسات و الطوابير بتنظيم معطيات في الذاكرة عند وصولها بالتوالي.
• تستعمل المكدّسات و الطوابير نظام قوائم متسلسلة لتجميع العناصر.
• في حالة المكدّسات ،تتم إضافة المعطيات الواحدة فوق الأخرى .و إن أردنا استخراج بيانة ،فسنستخرج
آخر واحدة و التي كنا بصدد إضافتها )الأحدث( .نت ّكلم هنا عن خوارزمية .(Last In First Out) LIFO
• في حالة الطوابير ،تتم إضافة المعطيات الواحدة بعد الأخرى .نقوم باستخراج البيانة الأولى و التي قمنا
بإضافتها أولا للطابور )الأقدم( .نتكل ّم عن خوارزمية .(First In First Out) FIFO
503
الفصل د .2.المكدّسات و الطوابير )(Stacks and Queues
504
الفصل د3.
جداول التجزئة )(Hash tables
للقوائم المتسلسلة نقطة ضعف كبيرة في حال أردنا قراءة محتواها :يستحيل الوصول إلى عنصر معي ّن مباشرة.
يجب التق ّدم في القائمة عنصرا ً بعنصر حتى نجد العنصر الذي نريد .هذا يطرح مشاكل من ناحية الأداء ما إن
يكون حجم القائمة المتسلسلة ضخما ً .تخي ّل قائمة متسلسلة ٺتكوّن من 1000عنصر بينما العنصر الذي نبحث عنه
موجود في آخرها !
تمث ّل جداول التجزئة طر يقة أخرى لتخزين البيانات .حيث أنها تستند على مبدأ الجداول في لغة الـ Cو التي
نعرف التعامل معها جي ّدا ً .ماهي فائدتها الـكُبرى ؟ هي تسمح بإيجاد سر يع لعنصر محدد ،سواء كان الجدول
يحتوي 10000 ،1000 ،100خانة أو حتى أكثر !
د 1.3.لماذا نستعمل جدول تجزئة ؟
لننطلق من المشكل الذي تطرحه القوائم المتسلسلة .هذه الأخيرة مرنة بشكل خاص ،هذا ما استطعنا
ملاحظته :يمكننا إضافة أو إزالة خانات في أي لحظة نريد ،بينما يكون الجدول ”ثابتا ً” ما إن يتم إنشاؤه.
لـكن ،كما قل ُت لك في المق ّدمة ،للقوائم المتسلسلة عيب كبير :إذا أردنا استرجاع عنصر محدد من القائمة،
يجب تص ّفح هذه الأخيرة حتى نجد ذلك العنصر !
تخي ّل قائمة متسلسلة تحتوي معلومات حول التلاميذ :الإسم ،الع ُمر و المع ّدل .سيتم تمثيل ك ّل تلميذ بهيكل
نسميه . Student
عملنا سابقا ًعلى القوائم المتسلسلة التي تحتوي على . intكما قل ُت لك ،من الممكن تخزين أي شيء نريد
في قائمة ،حتى مؤشّرا ً نحو هيكل آخر كما سأقترحه لك الآن.
إذا أرد ُت الوصول إلى المعلومات الخاصة بالشخص Luc Doncieuxفي الصورة الموالية ،يجب عليّ التق ّدم
في ك ّل القائمة كي أكتشف بأنه العنصر الأخير فيها !
505
الفصل د .3.جداول التجزئة )(Hash tables
بالفعل ،لو أننا بحثنا عن الشخص ،Julien Lefebvreكان البحث ليكون أسرع بما أنه متواجد في بداية
القائمة .و مع ذلك ،لتقييم كفاءة الخوارزمية ،يجب أن نفكّر دائما ًفي أسوء الحالات .و الأسوء هو Luc
هنا.
هنا ،نقول أن خوارزمية البحث لها تعقيد ) ،O(n) (complexityلأنه يجب تص ّفح كل القائمة المتسلسلة
للوصول إلى الع ُنصر المراد ،و في أسوء الحالات يكون هذا هو آخر عنصر .إذا كانت القائمة تحتوي على
9عناصر ،يجب أن يتم تشغيل 9دورات للحلقة كحد أقصى لإيجاد العنصر.
في هذا المثال ،لا تحتوي القائمة المتسلسلة سوى على أربعة عناصر .سيجد الحاسوب الشخص Luc Doncieux
بسرعة كبيرة لا تسمح لنا حتى بأن نقول كلمة ”أووه” .لـكن تخي ّل أن هذا الشخص يتواجد في آخر قائمة متسلسلة
من 10000عنصر ! ليس مقبولا أن يتم البحث في 10000عنصر لإيجاد المعلومة .هنا ٺتدخّل جداول التجزئة.
د 2.3.ماهي جداول التجزئة ؟
إذا كنت ٺتذكر جيدا ً ،لا تعرف الجداول هذا النوع من المشاكل .لهذا ،كي نصل إلى العنصر في الوضعية
2من الجدول تكفيني كتابة التالي :
;}1 int table[4] = {12, 7, 14, 33
;)]2 printf(”%d”, table[2
لو نعطي للحاسوب ] ، table[2فسيتوجّه مباشرة إلى المكان في الذاكرة أين هو مخز ّن العدد .14أي أنه
لن يتق ّدم في الجدول خانة بخانة.
هل أنت بصدد القول أن الجداول ليست ”بذلك القدر من السوء” ؟ لـكن في هذه الحالة سنخسر الميزات
التي توف ّرها القوائم المتسلسلة التي تسمح لنا بإضافة و إزالة خانات في أي لحظة !
في الواقع ،القوائم المتسلسلة مرنة أكثر .أما بالنسبة للجداول ،فهي تسمح بالوصول السر يع للمعطيات .يمكننا
القول أن جداول التجزئة تش ّكل حل ّا وسطا بين الإثنين.
يوجد عيب في استعمال الجداول لم نتكل ّم عنه سابقا ً :يتم تعر يف خانات الجدول عن طر يق أرقام نسمّيها
الر ّتب أو الدلالات ) .(indicesلا يمكن أن نطلب من الحاسوب ” :ماهي المعلومات المتواجدة في الخانة التي
تسمّى ” .”Luc Doncieuxأي أننا لإيجاد الع ُمر و المع ّدل لن نتم ّكن من كتابة :
506
ماهي جداول التجزئة ؟
;]”1 table[”Luc Doncieux
مع أنه سيكون عمليا ًلو أننا نستطيع الوصول إلى خانة ما باستعمال الاسم فقط ! حسنا ً ،هذا ممكن باستعمال
جداول التجزئة.
كما رأينا مؤ ّخرا ً .لا تش ّكل جداول التجزئة ”جزء ً” من لغة الـ .Cنتح ّدث هنا عن مبدأ .سنعيد استعمال
أساسيات لغة الـ Cالتي نعرفها من قبل لأجل إنشاء نظام ذكي جديد .و كأنه في لغة الـ ،Cباستعمال
بعض الأدوات القاعدية ،يمكننا إنشاء الـكثير من الأشياء !
بما أنه من الواجب أن يتم ترقيم الجدول بال ُر ّتب ،كيف سنجد رقم الخانة لو أننا نعرف الاسم ”Luc
”Doncieuxفقط ؟
ملاحظة جيدة .في الواقع ،يبقى الجدول جدولا ًو لن يعمل إلا بالر ّتب المرق ّمة .تخي ّل جدولا ًيوافق الصورة
الموالية :كل خانة لها رتبة و ٺتوفر على مؤش ّر نحو هيكل من نوع . Studentأنت تعرف القيام بهذا الآن :
لو أردنا إيجاد الخانة التي توافق ،Luc Doncieuxيجب أن نجيد تحو يل الإسم إلى رُتبة في الجدول .و بهذا،
يجب أن نتم ّكن من ربط ك ّل اسم برقم من خانة في الجدول :
• .0 = Julien Lefebvre
507
الفصل د .3.جداول التجزئة )(Hash tables
• .1 = Aurélie Bassoli
• .2 = Yann Martinez
• .3 = Luc Doncieux
لا يمكننا أن نكتب ]” table[”Luc Doncieuxكما فعل ُت سابقا ً .لأن هذا غير مسموح به في لغة
الـ.C
السؤال الذي ي ُطرح هو :كيف نحوّل سلسلة محارف إلى عدد ؟ هذا هو سحر التجزئة .تجب كتابة دالة تأخذ
كمعامل سلسلة محارف ،تطب ّق حسابات عليها ،ثم ت ُرجع لنا عددا ً يوافق تلك السلسلة .سيكون هذا العدد هو
رتبة الخانة في الجدول :
د 3.3.كتابة دالة تجزئة
تكمن ك ّل الصعوبة في كتابة دالة تجزئة صحيحة .كيف نحوّل سلسلة ً محرفي ّة إلى عدد وحيد ؟
أولا و قبل ك ّل شيء ،لنوضح الأمور :جدول التجزئة لا يحتوي 4خانات كما أضع في الأمثلة ،لـكن 100
أو 1000أو أكثر .لا يهم حجم الجدول ،لأن البحث سيكون سر يعا ًجدا ً دائما.
نقول أن هذا تعقيد بـدرجة ) O(1لأننا نجد مباشرة عنصر البحث .في الواقع ،دالة التجزئة سترجع لنا
رُتبة :يكفي ”القفز” مباشرة إلى الخانة الموافقة للجدول .لسنا بحاجة إلى تص ّفح ك ّل الخانات !
تخي ّل إذا ً جدولا ًمن 100خانة ،تقوم فيه بتخزين مؤش ّرات نحو هياكل من نوع . Student
;]1 Student* table[100
508
كتابة دالة تجزئة
يجب علينا أن نكتب دالة ،انطلاقا ًمن اسم ،تول ّد عددا ً محصورا ً بين 0و ) 99رُتب الجدول( .هنا يتطل ّب
منا الأمر الحذاقة .توجد ُطرق ر ياضية ج ّد مع ّقدة كي ”نجز ّء” البيانات ،أي أن نحوّلها إلى أعداد.
الخوارزميتان MD5و SHA1هما دالتا تجزئة مشهورتان ،لـكنهما متق ّدمتان كثيرا بالنسبة لنا حاليا ً.
يمكنك اختراع دالة التجزئة الخاصة بك .هنا ،لـكي نب ّسط الأمور ،أقترح عليك ببساطة أن تجمع القيم ASCII
لك ّل حرف من الاسم ،أي من أجل الاسم Luc Doncieuxستكون لدينا عملية الجمع التالية :
’1 ’L’ + ’u’ + ’c’ + ’ ’ + ’D’ + ’o’ + ’n’ + ’c’ + ’i’ + ’e’ + ’u’ + ’x
سيكون لدينا مشكل :هذا المجموع يتخ ّطى الـ ! 100بما أن الجدول الذي أنشأناه لا يحتوي سوى على 100
خانة ،فإن أخذنا بهذه القيمة فسنخاطر بالخروج من حدود الجدول.
أذكّرك بأن ك ّل محرف في جدول ASCIIيمكن أن يكون مرق ّما حت ّى .255و بهذا سنتجاوز بسرعة حاجز الـ.100
لح ّل هذا المشكل ،يمكننا استعمال عامل الترديد . %هل ٺتذكره ؟ إن ّه يعطي باقي القسمة ! لو نقوم بهذا
الحساب :
1 lettersSum % 100
سنتح ّصل قطعا ًعلى عدد محصور بين 0و .99مثلا ً ،لو أن المجموع يساوي ،4315باقي القسمة على 100
هو .15ست ُرجع إذا دالة التجزئة القيمة .15
إليك ما يمكن أن تكون عليه الدالة :
)1 int hash(char *string
{2
;3 int i = 0, hashNumber= 0
)4 for (i = 0 ; string[i] != ’\0’ ; i++
{5
;]6 hashNumber += string[i
}7
;8 hashNumber %= 100
;9 return hashNumber
} 10
لو نعطيها )” ، hash(”Luc Doncieuxست ُرجع لنا القيمة .55و بـ )”، hash(”Yann Martinez
نتح ّصل على .80
بفضل دالة التجزئة هذه ،يمكنك أن تعرف في أي خانة من الجدول يجب أن تضع المعلومات ! إذا أردت
الوصول إلى هذه الخانات لاحقا ًلاسترجاع المعلومة ،تكفي ”تجزئة” اسم الشخص من جديد لـكي نجد رُتبة الخانة
في الجدول أين تخز ّن المعلومات !
أنصحك بإنشاء دالة بحث ٺتك ّفل بتجزئة المفتاح )الاسم( و ت ُرجع لنا مؤشّرا ً نحو المعلومات التي نبح ُث عنها.
هذا سيعطينا مثلا :
509
الفصل د .3.جداول التجزئة )(Hash tables
;)”1 infoAboutLuc = findHashTable(table, ”Luc Doncieux
د 4.3.معالجة التصادمات )(Collisions management
حينما ت ُرجع دالة التجزئة نفس العدد من أجل مفتاحين مختلفين ،نقول أن ّه حدث تصادم .مثلا في دالتنا،
لو أننا نملك شخصا ً اسمه تحر يك أحرف لـ ،Luc Doncieuxمثلا ً ،Luc Doncueixسيكون مجموع الأحرف هو
نفسه ،و بهذا فإن نتيجة دالة التجزئة ستكون نفسها !
يمكن لسببين أن يشرحا التصادم :
• دالة التجزئة لا تعمل بكفاءة عالية .هذا يمث ّل حالتنا .لقد كتبنا دالة سهلة جدا ً )لـكن نوعا ًما كافية( من
أجل الأمثلة .الدالتان MD5و SHA1المذكورتان أعلاه هما ذات جودة عالية لأنهما تنتجان نسبة قليلة
من التصادُمات .و لتعلم أن SHA1مف ّضلة في أيامنا هذه أكثر من MD5لأنها تننج نسبة تصادمات أقل
مقارنة بنظيرتها.
• الجدول الذي نخزن به المعلومات صغير الحجم كثيرا ً .لو أننا ننشئ جدولا من 4خانات و نريد تخزين 5
أشخاص ،فسيحدث تصادم بالتأكيد ،أي ان دالة التجزئة ست ُعطي نفس الر ُتبة من أجل اسمين مختلفين.
إذا حصل تصادم فلا داعي للخوف ! هناك حلان يمكنك الاختيار بينهما :العـ َنو َنـ َة المفتوحة و السَل ْسَل َة.
العنونة المفتوحة )(Open addressing
إذا بقيت أمكنة شاغرة في الجدول ،يمكنك تطبيق التقنية التي ت ُدعى التجزئة الخطي ّة .المبدأ سهل .هل الخانة
محجوزة ؟ لا يوجد مشكل ،سننتقل للخانة التي تليها .آه ،هل هذه محجوزة أيضا ً؟ توجّه لللتي بعدها.
و هكذا حتى تجد خانة موالية فارغة .إن وصلت إلى نهاية الجدول ،فعد إلى البداية و أكمل البحث.
تطبيق هذه الطر يقة سهل جدا ً ،لـكن إن واجهت الـكثير من التصادمات ،فسيكون عليك استغراق وقت
كبير في البحث عن الخانة الشاغرة الموالية.
توجد طرق بديلة )التجزئة المزدوجة ،التجزئة الرباعية …( و التي تن ّص على التجز ّئة من جديد حسب دالة
أخرى في حالة وجود تصادم .هذه الدوال أكثر كفاءة لـكن أكثر تعقيدا من ناحية التطبيق.
510
مل ّخص
السَل ْسَل َة )(Chaining
ح ّل آخر ين ّص على إنشاء قائمة متسلسلة في مكان التصادم .هل تريد تخزين بيانتين )أو أكثر( في نفس الخانة ؟
استعمل قائمة متسلسلة و أنشئ مؤشّرا ً نحو هذه القائمة انطلاقا ًمن الجدول :
بالفعل ،سنعود لمشكل القوائم المتسلسلة :إذا كان هناك 300عنصرٍ في هذا الموقع من الجدول ،يجب
تص ّفح القائمة المتسلسلة إلى حين إيجاد العنصر الصحيح.
هنا ،كما ترى .ليست القوائم المتسلسلة دائما ًالأمثل ،لـكن لجداول التجزئة حدودها أيضا ً .يمكننا المزج بين
الإثنين من أجل الحصول على الجانب الأفضل من ك ّل بنية.
على أية حال ،النقطة الحساسة في جداول التجزئة هي دالة التجزئة .فكلّما أنتجت تصادُمات أقل .كلما كان
ذلك أفضل .سأترك لك مهمة إيجاد دالة التجزئة المناسبة لحالتنا !
مل ّخص
• القوائم المتسلسلة مرنة ،لـكن عملية إيجاد عنصر محدد تستغرق وقتا ًطو يلا ًلأنه يجب تص ّفح القائمة عنصرا ً
بعنصر.
• جداول التجزئة هي جداول نخز ّن فيها المعلومات في مكان محدد بواسطة دالة التجزئة.
• تأخذ دالة التجزئة مفتاحا ًكمعامل )مثلا ً :سلسلة محرفي ّة( و تعيد عددا كمخرج.
• يتم استعمال هذا العدد لمعرفة أي رتبة من الجدول يجب تخزين البيانات.
• دالة التجزئة الأكثر كفاءة هي التي لا تول ّد عددا ً كبيرا ً من التصادُمات ،أي أنها تتجنب قدر المستطاع
إرجاع نفس العدد من أجل مفتاحين مختلفين.
• في حالة التصادم ،يمكننا استعمال تقنية العنونة المفتوحة )البحث عن خانة شاغرة أخرى في الجدول( أو
استعمال تقنية السلسلة )الدمج مع القوائم المتسلسلة(.
511
الفصل د .3.جداول التجزئة )(Hash tables
512
خاتمة
هل تريد المزيد ؟
لماذا لا ٺتعلّم لغة الـ C++؟
http://www.siteduzero.com/tuto-3-5395-0-apprenez-a-programmer-en-c.
html
هذا درس آخر كتبت ُه حول هذه اللغة قريبة الـ .Cإذا كنت تعرف الـ ،Cفلن تكون ضائعا ًبل ستفهم بسرعة
فائقة الفصول الأولى !
فليكن في علمك أنني كتبت درسا ًقصيرا يسمّى ”من الـ Cإلى الـ ”C++الذي يبې ّن جزء ً من الاختلاف بين الـC
و الـ.C++
http://www.siteduzero.com/tutoriel-3-430167-du-c-au-c.html
بلغة الـ ،C++يمكنك البدء في البرمجة غرضية التوجّه )أو البرمجة الكائنية ) .((OOPقد يكون هذا المبدأ
مع ّقدا قليلا في البداية ،لـكن ستجد بأن هذه الطر يقة في البرمجة ناجعة جدا ً ! ستكتشف أيضا ًمعها المكتبة Qt
التي تسمح بإنجاز واجهات رسومية كاملة ج ّدا.
أشكر كثيرا Taurreو Pouet_foreverلمساعدتهم الـكبيرة في القيام بالمراجعات الأخيرة لهذه الدروس.
513