الفصل الثامن

Sorting in Linear Time 
مقدمة
في الفصول اللي عدت شفنا أكتر من algorithm بيحل مشكلة ال sorting , بداية من ال Insertion sort لل Merge sort لل Heap sort لل Quicksort , ولكن إيه أفضل performance وصلناله من حيث ال running time؟ الإجابة هي إن أفضل running time وصلناله هو O(nlgn) في ال worst case , السؤال بقى هل فيه أفضل من كدة؟ هل نقدر نعمل design ل Sorting algorithm يشتغل في running time أقل من O(nlgn)؟ الإجابة أه نقدر , بس لازم نغير الطريقة اللي بنفكر بيها , طيب هي إيه الطريقة اللي بنفكر بيها ومحتاجين نفكر بطريقة غيرها؟ أو السؤال بشكل تاني إيه المتشابه في كل ال algorithms اللي شفناها لغاية دلوقتي؟ في كل ال algorithms اللي شفناها لغاية دلوقتي أحنا بنحاول نجيب ال sorted order من خلال المقارنة (comparing) بين ال elements أو الأرقام اللي احنا بنرتبها , يعني في كل algorithm بنعمل عملية comparing بين ال elements وبعضها ومن خلال ال comparing بنعرف ال relative order يعني مين أكبر من مين وبالتالي نوصل لل sorted order , في الفصل ده هنشوف إن أي algorithm بيشتغل بطريقة ال comparing لا يمكن يوصل ل running time أقل من O(nlgn) في ال worst case , أو بمعنى تاني أي algorithm شغال بطريقة ال comparing ال running time بتاعه Ω(nlgn) يعني ال nlgn هي lower bound (حد أدنى) لل running time بمعنى إن مفيش أقل من كدة , كمان في الفصل ده هنشوف 3 algorithms بيشتغلو بطرق غير ال comparing وال lower bound بتاع ال comparing لا ينطبق عليهم , وبالتحديد ال 3 algorithms دول بيشتغلوا في linear time يعني O(n) زي معنوان الفصل بيقول Sorting in Linear Time , ال 3 algorithms دول هما ال Counting sort وال Bucket sort وال Radix sort وهنشوف كل algorithm بالتفصيل بإذن الله.


Lower bounds for sorting

في السكشن ده عايزين نثبت إن أي comparison-based sorting algorithm ال running time بتاعه Ω(nlgn) في ال worst case , من خلال الإثبات ده نقدر نتأكد إن ال Merge وال Heap هما theoretically (نظريا) أفضل algorithms بتشتغل بطريقة ال comparing وإن ال Quick sort أفضلهم practically (عمليا) , وبالتالي نقدر نحاول نفكر في طريقة تاني غير ال comparing وإحنا متأكدين إن إحنا جبنا أفضل حاجة في الطريقة دي قبل منفكر في غيرها.

أول خطوة عايزين نعمل mathematical model (نموذج رياضي) يمثل أي comparison sort algorithm , يعني طريقة تمثل الخطوات اللي بيعملها أي comparison sort algorithm علشان يوصل لل sorted order , وبالتالي نقدر نثبت من خلاله ال lower bound اللي احنا عايزين نثبتها , أو إن عدد الخطوات اللي بياخدها أي comparison sort algorithm بتخضع لل lower bound في ال worst case علشان تقدر توصل للحل.

ال mathematical model اللي هنستخدمه هو ال decision-tree model ومن إسمها هي binary tree بتمثل عملية ال comparison بين 2 elements اللي بتمثل ال base (الأساس) أو العملية الأساسية لأي comparison sort algorithm , في ال model ده إحنا بنهمل أي عملية خلاف ال comparison , زي عمليات ال data movement أو ال swap كمثال , وده لإن إحنا بنثبت lower bound , فلو أخدنا في إعتبارنا عمليات ال comparison بس وأثبتنا ال lower bound فلا شك إن ال lower bound صحيحة مع كل عمليات ال algorithm.


في الصورة اللي قدامك decision tree بتمثل ال Insertion sort (كمثال ل comparison sort algorithm) على دخل مكون من (a1, a2, a3) , في ال tree دي:
  • كل interior node (يعني node ليها children) بتمثل عملية comparison بين 2 elements من الدخل , يعني كمثال ال root node عبارة عن (1:2) وده معناه عملية comparsion بين العنصرين a1, a2.
  • كل leaf node (يعني node ملهاش children) عبارة عن خرج لل algorithm , يعني ال algorithm نفذ مجموعة خطوات ووصل لحل معين , يعني كمثال أول leaf node على اليمين عبارة عن (1, 2, 3) وده معناه إن خرج ال algorithm في الحالة دي عبارة عن ال sequence ده (a3 <= a2 <= a1).
  • كل transition أو سهم من node في level ل node في level أعلى منه (اعتبارا ان ال level بتاع ال root node هو level 0) بيوضح نتيجة عملية ال comparison اللي تمت , يعني كمثال ال root node بتقارن بين العنصر الأول والتاني فلو الأول أصغر من أو يساوي التاني هنتحرك في ال transition الشمال ولو الأول أكبر من التاني هنتحرك في ال transition اليمين وفي الحالتين هنروح لعملية جديدة , أو بعض ال transitions بتوصلك لناتج لل algorithm لو ال node اللي بتوصلك ليها هي leaf node.
لو ركزنا هنلاقي ان ال tree بتمثل كل الخطوات اللي ممكن ال insertion sort يعملها لو الدخل 3 elements مهما كانت ال elements دي , وحسب ال elements بنوصل ل leaf node بتمثل الحل , كمثال لو الدخل عبارة عن (a1 = 6, a2 = 8, a3 = 5) هتكون الخطوات اللي هياخدها ال algorithm علشان يوصل للحال عبارة عن الخطوات المتحددة بالمسار المظلل في الصورة , يعني في الأول بيقارن الأول(a1 = 6) بالتاني(a2 = 8) فيلاقي الأول أصغر , فيقارن التاني(a2 = 8) بالتالت(a3 = 5) فيلاقي التالت أصغر, فيقارن الأول(a1 = 6) بالتالت(a3 = 5) فيلاقي التالت أصغر فيكون الحل عبارة عن (a3 <= a1 < a2).


إحنا عايزين نعرف إيه أقل عدد من الخطوات(كدالة في الدخل) ممكن يعملها ال algorithm علشان يرتب أي دخل مكون من n elements , ويكون ده ال lower bound اللي احنا عايزين نثبته, إحنا عارفين إن عدد الخطوات للوصول لأي حل هو عدد ال transitions من ال root node لغاية ال node اللي فيها الحل ده (اللي هي leaf node) , وده بالظبط هو ال height بتاع ال tree , لإن ال height هو عدد ال transitions من ال root node لأبعد leaf node , بس إحنا مش عايزين عدد ال transitions لأبعد leaf , إحنا عايزينها ﻷقرب leaf لإن إحنا بنحسب ال lower bound , في الحقيقة الجزئية دي متفرقش لسببين: الأول إن ال decision-tree هي full binary tree , يعني ال leaf nodes بتبقى في ال level الأخير واللي قبل الأخير بس, التاني لإن الفرق ده constant وإحنا بنحل المشكلة asymptotically فبنهمل ال constants , يعني من الاخر إحنا عايزين نحسب ال lower bound لل height بتاع ال tree دي ويكون هو ده ناتج الإثبات بتاعنا. في كام معلومة عايزين نعرفهم:
  • علشان ال algorithm يكون correct لازم يكون قادر إنه ينتج ك output أي permutation من الدخل (إحنا عارفين إن permutation يعني إعادة ترتيب للدخل).
  • عدد ال permutation لمجموعة عناصر عددها n هو !n (يعني مضروب n).
  • ده معناه إن عدد ال leaves لا يمكن يكون أقل من !n , وبالتالي لو رمزنا لعدد ال leaves ب l يبقى n! <= l .
  • لو رمزنا لل height ب h , عدد ال leaves في أي binary tree بيساوي على الأقل 2^h , يعني l <= h2 .
نقدر نستنتج إن : n! <= l <= h2

وبالتالي : n! <= h2

لو خدنا lg للطرفين : lg(n!) <= h (نقدر ناخد lg للطرفين لان الدالة lg تعتبر monotonically increasing)

وبالتالي : h = Ω(nlgn) (ودي الكاتب أثبتها رياضيا)


Counting sort
 
دلوقتي هنشوف أول algorithm بيشتغل في running time أقل من Ω(nlgn), أو بالتحديد أول algorithm بيشتغل في O(n) أو in linear time , ال Counting sort هو algorithm مبني على فرضية , الفرضية هي إنك لو عرفت لكل عنصر i في ال input عدد العناصر اللي قيمتها أقل من قيمة ال i , فبالتالي تقدر مباشرة تحط i في مكانه الصحيح , يعني كمثال لو عندك input معين فيه العنصر 105 وقدرت تعرف إن ال input ده عدد العناصر اللي قيمتها أقل من 105 هي 50 عنصر فالعنصر 105 المفروض مكانه الصحيح في المكان 51 , ده الأساس اللي مبني عليه ال counting sort , مشكلة ال counting sort إنه عليه بعض ال constraints (القيود), زي:
  • لازم عناصر الدخل تكون موجبة.
  • لازم عناصر الدخل تكون قيمها في range محدد , يعني كمثال تقدر تحدد مسبقا إن العناصر اللي جاية في ال input قيمها لا تزيد عن 100000.
الشرطين اللي فاتوا لازم يتحققوا علشان ال algorithm يشتغل correctly.
  • لازم يكون ال range اللي بتتراوح فيه قيم عناصر الدخل يكون أقل asymptotically من عدد عناصر الدخل, يعني لو رمزنا لعدد عناصر الدخل ب n, ورمزنا لأكبر قيمة لعناصر الدخل ب k (يعني عتاصر الدخل قيمها بتتراوح من 0 ل k) فلازم يكون الشرط ده متحقق :           k = O(n).
الشرط اللي فات لازم يتحقق علشان ال algorithm يشتغل efficiently (يعني in linear time).

نشوف ال pseudocode ل counting sort :


  • ال algorithm بياخد دخل عبارة عن: A هي ال input array, و B هي ال output array , و k وهي القيمة اللي بتحدد ال range اللي بتتراوح فيه قيم عناصر الدخل.
  • ال algorithm كمان بيحتاج array حجمها k , يستخدمها ك temporary storage (مساحة تخزين مؤقتة), وبالتالي ال algorithm لا يعد sort in place.
  • أول حاجة عايزين نحسبها هي لكل عنصر i في ال input array A عدد العناصر اللي قيمتها أصغر من i وعايزين نخزن المعلومات دي في ال array C.
  • في السطر 1 : 2 بيعمل initialization لعناصر ال array C بصفر.
  • في السطر 3 : 4 بيعدي على عناصر array A , لكل عنصر i في A بيزود بواحد المكان اللي في ال array C واللي ال index بتاعه بيساوي i , وبالتالي كل عنصر في ال array C ال index بتاعه بيعبر عن عنصر في ال array A والقيمة اللي فيه هي عدد مرات تكرار العنصر ده في ال array A, يعني كمثال لو ال array A فيها رقم 10 اتكرر 17 مرة فال array C المكان اللي ال index بتاعه 10 هيكون فيه قيمة 17 وده معناه ان العنصر 10 اتكرر 17 مرة في ال array A.
  • في السطر 6 : 7 بيعدي على عناصر ال array C بيخلي قيمة كل عنصر هي مجموع القيمة اللي فيها والقيمة اللي قبليها , وبالتالي كل عنصر في array C ال index بيشاور على عنصر في array A والقيمة بتحدد عدد العناصر اللي أصغر من أو يساوي العنصر في ال array A , كمثال لو أول مكان في array C اللي ال index بتاعها 0 فيه قيمة 2 فده معناه إن العنصر 0 اتكرر مرتين في ال array A, ولو تاني عنصر في array C اللي ال index بتاعه 1 فيه قيمة 4 فده معناه إن العنصر 1 اتكرر 4 مرات في ال array A , فيعد تنفيذ السطرين دول المكان التاني في ال array C هيكون القيمة اللي فيه 6 وده مجموع العناصر اللي بتساوي 1 أو أصغر منه (0).
  • تاني حاجة عايزين نحط كل عنصر في مكانه الصحيح.
  • في السطر 9 : 11 بيعدي على عناصر ال array A بشكل عكسي يعني بداية من اخر عنصر وانتهاءا بأول عنصر(سطر 9), في كل مرة بيجيب عنصر من array A من خلال A[j] ويجيب المكان اللي المفروض يتحط فيه في ال array B (ال output) من خلال C[A[j]], وبعدين يحط العنصر ده في مكانه الصحيح في array B من خلال B[C[A[j]]] = A[j] (سطر 10), وبعدين يقلل C[A[j]] بواحد (سطر 11).
ليه الخطوة الأخيرة الغريبة دي؟
  • أول حاجة ليه بيقلل بواحد في سطر 11؟ علشان هو لكل عنصر في array C ال index بتاعه i مخزن عدد العناصر اللي في array A أقل من أو يساوي i وبالتالي لو فيه عنصر في array A قيمته 10 وفيه 5 عناصر أصغر منه وهو اتكرر 3 مرات فبالتالي المكان اللي ال index بتاعه 10 في array C فيه قيمة 8 ,وأنا عايز أحط ال 3 عناصر اللي قيمتهم بتساوي 10 في الأماكن 6 و 7 و 8 , فأول مرة هحط العنصر في مكان 8 بعد كدة المرة الجاية لما أقابل العنصر 10 وأنا بلف في ال loop لازم ألاقي في array C قيمة 7 علشان أحط العنصر المرة دي في المكان 7 علشان كدة خطوة ال decrement.
  • تاني حاجة ليه ب loop بالعكس في سطر 9؟ علشان يحتفظ بترتيب الدخل هو هو في الخرج , يعني لو عنصرين قيمتهم بتساوي x في array A واحد ظهر في array A قبل التاني هنسمي اللي ظهر الأول x1 واللي ظهر التاني x2 , عايز لما نرتب في array B العنصر x1 يظهر قبل x2 (نفس ترتيبهم في array A) , طيب ليه ما هما ليهم نفس القيمة؟ الخاصية دي إسهما stability ودي مفيدة لما تكون بترتب records عبارة عن key data and satellite data (لو مش فاهم إرجع لمقدمة الباب التاني) , وكمان مفيدة لما نستخدم ال counting sort ك subroutine لل radix sort , ال stability of counting sort مهمة جدا لل correctness of radix sort , هنشوف الكلام ده بالتفصيل بإذن الله.

في الشكل اللي قدامك توضيح لخطوات ال counting sort عل الدخل (3, 0, 3, 2, 0, 3, 5, 2):
  • في (a) بيوضح array A وخطوة ال construction ل array C اللي هي في سطر 1 : 4 في ال algorithm.
  • في (b) بيوضح خظوة تعديل array C اللي في سطر 6 : 7 في ال algorithm.
  • في (c) بيوضح تنفيذ أول iteration من ال loop اللي في سطر 9 : 11 من ال algorithm وهي بيحط اخر عنصر(3) من array A(الدخل) في مكانه الصحيح في array B(الخرج).
  • في (d) بيعمل تاني iteration مع العنصر القبل الأخير(0).
  • في (e) بيعمل تالت iteration مع العنصر القبل القبل الأخير(3).
  • في (f) يوضح ناتج ال algorithm.
في النهاية ال running time بتاع ال counting sort هو Θ(n + k) لإن ال algorithm عبارة عن:
  • عدد 2 loops عدد ال iterations فيهم k (ال loops سطر 1 و 6).
  • عدد 2 loops عدد ال iterations فيهم n (ال loops سطر 3 و 9).
وبما إن k = O(n) فال running time هيكون Θ(n) يعني linear time.

وده implementation لل Counting sort بلغة ال C :


Radix sort
تخيل انك معاك مجموعة dates (تواريخ) كل تاريخ عبارة عن (يوم - شهر - سنة) وعايز ترتبهم , الطريقة البديهية إنك بتبدأ ترتبهم بناءا على السنين , وبعدبن لو فيه elements متشابهة في السنين بترتبهم بناءا على الشهور , ولو اتشابهو كمان في الشهور بترتبهم بناءا على الأيام. يعني كمثال لو معاك ال dates دي:


أول خطوة هترتبهم بناءا على السنين بالشكل ده :


تاني خطوة تمسك الـ elements المشتركة في نفس السنة وترتبهم لوحدهم بناءا على الشهر بالشكل ده :


تالت خطوة تمسك ال elements المشتركين في نفس الشهر ونفس السنة وترتبهم لوحدهم بناءا على اليوم بالشكل ده :


وبكدة نكون رتبنا ال dates .

الطريقة دي ممكن نعملها algorithm مش بس يرتب ال dates ولكن يرتب الأرقام العادية , من خلال إنه يرتبهم في الأول بناءا على ال digit اللي لها أعلى قيمة (ال digit اللي على الشمال وده بتسمى ال most significant bit) زي ال year في مثال ال dates , وبعدين يرتبهم بناءا على ال digits الأقل في القيمة واحدة واحدة لغاية ميوصل لل least significant bit (اللي هي ال digit اللي على الشمال وبتكون قيمتها أقل قيمة) , بالطريقة دي انا بلف عدد من المرات بتساوي عدد ال digits في الأرقام وفي كل مرة بطبق sorting algorithm يرتب الأرقام بتاعتي بناءا على ال digit الحالية بس , فلو استخدمت ال counting sort هستفاد إن ال k هنا قليلة جدا (اللي هي في الحالة دي الأساس بتاع النظام , لو نظام عشري مثلا هيكون ال k بيساوي 9 , لإن أي digit قيمتها مابين 0 و 9) , فهطبق ال counting sort عدد من المرات بيساوي عدد ال digits(اللي هو بيكون constant) , فهنا أنا حليت مشكلة ال counting sort إنه كان لازم ياخد أرقام في range محدد , دلوقتي يقدر ياخد أي أرقام , ومازال ال running time يعتبر linear لإني بطبق ال counting sort عدد constant من المرات (اللي هو عدد ال digits).

مشكلة الطريقة دي إنك محتاج temporary storage كبيرة , لإنك كل مترتب على digit معينة زي ال year في المثال بتاع ال dates محتاج تفصل ال elements اللي ليهم نفس ال year علشان تقارن الشهور بتاعتهم ولو اتساوو بعض ال elements في الشهر محتاج تفصل المشتركين مع بعض علشان تقارن اليوم وهكذا. إيه الحل لمشكلة ال storage دي بقى؟ إزاي نعدل ال algorithm علشان ميحتاجش extra storage؟ الحل بسيط .. إحنا ممكن بدل منبدأ من ال most significant bit إحنا ممكن نبدأ بال least , يعني بدل مبنرتب السنين فالشهور فالأيام , ممكن نرتب الأيام فالشهور فالسنين , بشرط إن إحنا نستخدم stable sorting algorithm , بالطريقة دي مش هنحتاج extra storage , تعالى نشوف الطريقة دي على المثال اللي فات بتاع ال dates :

أول خطوة نرتب بناءا على الأيام بالشكل ده :


تاني خطوة نرتب الناتج بناءا على الشهور بالشكل ده :


تالت خطوة نرتب الناتج بناءا على الأيام بالشكل ده :


هو ده بالظبط ال Radix sort , لإن كلمة radix معناها الأساس أو أساس النظام العددي (زي مثلا النظام العشري الأساس بتاعه 10), وده لإن ال algorithm بيعتمد على الأساس ده اللي هو بيكون ال k بتاع ال counting sort في حالة إننا مستخدمين ال counting sort ك subroutine لل Radix sort , نشوف ال pseudocode بتاع ال Radix بسيط جدا عبارة عن سطرين:


  • سطر 1 بي loop حسب عدد ال digits من ال least ل ال most .
  • سطر 2 بيستخدم stable sorting algorithm علشان يرتب ال input بناءا على ال digit i .
طبعا لو استخدمنا ال counting sort في السطر 2 ال running time بتاع ال Radix sort هيكون عبارة عن Θ(d(n + k) , بحيث إن ال n + k ده ال running time بتاع ال counting sort وال d هي عدد ال digits لإن ال counting sort بيتنفذ عدد من المرات يساوي عدد ال digits . طبعا ال k بيكون قليل جدا , في النظام العشري الطبيعي مثلا بيساوي 9 , وال d كمان بيكون constant , فكدة ال running time بتاع ال Radix sort بيكون linear أو O(n) .

وده implementation لل Radix sort بلغة ال C :


Bucket sort


طبعا إحنا عارفين من الفصل التاني إن ال Insertion sort ال running time بتاعه quadratic يعني O(n2) في ال worst case , ولكن ال insertion sort فيه ميزة إنه سريع مع دخل قليل , بمعنى إنك لو هترتب 50 أو 100 عنصر عمليا ال insertion sort بيكون أسرع من ال Merge وال Heap وال Quick , علشان كدة لو عدلت ال Quick أو ال Heap أو ال Merge وخليته يستخدم نفسه لما يكون الدخل كبير ويستخدم ال insertion مع الدخل القليل ال algorithm الناتج هيكون أسرع عمليا , والطريقة دي شفناها في الفصل السابع مع ال Quick sort .

ال Bucket sort بيعتمد إنه يقسم الدخل لمجموعات صغيرة, العناصر اللي جوة كل مجموعة مش مرتبة ولكن المجموعات نفسها من برة مترتبة(يعني كمثال لو الدخل عبارة عن مجموعة عناصر قيم كل عنصر في range من 1 ل 100 فممكن نقسم الدخل ده ل 10 مجموعات المجموعة الأولى فيها العناصر اللي قيمها في ال range من 1 ل 10 والمجموعة التانية فيها العناصر اللي قيمها في ال range من 11 ل 20 وهكذا) , ويرتب كل واحدة لوحدها بال insertion sort وبعدين يضم المجموعات على بعض (concatenation) فيطلع الناتج ال sorted array.

مشكلة ال Bucket sort إنه عليه constraint إنه بيفرض إن الدخل اللي جايله بيخضع لشرطين:
  • الدخل عبارة عن أرقام في ال range من 0 ل 1 (يعني أرقام عشرية  floating-point).
  • الأرقام دي متوزعة uniformally وده له تعريف رياضي ولكن تسهيلا ده معناه إن الأرقام متوزعة في ال range ده بشكل منتظم (يعني كمثال لو ال input عبارة عن 10 عناصر قيمهم في ال range من 1 ل 100 فلو العناضر متوزعة uniformally نقدر نتوقع بشكل كبير إن عنصر هيكون قيمته في ال range من 1 ل 10 وعنصر هيكون قيمته من 11 ل 20 وعنصر هيكون قيمته من 21 ل 30 وهكذا , متوزعين على ال range بشكل منتظم).
ال Bucket sort من إسمه هيقسم الدخل على مجموعة من ال buckets (كلمة bucket يعني دلو) وبعدبن يرتب كل bucket لوحده وبعدين يعمل concatenation للعناصر اللي في ال buckets فتنتج ال array المترتبة , هو بيستخدم في تمثيل ال bucket برمجيا linked list , ودي data structure هنشوفها بالتفصيل في الباب التالت الفصل العاشر , اللي محتاج تعرفه دلوقتي عن ال linked list إنها data structure أقدر أخزن فيها عناصر وأعمل عمليات عادية زي insertion أو deletion وهكذا.

نشوف ال pseudocode بتاع ال Bucket sort :


  • هنا بيعتمد إنه معاه array of buckets أو array of linked lists إسمها B وعدد ال buckets نفس عدد ال input .
  • سطر 1 بيخزن عدد عناصر ال input .
  • سطر 2 : 3 بيلف على عناصر ال input ويخزن كل عنصر في ال bucket بتاعه أو ال linked list .
  • سطر 4 : 5 بيلف على ال buckets يرتبها باستخدام ال insertion sort .
  • سطر 6 بيجمع (concatenate) ال buckets مع بعض ويكون ده الناتج النهائي .
هنا بيضرب قيمة العنصر في عدد عناصر الدخل ويحسب ال floor للناتج ويكون ده هو ال index بتاع ال bucket بتاع العنصر الحالي , كمثال لو n = 100 و A[i] = 0.55 فهيخزن ال 0.55 في ال bucket للي ال index بتاعها 55 , لو ال A[i] = 0.33 فهيخزن ال 0.33 في ال bucket اللي ال index بتاعها 33 وبكدة لما يعمل concatenation في سطر 6 ال 0.33 هتيجي قبل ال 0.55 ولو عنصرين جم في نفس ال bucket هيترتبوا باستخدام ال insertion sort في سطر 5 .
الصورة دي بيوضح فيها إزاي ال Bucket sort بيشتغل على دخل مكون من 10 عناصر.


ال expected running time بتاع ال Bucket sort يعتبر linear أو Θ(n) وده ليه إثبات طويل.

وده implementation لل Bucket sort بلغة ال C :








 
 






تعليقات

المشاركات الشائعة من هذه المدونة

الفصل الرابع

الفصل السادس

الفصل الثاني عشر