الفصل العاشر
Elementary
Data Structures
هياكل
البيانات البدائية
مقدمة
في
الفصل ده بإذن الله هنتعرض لأبسط
Data
structure, ولكن
في الحقيقة فهم الـ Data
structures البسيطة
دي بيساعد في فهم مواضيع كتير في الـ
Computer
science بشكل
عام,
لإن
الـ Data
structures اللي
هنشوفها في الفصل ده بتستخدم في Problems
كتير
جدا.
في
الفصل ده هنشوف 4
هياكل
بيانات:
- Stack.
- Queue.
- Linked List.
- Rooted Tree.
Stack
أحنا
اتعرضنا في الفصل السادس لل Heap
وقلنا
ان معنى كلمة Heap
هي
كومة
(شوية
حاجات مرمين فوق بعض بشكل
غير منظم),
كلمة
Stack
بقى
معنى كومة (شوية
حاجات فوق بعض ولكن بشكل
منظم),
والطبيعي
جدا لو عندك مجموعة عناصر فوق بعض ومعاك
عنصر جديد أسهل مكان تحط فيه العنصر الجديد
هو من فوق,
وكمان
لو هتاخد عنصر من العناصر دي أسهل عنصر
تاخده هو العنصر اللي من فوق
خالص (اللي
هو اخر عنصر انت حطيته في الـ Stack),
وهي
دي الـ Stack:
عبارة
عن Data
structure بتوفر
عمليتين:
- Push.
- Pop.
عملية
الـ Push
هي
بتاخد عنصر وتحطه فوق الـ Stack,
وعملية
الـ Pop
بتجبلك
اخر عنصر اتحط في الـ Stack,
علشان
كدة بيسموها Last
In First Out أو
اختصارا LIFO
يعني
اخر عنصر تحطه هو أول عنصر تاخده.
بالنسبة
لواحد أول مرة يتعرض لل Stack
هيكون
الموضوع بالنسباله غريب,
وهيسأل:
هو
إيه الـ Data
اللي
انا ممكن أحتاج أخزنها في حاجة زي كدة؟
الإجابة:
كتير.
مع
إن الـ Stack
بسيطة
جدا إلا إن فيه Problems
كتير
بتتحل بيها,
كمثال
بسيط:
وانت
بتتعامل مع الـ web,
بتتحرك
ما بين الصفحات,
هنا
احنا محتاجين نخزن الصفحات اللي انت
بتتصفحها,
طبيعة
التخزين هنا محتاجة Stack
لانك
لما بتحتاج ترجع خطوة بترجع لاخر صفحة
انت كنت فاتحها (Last
In First Out), اخر
صفحة انت دخلتها (Last
in) هي
أول صفحة انت بتحتاجها (First
out).
علشان
نمثل الـ Stack
إحنا
محتاجين:
- الـ Array اللي هنخزن فيها العناصر.
- أي حاجة تشاور على اخر مكان واقفين عنده(المكان اللي نقدر نعمل منه Pop) وهنسميه top[s] وقيمته بداية بتكون 0 (في حالة الـ one-index based أما في الـ programming قيمته بداية بتكون ب -1 في معظم اللغات الـ zero-index based).
- عمليتين الـ Push وال Pop.
طبعا
الـ Stack
بيكون
ليها Size
محدد,
فلما
تيجي تعمل Push
ممكن
يكون الـ Stack
مفهش
مساحة وبالتالي تحصل مشكلة بنسميها
Overflow,
ولما
تيجي تعمل Pop
ممكن
تكون الـ Stack
مفهاش
عناصر أصلا وبالتالي تحصل مشكلة بنسميها
Underflow.
نشوف
بقى ازاي ممكن نمثل الـ Stack
ك
Pseudocode.
- أول Algorithm هو Stack_Empty وده بي check إذا كان الـ Stack فاضي ولا فيه عناصر, وطبعا الـ Stack بيكون empty لما يكون top[s] (اللي بيشاور على اخر عنصر حصله Push في ال Stack) بيساوي صفر.
- تاني Algorithm هو عملية الـ Push وده بيزود الـ top[s] ب 1 وبعدين يحط العنصر الجديد في المكان اللي بيشاور عليه top[s]. لاحظ انه هنا مش بي check الـ Overflow ولكن في أي code تكتبه انت المفروض انك ت check.
- تالت Algorithm هو عملية الـ Pop وده بي check في الاول لو الـ Stack فاضية يبقى Underflow لو مش فاضية بيقلل الـ top[s] ب 1 علشان يشاور على العنصر اللي قبل العنصر اللي هنعمله Pop وبعدين يرجع العنصر اللي بيشاور عليه top[s] + 1 (العنصر اللي كان بيشاور عليه قبل منقلله).
والشكل
ده بيوضح العمليات اللي بتحصل على الـ
Stack.
في
(a)
بيوريك
العناصر الموجودة في الـ Stack
وهما
عددهم 4
وبالتالي
top[s] =
4 , وفي
(b)
بيوريك
عملية الـ Push
مرتين
للعناصر 17
وبعدين
3 , وفي
(c)
بيوريك
عملية الـ Pop.
وده
implementation
لل
Stack
بلغة
الـ C.
Queue
كلمة
Queue
يعني
طابور,
وأي
طابور ليه بداية
هنسميها Head
(رأس)
ونهاية
هنسميها Tail
(ذيل),
الطبيعي
جدا لما يجي حد جديد بيقف في نهاية الطابور
عند الـ Tail
ولما
نيجي ناخد حد من الطابور علشان ياخد الخدمة
اللي مستنيها بناخد اللي في الـ Head
(أول
واحد دخل الطابور),
,وهي
دي الـ Queue:
عبارة
عن Data
structure بتوفر
عمليتين:
- Enqueue.
- Dequeue.
عملية
الـ Enqueue
بتضيف
عنصر عند الـ Tail
وعمليى
الـ Dequeue
بتاخد
العنصر اللي عند الـ Head
, علشان
كدة الـ Queue
بنسميها
First
In First Out أو
اختصارا FIFO
(أول
واحد يدخل هو أول واحد يخرج),
طبعا
الـ Queue
بتستخدم
في Problems
كتير
جدا,
كمثال
بسيط:
أي
Server
بيجيله
requests
من
الـ Clients
علشان
يأديلهم خدمة محددة,
من
الـ algorithms
المشهورة
في تنظيم عملية الـ Scheduling
للـ
requests
دي
(مين
ياخد الخدمة قبل مين)
algorithm إسمه
First
Come
First
Serve
يعني
اللي ييجي الأول ياخد الخدمة الأول,
وده
واضح جدا انه ممكن يتنفذ بال Queue.
علشان
نمثل الـ Queue
إحنا
محتاجين:
- الـ Array اللي هنخزن فيها العناصر وليها Size محدد بنرمزله Length[Q].
- أي حاجة تشاور على الـ Head بنرمزلها Head[Q] وحاجة تاني تشاور على الـ Tail بنرمزلها Tail[Q] , بداية بنخلي قيمتهم واحدة وده معناه إن الـ Queue مفهاش عناصر لسة. في الـ implementation اللي هنشوفه الـ Head بيشاور على أول عنصر دخل في الـ array(المكان اللي انت هتعمله Dequeue مباشرة) ولكن الـ Tail بيشاور على مكان فاضي(المكان اللي هتعمل فيه Enqueue مباشرة).
- عمليتين الـ Enqueue وال Dequeue.
طبعا
الـ Queue
ممكن
يحصل عليها Overflow
أو
Underflow
ولكن
هو هنا بيتجاهلهم.
نشوف
بقى ازاي ممكن نمثل الـ Queue
ك
Pseudocode.
- أول algorithm هو الـ Enqueue بيحط ال عنصر اللي جاله في المكان اللي بيشاور عليه Tail[Q] وبعدين يزود الـ Tail[Q] ب 1 (في حالة ان الـ Tail[Q] قيمته بتساوي الـ Length[Q] فبيخليه يشاور على أول عنصر في الـ Array وبالتالي هو كدة زوده 1 بطريقة circular).
- الـ algorithm التاني هو الـ Dequeue بيخزن العنصر اللي بيشاور عليه Head[Q] وبعدبن يزود الـ Head[Q] بطريقة circular وبعدين يرجع العنصر اللي خزنه(اللي كان بيشاور عليه الـ Head[Q] قبل ميزوده).
والشكل
ده بيوضح العمليات اللي بتحصل على الـ
Queue.
في
(a)
بيوريك
العناصر اللي موجودة في الـ Queue
وعددهم
5. الـ
Head[Q]
بياور
على الغنصر 15
وده
معناه ان العنصر 15
هو
أول عنصر دخل الـ array
مابين
العناصر الموجودة.
وفي
(b)
بيوريك
عملية الـ Enqueue
للعناصر
17 بعدين
3 بعدين
5
بالترتيب
ده ,
وفي
(c)
بيوريك
عملية الـ Dequeue.
وده
implementation
لل
Queue
بلغة
الـ C.
Linked
List
كلمة
linked
معناها
مرتبط أو متصل ,
وبالتالي
Linked
list
معناها
list
متصلة
ببعضها,
طيب
إيه الجديد أي list
متصلة
ببعضها بشكل ما؟ ده صحيح.
الـ
array
مثلا
هي عبارة عن عنوان مكان في الـ memory
متخزن
فيه أول عنصر في الـ array
وباقي
العناصر متخزنة في الأماكن اللي بعد
المكان ده مباشرة,
يعني
الـ array
بتتخزن
في block
من
الـ memory
مش
في أماكن متفرقة,
علشان
كدة الـ array
لازم
تحدد حجمها(عدد
عناصرها)
وانت
بتعمل declaration
علشان
يقدر يحجزلك block
في
الـ memory
على
قد الحجم اللي انت محتاجه.
الـ
linked
list
بقى
عبارة عن مجموعة عناصر متفرقة في الـ
memory كل
عنصر فيه pointer
بيشاور
على عنوان العنصر اللي بعديه علشان كدة
اسمها linked,
بالطريقة
دي الـ linked
list
انت
مش بتحتاج تحددلها حجم وانت بتعرفها لانها
dynamic
, كل
متحتاج تضيف عنصر جديد انت محتاج أي مكان
فاضي في الـ memory
يشيل
العنصر ده وهتشاور عليه من العنصر اللي
قبلبه في الـ list,
أما
الـ array
علشان
لازم تتخزن في block
فاضي
فلازم تحددله مسبقا حجمها قد إيه.
الـ
linked
list
فيه
منها أنواع:
- Singly-linked list.
ودي
list
متصلة
في اتجاه
واحد
(single)
يعني
كل عنصر فيه pointer
بيشاور
على العنصر اللي بعديه
لكن العنصر مفهش أي معلومات عن العنصر
اللي قبله في الـ list.
وفي
الحالة دي بيكون عندي pointer
اسمه
head
بيشاور
على أول عنصر في الـ list
واللي
منه ممكن أوصل لباقي العناصر,
وكل
عنصر (العنصر
في الـ linked
list بنسميه
node)
بيكون
فيه الـ key
و
next وده
الـ pointer
اللي
بيشاور على الـ node
اللي
بعده وأي satellite
data.
- Doubly-linked list.
ودي
list
متصلة
في الاتجاهين
(double)
يعني
كل node
فيها
pointer
بيشاور
على الـ node
اللي
بعده
و pointer
كمان
بيشاور على الـ node
اللي
قبله.
وفي
الحالة دي بيكون عندي head
بيشاور
على أول node
في
الـ list
وكمان
tail
بيشاور
على اخر node
في
الـ list.
وال
node
بيكون
فيها الـ key
و
next
و
previous
ودول
الـ pointers
اللي
بيشاورو على الـ node
اللي
بعد
وال node
اللي
قبل
بالترتيب وأي satellite
data.
[وده
النوع اللي هنستخدمه في توضيح الـ
operations].
- Sorted.
ودي
list
مرتبة
حسب الـ key
يعني
الـ head
بيكون
الـ node
صاحب
أصغر
key
في
الـ list
وال
tail
بيكون
الـ node
صاحب
أكبر
key
في
الـ list.
- Unsorted.
ودي
list غير
مرتبة.
- Circular.
الطبيعي
ان الـ head
بتاع
الـ list
الـ
previous
بتاعه
بيشاور على Null
لان
مفيش حاجة قبله وال tail
بتاع
الـ list
الـ
next
بتاعه
بيشاور على Null
لان
مفيش حاجة بعده,
أما
الـ circular
list
بتكون
عبارة عن ring
(حلقة)
الـ
previous
بتاع
الـ head
بيشاور
على الـ tail
وال
next
بتاع
الـ tail
بيشاور
على الـ head.
نبدأ
نشوف إزاي ممكن ننفذ العمليات المختلفة
زي الـ Search
وال
Insert
وال
Delete
على
الـ Linked
List.
- Searching a linked list.
معانا
key
معين
وعايزين نعرف هل فيه node
في
الـ list
بنفس
الـ key
ولا
لأ, لو
لقينا node
بنفس
الـ key
بنرجع
pointer
ليها
ولو ملقناش بنرجع Null
. الموضوع
بسيط معايا pointer
اسمه
head
بيشاور
على أول node
في
الـ list
وفكل
node فيه
pointer
اسمه
next
بيشاور
على الـ node
اللي
بعديه,
وبالتالي
أقدر أشاور على ال head
ب
pointer
جديد
(علشان
أقدر أحركه بدون ما أغير حاجة في الـ list)
وأبدا
أحرك الـ pointer
الجديد
ده على الـ nodes
بان
كل مرة أساويه بال next
بتاعه
فيشاور على الـ node
اللي
بعده واطلع من الـ loop
دي
في حالة من اتنين:
اما
ان الـ node
بقت
بتشاور على Null
(يعني
انا عديت على الـ list
كلها)
أو
ان الـ key
بيساوي
الـ key
اللي
انا بدور عليه ,
وفي
النهاية ارجع الـ pointer
الجديد
اللي هيكون ليه حالتين:
اما
وقف على الـ node
اللي
ليها نفس الـ key
اللي
بدور عليه أو عدى عال list
كلها
وبقى ب Null
وبالتالي
انا ملقتش أي node
بنفس
الـ key
وفعلا
عايز أرجع Null.
هنا
في السطر الأول بيشاور عال head
ب
pointer
جديد
وفي سطر 2
و
3 بيعدي
على الـ list
كلها
وبيطلع من الـ loop
في
حالة من الحالتين اللي شرحناهم وفي سطر
4 بيرجع
الـ pointer
الجديد
اللي ليه حالتين زي ماقلنا.
علشان
الـ algorithm
ممكن
يعدي على الـ list
كلها
فال running
time
بتاعه
هو O(n)
في
الـ worst
case.
- Inserting into a linked list.
فيه
أكتر من مكان ممكن ت insert
عنصر
جديد فيه:
ممكن
مثلا في بداية
الـ linked
list أو
نهايتها
أو بعد
عنصر
محدد,
هنا
هنشوف إزاي ن insert
عنصر
في بداية الـ list
. معانا
pointer ل
node
وعايزين
ن insert
الـ
node دي
في بداية الـ list
, بداية
الـ list
يعني
الـ head
, هنا انا محتاج انفذ 4 خطوات : أول خطوة اني اخلي ال next بتاع ال node الجديدة يشاور على ال head بتاع ال list وتاني خطوة ان ال previous بتاع ال node لجديدة يشاور عاى null لان مفيش قبله حاجة , تالت خطوة اني اعمل update لل head اخليه يشاور على ال node الجديدة لانها خلاص بقت ال head ولكن قبل الخطوة دي لازم اخلي ال previous بتاع ال head يشاور على ال node الجديدة ده في حالة ان ال head مش بيساور على Null يعني ال list فيها عناصر.
هنا
في السطر الأول بيخلي الـ next
بتاع
الـ node
الجديدة
يشاور على الـ head
, وهنا
متفرقش اذا كان الـ head
ب
Null أو
بقيمة لانه لو بقيمة فانا عايوز اشاور
على الـ head
ولو
ب Null
فانا
عايو أشاور على Null
, في
سطر 2
بيختبر
لو الـ head
مش
ب Null
بيخلي
الـ previous
بتاعه
يشاور على الـ node
الجديدة(سطر
3) , سطر
4 بيخلي
الـ head
بتاع
الـ list
يشاور
على الـ node
الجديدة,
وسطر
5 بيخلي
الـ previous
بتاع
الـ node
الجديدة
يشاور على Null
لانه
الـ head
وبالتالي
مفيش حاجة قبله.
الـ
running
time
هنا
هو O(1)
يعني
constant
لان
كل العمليات بتتنفذ في constant
time ومش
بن loop
على
أي حاجة.
- Deleting from a linked list.
في
عملية الـ delete
معانا
node
معينة
عايزين نحذفها من الـ list
, هنا
معانا الـ node
نفسها
(يعني
pointer
بيشاور
عليها)
مش
الـ key
بتاعها
, لان
لو معانا الـ key
كنا
هنحتاج نعمل search
في
الاول لغاية منوصل لل node
نفسها
علشان نحذفها,
في
العملية دي احنا عايزين نعمل عمليتين:
الاولى
اننا نخلي ال next
بتاع
الـ node
اللي
قبلها يشاور على الـ node
الي
بعدها والتانية اننا نخلي الـ previous
بتاع
الـ node
اللي
بعدها يشاور على الـ node
اللي
قبلها وبكدة نكون شلناها من الـ list
بأمان.
من
سطر 1
ل
3 بينفذ
العملية الأولى:
سطر
1 بيختبر
اذا كان الـ node
اللي
عايزين نحذفها ليها previous
ولا
لأ , لو
ليها بينفذ العملية زي ما وصفنا ,
لو
ملهاش ده معناه ان الـ node
اللي
احنا عايزين نحذفها هي الـ head
بتاع
الـ list
وبالتالي
انا عايز أغير الـ head
اخليه
الـ next
بتاع
الـ node
اللي
هحذفها.
سطر
4 و
5 بينفذ
العملية التانية:
سطر
4 بيختبر
اذا كان الـ node
اللي
عايز احذفها ليها next
ولا
لا , لو
ليها فانا بنفذ العملية زي ما وصفنا ,
لكن
لو ملهاش فده معناه انها كانت الـ tail
بتاع
الـ list
وبالتالي
مش محتاج أعمل حاجة.
الـ
running
time
هنا
هو O(1)
أو
constant
لكن
لو هيجيلي key
مش
node
فهضطر
أعمل search
في
الأول وبالتالي هيكون الـ running
time يعتبر
linear أو
O(n) في
الـ worst
case.
وده
شكل توضيحي بيوضح العمليات بتتنفذ على
linked
list.
الشكل
(a) بيوضح
Linked
list فيها
4 nodes ,
الشكل
(b) بيوضح
عملية insert
ل
node الـ
key
بتاعها
25 ,
والشكل
(c) بيوضح
عملية delete
لل
node
الرابعة
في الـ list
اللي
الـ key
بتاعها
4
وده
implementation
لل
Linked
list باستخدام
لغة الـ C.
Representing
Rooted Trees
طريقة
representation
الـ
Linked
List
اللي
شفناها حالا (باستخدام
objects و
pointers)
بتستخدم
في representation
لهياكل
بيانات تانية منها الـ Trees
أو
بالأخص الـ Rooted
trees.
الشكل
اللي قدامك عبارة عن Binary
tree
ودي
مش أول مرة نشوفها ,
إحنا
اتعرضنا لل Binary
tree في
الفصل السادس في الـ Mergesort
وعملنا
representation
ليها
باستخدام Array.
الشكل
اللي قدامك عبارة عن مجموعة nodes
متصلة
ببعض عن طريق pointers
, كل
node
عبارة
عن object
فيه
أي data
ومجموعة
pointers
بيربطوه
في الـ Tree
, مجموعة
الـ pointers
دي
بتختلف حسب نوع الـ Tree
مثلا
في الـ Binary
Tree هما
3 بس
لان كل node
ليها
left
child و
right
child و
parent ,
فيه
node الـ
parent
بتاعها
بيساوي Null
ودي
الـ Root
node , وفيه
node الـ
left
child وال
right
child بيساوو
Null ودول
اللي بنسميهم leaves.
بالطريقة
دي نقدر نمثل أي Tree
عدد
الـ children
محدود
(bounded)
ولكن
منقدرش نستخدمها في تمثيل أي Tree
عدد
الـ children
فيها
غير محدود (unbounded)
لإن
أحنا في النهاية لازم يكون عندنا عدد محدد
من الـ children
, كمان
الطريقة دي مبتستخدمش لما يكون عدد الـ
children
كتير
حتى لو كان bounded
, لإن
هننتهي ب nodes
كتير
قيمها ب Null
ود
إهدار لل memory.
في
طريقة تانية إسمها left-child,
right-sibling
representation
ودي
زي ماانت شايف في الشكل ده
عبارة
عن Tree
الـ
node فيها
عبارة عن الـ data
و
3
pointers: الـ
parent
وال
left-child
ودول
عارفينهم ولكن التالت هو الـ right-sibling
وده
بيمثل الـ node
اللي
جنب الـ node
الحالية
وبالتالي كل level
ممكن
يكون فيه عدد unbounded
من
الـ nodes
اخر
node في
الـ level
بيكون
الـ right-sibling
ب
Null.
تعليقات
إرسال تعليق