قائمة مرتبطة

في علم الحاسوب ، تُعرَّف القائمة المتصلة بأنها مجموعة خطية من عناصر البيانات، لا يُحدد ترتيبها بموقعها الفعلي في الذاكرة، بل يشير كل عنصر إلى العنصر التالي. وهي بنية بيانات تتكون من مجموعة من العُقد التي تُمثل معًا تسلسلًا . في أبسط صورها، تحتوي كل عقدة على بيانات ، ومرجع ( أي رابط ) إلى العقدة التالية في التسلسل. تُتيح هذه البنية إمكانية إدراج أو إزالة العناصر بكفاءة من أي موضع في التسلسل أثناء التكرار. تُضيف المتغيرات الأكثر تعقيدًا روابط إضافية، مما يُتيح إدراج أو إزالة العُقد بكفاءة أكبر في أي موضع. من عيوب القوائم المتصلة أن زمن الوصول إلى البيانات يتناسب طرديًا مع عدد العُقد في القائمة. ولأن العُقد مرتبطة تسلسليًا، فإن الوصول إلى أي عقدة يتطلب الوصول إلى العقدة السابقة لها (مما يُسبب صعوبات في تقنية التوازي ). لذا، فإن الوصول الأسرع، مثل الوصول العشوائي، غير ممكن. تتميز المصفوفات بميزة أفضل في توطين البيانات في الذاكرة المؤقتة مقارنةً بالقوائم المتصلة.
تُعدّ القوائم المتصلة من أبسط هياكل البيانات وأكثرها شيوعًا. ويمكن استخدامها لتنفيذ العديد من أنواع البيانات المجردة الشائعة الأخرى ، بما في ذلك القوائم ، والمكدسات ، والطوابير ، والمصفوفات الترابطية ، وتعبيرات S ، على الرغم من أنه ليس من غير المألوف تنفيذ هياكل البيانات هذه مباشرةً دون استخدام قائمة متصلة كأساس.
تتمثل الميزة الرئيسية للقوائم المتصلة مقارنةً بالمصفوفات التقليدية في سهولة إدراج عناصر القائمة أو إزالتها دون الحاجة إلى إعادة تخصيص الذاكرة أو إعادة تنظيم البنية بأكملها، لأن البيانات لا تحتاج إلى التخزين بشكل متجاور في الذاكرة أو على القرص، بينما تُعد إعادة هيكلة المصفوفة أثناء التشغيل عمليةً مكلفةً للغاية. في المصفوفة، تُخزَّن البيانات في الذاكرة بشكل متجاور، أي تُخزَّن البيانات في كل موقع ذاكرة فارغ متتالي. أما في القوائم المتصلة، فلا تُخزَّن البيانات في مواقع ذاكرة متجاورة، بل يكون ترتيب العقدة التي تحمل مرجعًا إلى عنوان ذاكرة العقدة الأخرى تسلسليًا. تسمح القوائم المتصلة بإدراج العقد وإزالتها في أي نقطة من القائمة، وبعدد ثابت من العمليات، وذلك عن طريق الاحتفاظ بالرابط السابق للرابط المُضاف أو المُزال في الذاكرة أثناء اجتياز القائمة.
من ناحية أخرى، بما أن القوائم المرتبطة البسيطة بحد ذاتها لا تسمح بالوصول العشوائي إلى البيانات أو أي شكل من أشكال الفهرسة الفعالة، فإن العديد من العمليات الأساسية - مثل الحصول على العقدة الأخيرة من القائمة، أو العثور على عقدة تحتوي على بيانات معينة، أو تحديد المكان الذي يجب إدراج عقدة جديدة فيه - قد تتطلب المرور عبر معظم أو كل عناصر القائمة.
تاريخ
يسبق نظام سرد المعلومات المترابط العصر الرقمي بأكثر من ألفي عام، إذ يعود أصله إلى العصر الهوميري على أبعد تقدير، عندما كان النساخ الذين ينسخون لفائف البردي يُقدمون للقراء والناسخين اللاحقين إرشادات حول الترتيب المقصود للقراءة، وذلك بكتابة الكلمة الأولى في نهاية كل لفافة، والتي عُرفت لاحقًا بالاسم اللاتيني المضارع reclamans (بصيغة الجمع reclamantes ؛ وتعني حرفيًا "الصراخ للخلف" أو ما يُقابلها اسميًا )، من اللفافة التالية في الترتيب. [ 1 ] وقد عادت هذه الممارسة للظهور خلال السنوات الأولى للطباعة الآلية للكتب في أوروبا، حيث كان من الشائع أن يُدرج الطابعون في نهاية كل صفحة مطبوعة " كلمة إرشادية " تُطابق الكلمة الأولى في الصفحة التالية في الترتيب. وقد حسّنت هذه الممارسة قدرة الطابعين على التحقق من أنهم يطبعون كل صفحة من صفحات " الوجه الخلفي " (الجانب الأيسر للقارئ في اللغات الأوروبية) على ظهر الصفحة الأمامية التي تسبقها بشكل صحيح ، وأنهم أثناء التجميع والتجليد يُرتبون كل صفحة أمامية بحيث تتبع الصفحة الخلفية التي تسبقها بشكل صحيح. [ 2 ]
طُوِّر أول تطبيق للقوائم المتصلة في سياق علوم الحاسوب في الفترة ما بين عامي 1955 و1956 على يد ألين نيويل وكليف شو وهربرت أ. سيمون في مؤسسة راند وجامعة كارنيجي ميلون ، حيث مثّلت هذه القوائم البنية الأساسية للبيانات في لغة معالجة المعلومات (IPL). وقد استخدم المؤلفون لغة IPL لتطوير العديد من برامج الذكاء الاصطناعي المبكرة ، بما في ذلك آلة نظرية المنطق، وبرنامج حل المشكلات العامة ، وبرنامج شطرنج حاسوبي. نُشرت تقارير عن أعمالهما في مجلة IRE Transactions on Information Theory عام 1956، وفي العديد من وقائع المؤتمرات بين عامي 1957 و1959، بما في ذلك وقائع المؤتمر المشترك الغربي للحاسوب عامي 1957 و1958، ووقائع مؤتمر معالجة المعلومات (وقائع المؤتمر الدولي الأول لليونسكو حول معالجة المعلومات) عام 1959. ويظهر المخطط الكلاسيكي، الذي يتألف من كتل تمثل عقد القوائم مع أسهم تشير إلى عقد القوائم المتتالية، في كتاب "برمجة آلة نظرية المنطق" لنيويل وشاو، المنشور في وقائع المؤتمر المشترك الغربي للحاسوب، فبراير 1957. وقد مُنح نيويل وسيمون جائزة تورينج من جمعية آلات الحوسبة عام 1975 لمساهماتهما الأساسية في الذكاء الاصطناعي، وعلم نفس الإدراك البشري، ومعالجة القوائم. وقد دفعت مشكلة الترجمة الآلية لمعالجة اللغة الطبيعية فيكتور ينجفي، من معهد ماساتشوستس للتكنولوجيا (MIT)، إلى استخدام القوائم المرتبطة كهياكل بيانات في لغة البرمجة COMIT الخاصة به لأبحاث الحاسوب في مجال اللغويات . ظهر تقرير عن هذه اللغة بعنوان "لغة برمجة للترجمة الآلية" في مجلة الترجمة الآلية عام 1958.
ومن بين الظهورات المبكرة الأخرى للقوائم المرتبطة، كان هانز بيتر لونه الذي كتب مذكرة داخلية لشركة IBM في يناير 1953 اقترح فيها استخدام القوائم المرتبطة في جداول التجزئة المتسلسلة. [ 3 ]
لغة ليسب (LISP) ، اختصارًا لـ "معالج القوائم"، ابتكرها جون مكارثي عام 1958 أثناء دراسته في معهد ماساتشوستس للتكنولوجيا، وفي عام 1960 نشر تصميمها في ورقة بحثية في مجلة " اتصالات رابطة آلات الحوسبة" (Communications of the ACM) بعنوان "الدوال التكرارية للتعبيرات الرمزية وحسابها بواسطة الآلة، الجزء الأول". وتُعد القائمة المتصلة إحدى أهم هياكل البيانات في ليسب.
بحلول أوائل الستينيات، ترسخت أهمية كل من القوائم المتصلة واللغات التي تستخدم هذه البنى كتمثيل أساسي للبيانات. نشر بيرت غرين من مختبر لينكولن التابع لمعهد ماساتشوستس للتكنولوجيا مقالًا استعراضيًا بعنوان "لغات حاسوبية لمعالجة الرموز" في مجلة IRE Transactions on Human Factors in Electronics في مارس 1961، لخص فيه مزايا أسلوب القوائم المتصلة. وفي وقت لاحق، نُشر مقال استعراضي آخر بعنوان "مقارنة بين لغات الحاسوب لمعالجة القوائم" بقلم بوبرو ورافائيل في مجلة Communications of the ACM في أبريل 1964.
استخدمت العديد من أنظمة التشغيل التي طورتها شركة Technical Systems Consultants (التي كان مقرها الأصلي في ويست لافاييت، إنديانا، ثم انتقلت لاحقًا إلى تشابل هيل، كارولاينا الشمالية) قوائم مرتبطة أحادية كبنية للملفات. يشير مدخل الدليل إلى القطاع الأول من الملف، ويتم تحديد مواقع الأجزاء اللاحقة من الملف عن طريق تتبع المؤشرات. من بين الأنظمة التي استخدمت هذه التقنية: Flex (لمعالج Motorola 6800 )، وmini-Flex (للمعالج نفسه)، وFlex9 (لمعالج Motorola 6809 ). كما استخدمت نسخة معدلة طورتها شركة TSC لصالح شركة Smoke Signal Broadcasting في كاليفورنيا، وقامت بتسويقها، قوائم مرتبطة ثنائية بنفس الطريقة.
استخدم نظام التشغيل TSS/360 ، الذي طورته شركة IBM لأجهزة System 360/370، قائمة مرتبطة مزدوجة لفهرس نظام الملفات . وكان هيكل الدليل مشابهًا لنظام Unix، حيث يمكن أن يحتوي الدليل على ملفات وأدلة أخرى ويمتد إلى أي عمق.
المفاهيم الأساسية والتسميات
يُطلق على كل سجل في القائمة المرتبطة غالبًا اسم "عنصر" أو " عقدة ".
يُطلق عادةً على الحقل الذي يحتوي على عنوان العقدة التالية في كل عقدة اسم "الرابط التالي" أو "المؤشر التالي". أما الحقول المتبقية فتُعرف باسم حقول "البيانات" أو "المعلومات" أو "القيمة" أو "الحمولة" أو "البيانات".
رأس القائمة هو أول عنصر فيها. أما ذيل القائمة فقد يشير إما إلى باقي عناصر القائمة بعد الرأس، أو إلى آخر عنصر فيها. في لغة ليسب وبعض اللغات المشتقة منها، يُطلق على العنصر التالي اسم " cdr " (يُنطق /kʊd.əɹ/ )، بينما يُطلق على محتوى عنصر الرأس اسم "car".
قائمة مرتبطة بشكل فردي
تحتوي القوائم المرتبطة أحادية الاتجاه على عقد لها حقل "القيمة" بالإضافة إلى حقل "التالي" الذي يشير إلى العقدة التالية في سلسلة العقد. تشمل العمليات التي يمكن إجراؤها على القوائم المرتبطة أحادية الاتجاه الإضافة والحذف والتنقل.

يوضح الكود التالي المكتوب بلغة C كيفية إضافة عقدة جديدة بالقيمة "value" إلى نهاية قائمة مرتبطة أحادية:
#include <stdlib.h>// كل عقدة في القائمة المتصلة هي بنية. العقدة الرئيسية هي العقدة الأولى في القائمة.typedef struct Node {قيمة عددية صحيحة ؛struct Node * next ;} عقدة ؛Node * addNodeToTail ( Node * head , int value ) {// قم بتعريف مؤشر العقدة وتهيئته ليشير إلى العقدة الجديدة (أي أنه سيحتوي على عنوان ذاكرة العقدة الجديدة) التي ستتم إضافتها إلى نهاية القائمة.Node * temp = ( Node * ) malloc ( sizeof * temp ); /// 'malloc' in stdlib.temp -> value = value ; // أضف البيانات إلى حقل القيمة الخاص بالعقدة الجديدة.temp -> next = NULL ; // تهيئة الروابط غير الصالحة إلى قيمة فارغة.إذا لم يكن الرأس موجودًا {head = temp ; // إذا كانت القائمة المرتبطة فارغة (أي أن مؤشر عقدة الرأس هو مؤشر فارغ)، فاجعل مؤشر عقدة الرأس يشير إلى العقدة الجديدة.} آخر {Node * p = head ; // قم بتعيين مؤشر العقدة الرئيسية إلى مؤشر العقدة 'p'.بينما ( p -> التالي ) {p = p -> next ; // اجتياز القائمة حتى تصبح p هي العقدة الأخيرة. تشير العقدة الأخيرة دائمًا إلى NULL.}p -> next = temp ; // اجعل العقدة الأخيرة السابقة تشير إلى العقدة الجديدة.}return head ; // إرجاع مؤشر عقدة الرأس.}قائمة مرتبطة ثنائياً
في "القائمة المرتبطة ثنائياً"، تحتوي كل عقدة، بالإضافة إلى رابط العقدة التالية، على حقل رابط ثانٍ يشير إلى العقدة "السابقة" في التسلسل. ويمكن تسمية هذين الرابطين "للأمام" و"للخلف"، أو "التالي" و"السابق".

تتيح تقنية تُعرف باسم الربط XOR تنفيذ قائمة مرتبطة ثنائياً باستخدام حقل ربط واحد في كل عقدة. مع ذلك، تتطلب هذه التقنية القدرة على إجراء عمليات بت على العناوين، وبالتالي قد لا تكون متاحة في بعض لغات البرمجة عالية المستوى.
Many modern operating systems use doubly linked lists to maintain references to active processes, threads, and other dynamic objects.[4] A common strategy for rootkits to evade detection is to unlink themselves from these lists.[5]
Multiply linked list
In a 'multiply linked list', each node contains two or more link fields, each field being used to connect the same set of data arranged in a different order (e.g., by name, by department, by date of birth, etc.). While a doubly linked list can be seen as a special case of multiply linked list, the fact that the two and more orders are opposite to each other leads to simpler and more efficient algorithms, so they are usually treated as a separate case.
Circular linked list
In the last node of a linked list, the link field often contains a null reference, a special value used to indicate the lack of further nodes. A less common convention is to make it point to the first node of the list; in that case, the list is said to be 'circular' or 'circularly linked'; otherwise, it is said to be 'open' or 'linear'. It is a list where the last node pointer points to the first node (i.e., the "link to next node" pointer of the last node has the memory address of the first node).

In the case of a circular doubly linked list, the first node also points to the last node of the list.
Sentinel nodes
In some implementations an extra 'sentinel' or 'dummy' node may be added before the first data record or after the last one. This convention simplifies and accelerates some list-handling algorithms, by ensuring that all links can be safely dereferenced and that every list (even one that contains no data elements) always has a "first" and "last" node.
Empty lists
An empty list is a list that contains no data records. This is usually the same as saying that it has zero nodes. If sentinel nodes are being used, the list is usually said to be empty when it has only sentinel nodes.
Hash linking
The link fields need not be physically part of the nodes. If the data records are stored in an array and referenced by their indices, the link field may be stored in a separate array with the same indices as the data records.
List handles
بما أن الإشارة إلى العقدة الأولى تتيح الوصول إلى القائمة بأكملها، يُطلق على هذه الإشارة غالبًا اسم "عنوان" أو "مؤشر" أو "مقبض" القائمة. عادةً ما تحصل الخوارزميات التي تتعامل مع القوائم المتصلة على هذه المقابض لقوائم الإدخال، ثم تُعيد مقابض القوائم الناتجة. في الواقع، في سياق هذه الخوارزميات، غالبًا ما تعني كلمة "قائمة" "مقبض قائمة". مع ذلك، في بعض الحالات، قد يكون من الأنسب الإشارة إلى قائمة بواسطة مقبض يتكون من رابطين، يشيران إلى عقدتها الأولى والأخيرة.
الجمع بين البدائل
يمكن دمج البدائل المذكورة أعلاه بشكل تعسفي في كل طريقة تقريبًا، لذلك يمكن للمرء أن يحصل على قوائم دائرية مزدوجة الارتباط بدون حراس، وقوائم دائرية أحادية الارتباط مع حراس، وما إلى ذلك.
المفاضلات
كما هو الحال مع معظم الخيارات في برمجة وتصميم الحاسوب، لا توجد طريقة مثالية لجميع الظروف. قد يكون استخدام بنية بيانات القائمة المتصلة فعالاً في حالة معينة، ولكنه قد يسبب مشاكل في حالة أخرى. فيما يلي قائمة ببعض المفاضلات الشائعة المتعلقة ببنى القوائم المتصلة.
القوائم المتصلة مقابل المصفوفات الديناميكية
| نظرة خاطفة (فهرس) | قم بالتعديل (الإدراج أو الحذف) في … | مساحة زائدة، متوسط | |||
|---|---|---|---|---|---|
| بداية | نهاية | وسط | |||
| قائمة مرتبطة | Θ( n ) | Θ(1) | Θ(1)، العنصر النهائي المعروف؛ Θ( n )، عنصر نهائي غير معروف | Θ( n ) | Θ( n ) |
| المصفوفة | Θ(1) | غير متوفر | غير متوفر | غير متوفر | 0 |
| مصفوفة ديناميكية | Θ(1) | Θ( n ) | Θ(1) المستهلكة | Θ( n ) | Θ( n ) [ 6 ] |
| شجرة متوازنة | Θ(log n) | Θ(log n) | Θ(log n ) | Θ(log n ) | Θ( n ) |
| قائمة الوصول العشوائي | Θ(log n) [ 7 ] | Θ(1) | غير متوفر [ 7 ] | غير متوفر [ 7 ] | Θ( n ) |
| شجرة المصفوفة المجزأة | Θ(1) | Θ( n ) | Θ(1) المستهلكة | Θ( n ) | Θ(√ n ) |
المصفوفة الديناميكية هي بنية بيانات تُخصص جميع عناصرها بشكل متجاور في الذاكرة، وتحتفظ بعدد العناصر الحالي. إذا تجاوزت المساحة المحجوزة للمصفوفة الديناميكية، يُعاد تخصيصها (وربما تُنسخ)، وهي عملية مكلفة.
تتميز القوائم المتصلة بعدة مزايا مقارنةً بالمصفوفات الديناميكية. فإضافة أو حذف عنصر من نقطة محددة في القائمة، بافتراض وجود مؤشر يشير إلى العقدة (قبل العقدة المراد حذفها، أو قبل نقطة الإضافة)، هي عملية ذات زمن ثابت (وإلا ستكون العملية O(n))، بينما تتطلب إضافة عنصر في مصفوفة ديناميكية في مواقع عشوائية تحريك نصف العناصر في المتوسط، وجميع العناصر في أسوأ الأحوال. ورغم إمكانية "حذف" عنصر من مصفوفة في زمن ثابت عن طريق تحديد خانته على أنها "فارغة"، إلا أن هذا يُسبب تجزئة تُعيق أداء التكرار.
علاوة على ذلك، يمكن إدراج عدد غير محدود من العناصر في قائمة مرتبطة، ولا يحدّها سوى إجمالي الذاكرة المتاحة؛ بينما ستمتلئ بنية بيانات المصفوفة الديناميكية في نهاية المطاف، مما يستدعي إعادة تخصيصها - وهي عملية مكلفة، قد لا تكون ممكنة أصلاً في حال تجزئة الذاكرة، مع العلم أنه يمكن حساب متوسط تكلفة إعادة التخصيص على عمليات الإدراج، وستظل تكلفة الإدراج الناتجة عن إعادة التخصيص ثابتة عند O(1). يُسهّل هذا الأمر إضافة العناصر إلى نهاية المصفوفة، لكن الإدراج في (أو الإزالة من) المواضع الوسطى لا يزال مكلفًا للغاية بسبب نقل البيانات للحفاظ على تسلسلها. كما قد يتطلب الأمر تغيير حجم المصفوفة التي أُزيلت منها عناصر كثيرة لتجنب إهدار مساحة كبيرة.
من ناحية أخرى، تسمح المصفوفات الديناميكية (وكذلك هياكل بيانات المصفوفات ذات الحجم الثابت) بالوصول العشوائي في وقت ثابت ، بينما تسمح القوائم المتصلة بالوصول التسلسلي فقط إلى العناصر. في الواقع، يمكن اجتياز القوائم المتصلة أحادية الاتجاه بسهولة في اتجاه واحد فقط. هذا يجعل القوائم المتصلة غير مناسبة للتطبيقات التي يكون فيها من المفيد البحث عن عنصر ما بسرعة باستخدام فهرسه، مثل خوارزمية فرز الكومة . كما أن الوصول التسلسلي على المصفوفات والمصفوفات الديناميكية أسرع من الوصول التسلسلي على القوائم المتصلة في العديد من الأجهزة، لأنها تتمتع بموقع مرجعي مثالي ، وبالتالي تستفيد بشكل جيد من تخزين البيانات المؤقت.
من عيوب القوائم المتصلة الأخرى الحاجة إلى مساحة تخزين إضافية للمراجع، مما يجعلها غير عملية في كثير من الأحيان لقوائم البيانات الصغيرة كالأحرف أو القيم المنطقية ، لأن حجم التخزين اللازم للروابط قد يتجاوز حجم البيانات بمرتين أو أكثر. في المقابل، لا تتطلب المصفوفة الديناميكية سوى مساحة البيانات نفسها (وكمية ضئيلة جدًا من بيانات التحكم). [ ملاحظة 1 ] كما أن تخصيص الذاكرة بشكل منفصل لكل عنصر جديد قد يكون بطيئًا، بل ومُهدرًا للذاكرة مع مُخصِّص بسيط، وهي مشكلة تُحل عادةً باستخدام مجمعات الذاكرة .
تحاول بعض الحلول الهجينة الجمع بين مزايا التمثيلين. تخزن القوائم المرتبطة غير الملفوفة عدة عناصر في كل عقدة، مما يزيد من أداء ذاكرة التخزين المؤقت ويقلل من استهلاك الذاكرة للمراجع. ويحقق ترميز CDR كلا الأمرين أيضًا، من خلال استبدال المراجع بالبيانات الفعلية المشار إليها، والتي تمتد من نهاية سجل المرجع.
يُعدّ تنفيذ برنامج لحلّ مسألة جوزيفوس مثالًا جيدًا يُبرز مزايا وعيوب استخدام المصفوفات الديناميكية مقابل القوائم المتصلة . تعتمد مسألة جوزيفوس على طريقة انتخابية، حيث يقف مجموعة من الأشخاص في دائرة. بدءًا من شخص مُحدد مسبقًا، يُمكن عدّ عدد مرات الدوران حول الدائرة n مرة. عند الوصول إلى الشخص رقم n ، يُزال من الدائرة ويُطلب من باقي الأعضاء إغلاقها. تُكرر هذه العملية حتى يبقى شخص واحد فقط، وهو الفائز في الانتخابات. يُظهر هذا المثال نقاط القوة والضعف في استخدام القائمة المتصلة مقارنةً بالمصفوفة الديناميكية، فإذا نُظر إلى الأشخاص كعُقد متصلة في قائمة متصلة دائرية، يتضح مدى سهولة حذف العُقد (إذ لا يتطلب الأمر سوى إعادة ترتيب الروابط بين العُقد المختلفة). مع ذلك، ستكون القائمة المتصلة غير فعّالة في إيجاد الشخص التالي المراد إزالته، وستحتاج إلى البحث في القائمة حتى تجده. من ناحية أخرى، تُعدّ المصفوفة الديناميكية غير فعّالة في حذف العُقد (أو العناصر) لأنها لا تستطيع إزالة عُقدة واحدة دون إزاحة جميع العناصر في القائمة بمقدار عنصر واحد. مع ذلك، من السهل جدًا العثور على الشخص رقم n في الدائرة من خلال الإشارة إليه مباشرةً بموقعه في المصفوفة.
تتعلق مشكلة ترتيب القوائم بتحويل تمثيل القائمة المتصلة إلى مصفوفة بكفاءة. ورغم بساطة هذه المشكلة بالنسبة للحاسوب التقليدي، إلا أن حلها باستخدام خوارزمية متوازية أمر معقد، وقد خضع للعديد من الأبحاث.
تتشابه أنماط الوصول إلى الذاكرة واستهلاك المساحة في الشجرة المتوازنة مع القائمة المرتبطة، مع توفير فهرسة أكثر كفاءة، حيث تستغرق وقتًا قدره O(log n) بدلًا من O(n) للوصول العشوائي. مع ذلك، تُعدّ عمليات الإضافة والحذف أكثر تكلفةً نظرًا لتكلفة عمليات معالجة الشجرة للحفاظ على توازنها. توجد آلياتٌ تُمكّن الأشجار من الحفاظ على توازنها تلقائيًا، مثل أشجار AVL أو الأشجار الحمراء والسوداء .
القوائم الخطية المرتبطة بشكل فردي مقابل القوائم الأخرى
على الرغم من أن القوائم المرتبطة بشكل مزدوج والقوائم الدائرية لها مزايا على القوائم الخطية المرتبطة بشكل فردي، إلا أن القوائم الخطية تقدم بعض المزايا التي تجعلها مفضلة في بعض الحالات.
القائمة الخطية المرتبطة أحادية الاتجاه هي بنية بيانات تكرارية ، لأنها تحتوي على مؤشر إلى كائن أصغر من نفس النوع. لهذا السبب، غالبًا ما تكون العديد من العمليات على القوائم الخطية المرتبطة أحادية الاتجاه (مثل دمج قائمتين، أو تعداد العناصر بترتيب عكسي) ذات خوارزميات تكرارية بسيطة للغاية، أبسط بكثير من أي حل يستخدم أوامر تكرارية . في حين أنه يمكن تكييف هذه الحلول التكرارية للقوائم المرتبطة ثنائية الاتجاه والدائرية، إلا أن الإجراءات تتطلب عمومًا وسائط إضافية وحالات أساسية أكثر تعقيدًا.
تسمح القوائم المرتبطة الخطية أحادية الاتجاه أيضًا بمشاركة الذيل ، أي استخدام الجزء الأخير المشترك من القائمة الفرعية كجزء نهائي لقائمتين مختلفتين. على وجه الخصوص، إذا أُضيفت عقدة جديدة في بداية قائمة، تظل القائمة السابقة متاحة كجزء نهائي للقائمة الجديدة - وهو مثال بسيط على بنية بيانات مستمرة . مرة أخرى، لا ينطبق هذا على الأنواع الأخرى: لا يمكن أن تنتمي عقدة واحدة إلى قائمتين دائريتين أو مرتبطتين ثنائيًا مختلفتين.
على وجه الخصوص، يمكن مشاركة عقد النهاية بين القوائم غير الدائرية المرتبطة بشكل فردي. ويمكن استخدام عقدة النهاية نفسها لكل قائمة من هذا النوع. في لغة ليسب ، على سبيل المثال، تنتهي كل قائمة صحيحة برابط إلى عقدة خاصة، يُرمز لها بـ nilأو ().
غالبًا ما تقتصر مزايا الصيغ المتقدمة على تعقيد الخوارزميات، لا على كفاءتها. فعلى سبيل المثال، يمكن عادةً محاكاة القائمة الدائرية بقائمة خطية مع متغيرين يشيران إلى العقدة الأولى والأخيرة، دون أي تكلفة إضافية.
الارتباط المزدوج مقابل الارتباط الأحادي
تتطلب القوائم المرتبطة المزدوجة مساحة أكبر لكل عقدة (إلا في حالة استخدام ربط XOR )، وتكون عملياتها الأساسية أكثر تكلفة؛ لكنها غالبًا ما تكون أسهل في التعامل معها لأنها تتيح الوصول التسلسلي السريع والسهل إلى القائمة في كلا الاتجاهين. في القائمة المرتبطة المزدوجة، يمكن إدراج أو حذف عقدة بعدد ثابت من العمليات بمعرفة عنوان تلك العقدة فقط. وللقيام بالشيء نفسه في القائمة المرتبطة المفردة، يجب معرفة عنوان المؤشر إلى تلك العقدة، والذي يكون إما مقبض القائمة بأكملها (في حالة العقدة الأولى) أو حقل الربط في العقدة السابقة . تتطلب بعض الخوارزميات الوصول في كلا الاتجاهين. من ناحية أخرى، لا تسمح القوائم المرتبطة المزدوجة بمشاركة الذيل ولا يمكن استخدامها كهياكل بيانات مستمرة .
الارتباط الدائري مقابل الارتباط الخطي
قد تكون القائمة المرتبطة دائريًا خيارًا طبيعيًا لتمثيل المصفوفات الدائرية بطبيعتها، مثل زوايا المضلع ، أو مجموعة من المخازن المؤقتة التي تُستخدم وتُحرر وفقًا لترتيب FIFO (الأول في الأول خارجًا)، أو مجموعة من العمليات التي يجب مشاركتها زمنيًا بترتيب التناوب الدوري . في هذه التطبيقات، يعمل المؤشر إلى أي عقدة كمقبض للقائمة بأكملها.
في القوائم الدائرية، يتيح المؤشر إلى العقدة الأخيرة الوصول بسهولة إلى العقدة الأولى أيضًا، عبر رابط واحد. لذا، في التطبيقات التي تتطلب الوصول إلى طرفي القائمة (مثل تنفيذ قائمة انتظار)، يسمح الهيكل الدائري بالتعامل مع الهيكل بمؤشر واحد فقط، بدلًا من مؤشرين.
يمكن تقسيم قائمة دائرية إلى قائمتين دائريتين، في وقت ثابت، بتحديد عنواني العقدة الأخيرة في كل جزء. وتتمثل العملية في تبديل محتويات حقول الربط بين هاتين العقدتين. بتطبيق العملية نفسها على أي عقدتين في قائمتين مختلفتين، يتم دمج القائمتين في قائمة واحدة. تُبسط هذه الخاصية بشكل كبير بعض الخوارزميات وهياكل البيانات، مثل خوارزمية الحافة الرباعية وخوارزمية الحافة الوجهية .
أبسط تمثيل لقائمة دائرية فارغة (عندما يكون ذلك منطقيًا) هو مؤشر فارغ، مما يشير إلى أن القائمة لا تحتوي على أي عناصر. وبدون هذا الخيار، يتعين على العديد من الخوارزميات اختبار هذه الحالة الخاصة ومعالجتها بشكل منفصل. في المقابل، يُعد استخدام المؤشر الفارغ للدلالة على قائمة خطية فارغة أكثر طبيعية، وغالبًا ما يُقلل من الحالات الخاصة.
في بعض التطبيقات، قد يكون من المفيد استخدام قوائم مرتبطة أحادية، والتي قد تتنوع بين كونها دائرية أو خطية، أو حتى دائرية ذات بداية خطية. يجب على خوارزميات البحث أو العمليات الأخرى على هذه القوائم اتخاذ احتياطات لتجنب الدخول في حلقة لا نهائية. إحدى الطرق المعروفة هي استخدام مؤشر ثانٍ يتجول في القائمة بنصف أو ضعف السرعة، وإذا التقى المؤشران عند نفس العقدة، فهذا يعني وجود حلقة.
استخدام العقد الحارسة
قد تُسهّل عقدة الحارس بعض عمليات القوائم، بضمان وجود العقدة التالية أو السابقة لكل عنصر، وأن تحتوي القوائم الفارغة على عقدة واحدة على الأقل. كما يُمكن استخدام عقدة حارس في نهاية القائمة، مع حقل بيانات مناسب، لتجنب بعض اختبارات نهاية القائمة. على سبيل المثال، عند مسح القائمة بحثًا عن عقدة بقيمة معينة x ، فإن تعيين حقل بيانات الحارس إلى x يُغني عن اختبار نهاية القائمة داخل الحلقة. مثال آخر هو دمج قائمتين مُرتبتين: إذا كانت حقول بيانات الحارس فيهما مُعيّنة إلى +∞، فإن اختيار عقدة الإخراج التالية لا يتطلب معالجة خاصة للقوائم الفارغة.
ومع ذلك، فإن عقد المراقبة تستهلك مساحة إضافية (خاصة في التطبيقات التي تستخدم العديد من القوائم القصيرة)، وقد تؤدي إلى تعقيد العمليات الأخرى (مثل إنشاء قائمة فارغة جديدة).
مع ذلك، إذا استُخدمت القائمة الدائرية لمجرد محاكاة قائمة خطية، يُمكن تجنُّب بعض هذا التعقيد بإضافة عقدة مراقبة واحدة إلى كل قائمة، بين آخر عقدة بيانات وأولها. وفقًا لهذا الاصطلاح، تتكون القائمة الفارغة من عقدة المراقبة وحدها، وتشير إلى نفسها عبر رابط العقدة التالية. يجب أن يكون مُعرِّف القائمة مؤشرًا إلى آخر عقدة بيانات، قبل عقدة المراقبة، إذا لم تكن القائمة فارغة؛ أو إلى عقدة المراقبة نفسها، إذا كانت القائمة فارغة.
يمكن استخدام الحيلة نفسها لتبسيط التعامل مع قائمة خطية مرتبطة ثنائياً، وذلك بتحويلها إلى قائمة دائرية مرتبطة ثنائياً بعقدة حارس واحدة. مع ذلك، في هذه الحالة، يجب أن يكون المؤشر عبارة عن مؤشر واحد إلى العقدة الوهمية نفسها. [ 8 ]
عمليات القوائم المرتبطة
عند التعامل مع القوائم المتصلة مباشرةً، يجب توخي الحذر لتجنب استخدام القيم التي تم إبطالها في عمليات سابقة. هذا يجعل خوارزميات إدراج أو حذف عقد القوائم المتصلة معقدة نوعًا ما. يقدم هذا القسم رمزًا زائفًا لإضافة أو إزالة العقد من القوائم المتصلة أحادية وثنائية ودائرية مباشرةً. في جميع أنحاء هذا القسم، يُستخدم مصطلح "null" للإشارة إلى علامة نهاية القائمة أو ما يُعرف بـ "الحارس" ، والذي يمكن تنفيذه بعدة طرق.
القوائم المرتبطة خطيًا
القوائم المرتبطة بشكل فردي
ستحتوي بنية بيانات العقدة على حقلين. كما يوجد متغير، firstNode ، يشير دائمًا إلى العقدة الأولى في القائمة، أو يكون فارغًا (null) في حالة القائمة الفارغة.
عقدة التسجيل { البيانات؛ // البيانات المخزنة في العقدة التالية // مرجع [ 4 ] إلى العقدة التالية، وnull للعقدة الأخيرة }قائمة السجلات { Node firstNode // يشير إلى العقدة الأولى في القائمة؛ null للقائمة الفارغة }يُعدّ اجتياز قائمة مرتبطة أحادية أمرًا بسيطًا، حيث يبدأ من العقدة الأولى ويتبع كل رابط تالٍ حتى الوصول إلى النهاية:
node := list.firstNode while node not null (do something with node.data) node := node.next
يُدرج الكود التالي عقدةً بعد عقدة موجودة في قائمة مرتبطة أحادية. يوضح الرسم التخطيطي كيفية عمله. لا يمكن إدراج عقدة قبل عقدة موجودة مباشرةً؛ بل يجب تتبع العقدة السابقة وإدراج العقدة بعدها.

دالة insertAfter( Node node, Node newNode) // إدراج العقدة الجديدة بعد العقدة newNode.next := node.next node.next := newNode
يتطلب إدراج عنصر في بداية القائمة دالة منفصلة. وهذا يتطلب تحديث العقدة الأولى (firstNode) .
دالة إدراج بداية العقدة ( قائمة القائمة ، عقدة العقدة الجديدة) // إدراج عقدة قبل العقدة الأولى الحالية newNode.next := list.firstNode list.firstNode := newNode
وبالمثل، توجد دوال لحذف العقدة التي تلي عقدة معينة، ولحذف عقدة من بداية القائمة. يوضح الرسم التخطيطي الحالة الأولى. للعثور على عقدة معينة وحذفها، يجب تتبع العنصر السابق لها.

دالة removeAfter( Node node) // إزالة العقدة التي تلي هذه العقدة obsoleteNode := node.next node.next := node.next.next تدمير العقدة القديمة
دالة removeBeginning( List list) // إزالة العقدة الأولى obsoleteNode := list.firstNode list.firstNode := list.firstNode.next // الإشارة إلى العقدة المحذوفة destroy obsoleteNode
لاحظ أن هذا removeBeginning()يتم تعيينه list.firstNodeعند nullإزالة العقدة الأخيرة في القائمة.
بما أنه لا يمكن التكرار للخلف، فإن عمليات " insertBeforeأو " الفعالة removeBeforeغير ممكنة. يتطلب إدراج عنصر في قائمة قبل عقدة معينة اجتياز القائمة، وهو ما قد يستغرق وقتًا طويلاً يصل إلى O(n).
قد تكون عملية إلحاق قائمة مرتبطة بأخرى غير فعالة ما لم يتم الاحتفاظ بمرجع إلى نهاية القائمة كجزء من بنية القائمة، لأنه يلزم اجتياز القائمة الأولى بأكملها للعثور على النهاية، ثم إلحاق القائمة الثانية بها. وبالتالي، إذا كانت كلتا القائمتين المرتبطتين خطيًا بطول، يبلغ التعقيد الزمني التقاربي لعملية إضافة القوائمفي عائلة لغات Lisp، يتم توفير إضافة القوائم بواسطة appendالإجراء.
يمكن التخلص من العديد من الحالات الخاصة لعمليات القوائم المتصلة بإضافة عنصر وهمي في بداية القائمة. يضمن هذا عدم وجود حالات خاصة لبداية القائمة، ويجعل كلاً من `<<>` و`<<>` insertBeginning()غير removeBeginning()ضروريين، أي أن كل عنصر أو عقدة يكون مجاورًا لعقدة أخرى (حتى العقدة الأولى تكون مجاورة للعقدة الوهمية). في هذه الحالة، ستكون أول بيانات مفيدة في القائمة موجودة عند `<<>` .list.firstNode.next
قائمة مرتبطة بشكل دائري
في القائمة المرتبطة دائريًا، ترتبط جميع العقد في دائرة متصلة، دون استخدام قيمة فارغة (null). أما في القوائم ذات المقدمة والمؤخرة (مثل قائمة الانتظار)، فيُخزَّن مرجع إلى آخر عقدة في القائمة. وتُعتبر العقدة التالية للعقدة الأخيرة هي العقدة الأولى. ويمكن إضافة العناصر إلى مؤخرة القائمة وحذفها من مقدمتها في وقت ثابت.
يمكن أن تكون القوائم المرتبطة بشكل دائري إما مرتبطة بشكل فردي أو مزدوج.
يستفيد كلا نوعي القوائم المرتبطة دائريًا من إمكانية اجتياز القائمة بأكملها بدءًا من أي عقدة معينة. وهذا غالبًا ما يُغنينا عن تخزين العقدة الأولى والعقدة الأخيرة ، مع العلم أنه في حال كانت القائمة فارغة، يلزم وجود تمثيل خاص لها، مثل متغير العقدة الأخيرة الذي يشير إلى عقدة معينة في القائمة أو يكون فارغًا (null) إذا كانت فارغة؛ ويُستخدم هنا متغير العقدة الأخيرة هذا . يُسهّل هذا التمثيل بشكل كبير إضافة العقد وإزالتها من قائمة غير فارغة، لكن القوائم الفارغة تُعدّ حالة خاصة.
الخوارزميات
بافتراض أن someNode هي عقدة ما في قائمة مرتبطة أحادية دائرية غير فارغة، فإن هذا الكود يتكرر عبر تلك القائمة بدءًا من someNode :
دالة iterate(someNode) إذا كانت someNode ≠ null node := someNode يفعل قم بتنفيذ شيء ما باستخدام قيمة العقدة. node := node.next بينما node ≠ someNode
لاحظ أن شرط " while node ≠ someNode" يجب أن يكون في نهاية الحلقة. إذا نُقل الشرط إلى بداية الحلقة، فسيفشل الإجراء كلما احتوت القائمة على عقدة واحدة فقط.
تقوم هذه الدالة بإدراج عقدة "newNode" في قائمة مرتبطة دائرية بعد عقدة معينة "node". إذا كانت قيمة "node" فارغة، فإنها تفترض أن القائمة فارغة.
دالة insertAfter( Node node, Node newNode) إذا كان node = null // نفترض أن القائمة فارغة newNode.next := newNode آخر newNode.next := node.next node.next := newNode قم بتحديث متغير lastNode إذا لزم الأمر.
لنفترض أن "L" متغير يشير إلى العقدة الأخيرة في قائمة مرتبطة دائرية (أو قيمة فارغة إذا كانت القائمة فارغة). لإضافة "newNode" إلى نهاية القائمة ، يمكن القيام بما يلي:
insertAfter(L, newNode) L := newNode
لإدراج "newNode" في بداية القائمة، يمكن القيام بما يلي:
أدخل العقدة الجديدة بعد العقدة L إذا كانت L تساوي null، ثم اجعل L تساوي العقدة الجديدة.
تُدرج هذه الدالة القيمة "newVal" قبل العقدة "node" في زمن ثابت O(1). يتم إنشاء عقدة جديدة بين "node" والعقدة التالية، ثم تُدرج قيمة "node" في تلك العقدة الجديدة، وتُدرج "newVal" في "node". بالتالي، يمكن لقائمة مرتبطة دائرية أحادية الارتباط تحتوي على متغير firstNode فقط أن تُدرج القيمة في بداية ونهاية العقدة في زمن ثابت O(1).
دالة insertBefore( Node node, newVal) إذا كان node = null // نفترض أن القائمة فارغة newNode := new Node(data:=newVal, next:=newNode) else newNode := new Node(data:=node.data, next:=node.next) node.data := newVal node.next := newNode قم بتحديث متغير العقدة الأولى إذا لزم الأمر.
تقوم هذه الدالة بإزالة عقدة غير فارغة من قائمة يزيد حجمها عن 1 في زمن ثابت O(1). وتقوم بنسخ البيانات من العقدة التالية إلى العقدة الحالية، ثم تُعيّن مؤشر العقدة التالية لتجاوز العقدة التالية.
دالة remove( Node node) إذا كان node ≠ null وكان حجم القائمة > 1 removedData := node.data node.data := node.next.data node.next = node.next.next إرجاع البيانات المحذوفة
القوائم المتصلة باستخدام مصفوفات من العقد
يمكن للغات التي لا تدعم أي نوع من المراجع إنشاء روابط عن طريق استبدال المؤشرات بفهارس المصفوفات. تعتمد هذه الطريقة على الاحتفاظ بمصفوفة من السجلات ، حيث يحتوي كل سجل على حقول عددية تشير إلى فهرس العقدة التالية (وربما السابقة) في المصفوفة. ليس من الضروري استخدام جميع العقد في المصفوفة. وإذا لم تكن السجلات مدعومة أيضًا، فيمكن غالبًا استخدام المصفوفات المتوازية بدلاً منها.
كمثال على ذلك، ضع في اعتبارك سجل القائمة المرتبطة التالي الذي يستخدم المصفوفات بدلاً من المؤشرات:
سجل الإدخال { عدد صحيح next; // فهرس الإدخال التالي في المصفوفة عدد صحيح prev; // الإدخال السابق (إذا كان مرتبطًا برابط مزدوج) سلسلة نصية name; عدد حقيقي balance; }يمكن إنشاء قائمة مرتبطة عن طريق إنشاء مصفوفة من هذه الهياكل، ومتغير عدد صحيح لتخزين فهرس العنصر الأول.
قائمة الأعداد الصحيحة ، رأس المدخلات، السجلات[1000]
يتم إنشاء الروابط بين العناصر عن طريق وضع فهرس المصفوفة للخلية التالية (أو السابقة) في حقل "التالي" أو "السابق" داخل عنصر معين. على سبيل المثال:
| فِهرِس | التالي | السابق | اسم | توازن |
|---|---|---|---|---|
| 0 | 1 | 4 | جونز، جون | 123.45 |
| 1 | -1 | 0 | سميث، جوزيف | 234.56 |
| 2 (listHead) | 4 | -1 | آدمز، آدم | 0.00 |
| 3 | تجاهل يا إغناطيوس | 999.99 | ||
| 4 | 0 | 2 | أخرى، أنيتا | 876.54 |
| 5 | ||||
| 6 | ||||
| 7 |
في المثال أعلاه، ListHeadسيتم تعيين قيمة المتغير إلى 2، وهو موقع أول عنصر في القائمة. لاحظ أن العناصر من 3 إلى 7 ليست جزءًا من القائمة. هذه الخلايا متاحة لإضافة أي عناصر جديدة إلى القائمة. من خلال إنشاء ListFreeمتغير عددي، يمكن إنشاء قائمة فارغة لتتبع الخلايا المتاحة. إذا كانت جميع العناصر مستخدمة، فسيتعين زيادة حجم المصفوفة أو حذف بعض العناصر قبل إضافة عناصر جديدة إلى القائمة.
سيقوم الكود التالي باستعراض القائمة وعرض الأسماء ورصيد الحساب:
i := listHead while i ≥ 0 // تكرار عبر القائمة print i, Records[i].name, Records[i].balance // طباعة المدخل i := Records[i].next
عند مواجهة خيار، تشمل مزايا هذا النهج ما يلي:
- القائمة المرتبطة قابلة لإعادة التوطين، مما يعني أنه يمكن نقلها في الذاكرة حسب الرغبة، ويمكن أيضًا تسلسلها بسرعة وبشكل مباشر للتخزين على القرص أو نقلها عبر الشبكة.
- خاصةً بالنسبة للقوائم الصغيرة، يمكن أن تشغل فهارس المصفوفات مساحة أقل بكثير من المؤشر الكامل في العديد من البنى المعمارية.
- يمكن تحسين موضعية المرجع عن طريق الاحتفاظ بالعقد معًا في الذاكرة وعن طريق إعادة ترتيبها بشكل دوري، على الرغم من أنه يمكن القيام بذلك أيضًا في مخزن عام.
- يمكن أن تؤدي أدوات تخصيص الذاكرة الديناميكية البسيطة إلى إنتاج كمية مفرطة من مساحة التخزين الإضافية لكل عقدة مخصصة؛ لا يتم تكبد أي تكلفة إضافية للتخصيص لكل عقدة في هذا النهج.
- إن الاستيلاء على عنصر من مصفوفة مخصصة مسبقًا أسرع من استخدام تخصيص الذاكرة الديناميكي لكل عقدة، لأن تخصيص الذاكرة الديناميكي يتطلب عادةً البحث عن كتلة ذاكرة حرة بالحجم المطلوب.
إلا أن لهذا النهج عيبًا رئيسيًا واحدًا: فهو يُنشئ ويدير مساحة ذاكرة خاصة لعُقده. وهذا يؤدي إلى المشكلات التالية:
- يزيد ذلك من تعقيد عملية التنفيذ.
- قد يكون توسيع مصفوفة كبيرة عندما تكون ممتلئة أمرًا صعبًا أو مستحيلاً، في حين أن إيجاد مساحة لعقدة قائمة مرتبطة جديدة في مجموعة ذاكرة عامة كبيرة قد يكون أسهل.
- إضافة عناصر إلى مصفوفة ديناميكية ستستغرق أحيانًا (عندما تكون ممتلئة) وقتًا خطيًا ( O (n)) بدلاً من وقت ثابت (على الرغم من أنه لا يزال ثابتًا مستهلكًا ).
- إن استخدام مجموعة ذاكرة عامة يترك مساحة أكبر من الذاكرة لبيانات أخرى إذا كانت القائمة أصغر من المتوقع أو إذا تم تحرير العديد من العقد.
لهذه الأسباب، يُستخدم هذا الأسلوب بشكل أساسي مع اللغات التي لا تدعم تخصيص الذاكرة الديناميكي. كما يمكن التخفيف من هذه العيوب إذا كان الحد الأقصى لحجم القائمة معروفًا عند إنشاء المصفوفة.
الدعم اللغوي
تتضمن العديد من لغات البرمجة، مثل ليسب وسكيم ، قوائم مرتبطة أحادية مدمجة. في العديد من اللغات الوظيفية ، تُبنى هذه القوائم من عُقد ، تُسمى كل منها خلية cons . تحتوي خلية cons على حقلين: car ، وهو مرجع لبيانات تلك العقدة، و cdr ، وهو مرجع للعقدة التالية. على الرغم من إمكانية استخدام خلايا cons لبناء هياكل بيانات أخرى، إلا أن هذا هو غرضها الأساسي.
في اللغات التي تدعم أنواع البيانات المجردة أو القوالب، تتوفر أنواع البيانات المجردة أو القوالب الخاصة بالقوائم المتصلة لإنشاء هذه القوائم. أما في اللغات الأخرى، فتُبنى القوائم المتصلة عادةً باستخدام المراجع إلى جانب السجلات .
التخزين الداخلي والخارجي
عند إنشاء قائمة مرتبطة، يواجه المرء خيار تخزين بيانات القائمة مباشرةً في عقد القائمة المرتبطة، وهو ما يُسمى بالتخزين الداخلي ، أو تخزين مرجع للبيانات فقط، وهو ما يُسمى بالتخزين الخارجي . يتميز التخزين الداخلي بتحسين كفاءة الوصول إلى البيانات، وتقليل المساحة المطلوبة إجمالاً، وتحسين موضعية المرجع ، وتبسيط إدارة الذاكرة للقائمة (حيث يتم تخصيص بياناتها وتحريرها في نفس وقت تخصيص عقد القائمة وتحريرها).
من ناحية أخرى، يتميز التخزين الخارجي بكونه أكثر عمومية، إذ يمكن استخدام نفس بنية البيانات ورمز الآلة للقائمة المرتبطة بغض النظر عن حجم البيانات. كما يُسهّل وضع البيانات نفسها في قوائم مرتبطة متعددة. مع أن التخزين الداخلي يسمح بوضع البيانات نفسها في قوائم متعددة عبر تضمين مراجع متعددة للعناصر التالية في بنية بيانات العقدة، إلا أنه يتطلب حينها إنشاء إجراءات منفصلة لإضافة أو حذف الخلايا بناءً على كل حقل. من الممكن إنشاء قوائم مرتبطة إضافية من العناصر التي تستخدم التخزين الداخلي باستخدام التخزين الخارجي، بحيث تخزن خلايا هذه القوائم الإضافية مراجع لعقد القائمة المرتبطة التي تحتوي على البيانات.
بشكل عام، إذا كانت هناك حاجة لتضمين مجموعة من هياكل البيانات في قوائم مرتبطة، فإن التخزين الخارجي هو الخيار الأمثل. أما إذا كانت هناك حاجة لتضمين مجموعة من هياكل البيانات في قائمة مرتبطة واحدة فقط، فإن التخزين الداخلي أفضل قليلاً، إلا إذا كانت هناك حزمة عامة للقوائم المرتبطة تستخدم التخزين الخارجي. وبالمثل، إذا كانت هناك حاجة لتضمين مجموعات بيانات مختلفة يمكن تخزينها في نفس هيكل البيانات في قائمة مرتبطة واحدة، فإن التخزين الداخلي سيكون مناسبًا.
هناك نهج آخر يُمكن استخدامه مع بعض اللغات، وهو استخدام هياكل بيانات مختلفة، ولكن جميعها تشترك في الحقول الأولية، بما في ذلك مرجع " التالي" (و "السابق " في حالة القوائم المرتبطة المزدوجة)، في نفس الموقع. بعد تعريف هياكل منفصلة لكل نوع من البيانات، يُمكن تعريف هيكل عام يحتوي على الحد الأدنى من البيانات المشتركة بين جميع الهياكل الأخرى، ويقع في بداية كل هيكل. بعد ذلك، يُمكن إنشاء إجراءات عامة تستخدم هذا الهيكل الأدنى لتنفيذ عمليات القوائم المرتبطة، بينما تتولى إجراءات منفصلة معالجة البيانات الخاصة بكل نوع. يُستخدم هذا النهج غالبًا في إجراءات تحليل الرسائل، حيث يتم استقبال أنواع متعددة من الرسائل، ولكنها جميعًا تبدأ بنفس مجموعة الحقول، والتي تتضمن عادةً حقلًا لنوع الرسالة. تُستخدم الإجراءات العامة لإضافة الرسائل الجديدة إلى قائمة انتظار عند استقبالها، وإزالتها منها لمعالجة الرسالة. ثم يُستخدم حقل نوع الرسالة لاستدعاء الإجراء المناسب لمعالجة نوع الرسالة المحدد.
مثال على التخزين الداخلي والخارجي
لإنشاء قائمة مرتبطة بالعائلات وأفرادها، باستخدام وحدة التخزين الداخلية، قد يبدو الهيكل كما يلي:
سجل العضو { // عضو من عائلة العضو التالي؛ سلسلة الاسم الأول؛ عدد صحيح العمر؛ } سجل العائلة { // العائلة نفسها family next; string lastName; string address; member members // رأس قائمة أعضاء هذه العائلة }لطباعة قائمة كاملة بالعائلات وأفرادها باستخدام وحدة التخزين الداخلية، اكتب:
aFamily := Families // ابدأ من رأس قائمة العائلات while aFamily ≠ null // قم بالتكرار عبر قائمة العائلات معلومات مطبوعة عن العائلة aMember := aFamily.members // الحصول على أول عضو في قائمة أعضاء هذه العائلة while aMember ≠ null // المرور على قائمة الأعضاء اطبع معلومات عن العضو aMember := aMember.next aFamily := aFamily.next
باستخدام وحدة تخزين خارجية، يمكن إنشاء الهياكل التالية:
عقدة السجل { // بنية ارتباط عامة للعقدة التالية؛ مؤشر البيانات // مؤشر عام للبيانات في العقدة } سجل العضو { // بنية لعضو العائلة سلسلة الاسم الأول؛ عدد صحيح العمر } سجل العائلة { // بنية العائلة string lastName; string address; node members // رأس قائمة أعضاء هذه العائلة }لطباعة قائمة كاملة بالعائلات وأفرادها باستخدام وحدة تخزين خارجية، اكتب ما يلي:
famNode := Families // ابدأ من رأس قائمة العائلات while famNode ≠ null // تكرار عبر قائمة العائلات aFamily := (family) famNode.data // استخراج العائلة من العقدة معلومات مطبوعة عن العائلة memNode := aFamily.members // الحصول على قائمة بأفراد العائلة while memNode ≠ null // المرور على قائمة الأفراد aMember := (member)memNode.data // استخراج العضو من العقدة اطبع معلومات عن العضو memNode := memNode.next famNode := famNode.next
لاحظ أنه عند استخدام وحدة تخزين خارجية، يلزم اتخاذ خطوة إضافية لاستخراج السجل من العقدة وتحويله إلى نوع البيانات المناسب. وذلك لأن كلاً من قائمة العائلات وقائمة الأعضاء داخل العائلة تُخزَّن في قائمتين مرتبطتين باستخدام نفس بنية البيانات ( العقدة )، وهذه اللغة لا تدعم الأنواع البارامترية.
طالما أن عدد العائلات التي يمكن أن ينتمي إليها عضو ما معروف وقت الترجمة، فإن التخزين الداخلي يعمل بشكل جيد. أما إذا كان من الضروري تضمين عضو ما في عدد غير محدد من العائلات، مع معرفة العدد المحدد فقط وقت التشغيل، فسيكون التخزين الخارجي ضروريًا.
تسريع البحث
يتطلب العثور على عنصر محدد في قائمة مرتبطة، حتى لو كانت مرتبة، عادةً زمنًا قدره O( n ) ( بحث خطي ). هذه إحدى أبرز عيوب القوائم المرتبطة مقارنةً بهياكل البيانات الأخرى. بالإضافة إلى المتغيرات المذكورة أعلاه، إليك طريقتان بسيطتان لتحسين زمن البحث.
في القوائم غير المرتبة، يُعدّ أسلوب "نقل العنصر إلى المقدمة" أحد الأساليب البسيطة لتقليل متوسط وقت البحث ، حيث ينقل العنصر إلى بداية القائمة بمجرد العثور عليه. يضمن هذا الأسلوب، المفيد لإنشاء ذاكرات تخزين مؤقتة بسيطة، أن تكون العناصر الأكثر استخدامًا هي الأسرع في العثور عليها مرة أخرى.
ثمة طريقة شائعة أخرى تتمثل في " فهرسة " القائمة المتصلة باستخدام بنية بيانات خارجية أكثر كفاءة. على سبيل المثال، يمكن إنشاء شجرة حمراء-سوداء أو جدول تجزئة تكون عناصره عبارة عن مراجع لعقد القائمة المتصلة. ويمكن إنشاء عدة فهارس من هذا النوع على قائمة واحدة. أما عيب هذه الطريقة فهو أنها قد تحتاج إلى التحديث في كل مرة تُضاف فيها عقدة أو تُحذف (أو على الأقل، قبل استخدام هذا الفهرس مرة أخرى).
قوائم الوصول العشوائي
قائمة الوصول العشوائي هي قائمة تدعم الوصول العشوائي السريع لقراءة أو تعديل أي عنصر فيها. [ 9 ] أحد التطبيقات الممكنة هو قائمة الوصول العشوائي الثنائية المائلة باستخدام نظام الأعداد الثنائية المائلة ، والذي يتضمن قائمة من الأشجار ذات خصائص مميزة؛ مما يسمح بعمليات رأس/خاتمة ذات زمن ثابت في أسوأ الحالات، والوصول العشوائي إلى عنصر بواسطة فهرسه بزمن لوغاريتمي في أسوأ الحالات. [ 9 ] يمكن تنفيذ قوائم الوصول العشوائي كهياكل بيانات مستمرة . [ 9 ]
يمكن اعتبار قوائم الوصول العشوائي بمثابة قوائم مرتبطة غير قابلة للتغيير، حيث إنها تدعم بنفس القدر عمليات الرأس والذيل O(1). [ 9 ]
يُعدّ امتداد القائمة الدنيا امتدادًا بسيطًا لقوائم الوصول العشوائي ، حيث يوفر عملية إضافية تُنتج أصغر عنصر في القائمة بأكملها في وقت ثابت (بدون تعقيدات الطفرة). [ 9 ]
هياكل البيانات ذات الصلة
غالبًا ما يتم تنفيذ كل من المكدسات والطوابير باستخدام القوائم المرتبطة، وهي ببساطة تقيد نوع العمليات المدعومة.
قائمة التخطي هي قائمة مرتبطة مُعززة بطبقات من المؤشرات للقفز السريع فوق عدد كبير من العناصر، ثم النزول إلى الطبقة التالية. تستمر هذه العملية وصولاً إلى الطبقة السفلية، وهي القائمة الفعلية.
يمكن اعتبار الشجرة الثنائية نوعًا من القوائم المتصلة، حيث تكون عناصرها قوائم متصلة من نفس النوع. ونتيجة لذلك، قد تتضمن كل عقدة مرجعًا إلى العقدة الأولى في قائمة متصلة واحدة أو اثنتين، تشكلان مع محتوياتهما الأشجار الفرعية أسفل تلك العقدة.
القائمة المتصلة غير الملفوفة هي قائمة متصلة تحتوي كل عقدة فيها على مصفوفة من قيم البيانات. يؤدي هذا إلى تحسين أداء التخزين المؤقت ، حيث يكون عدد أكبر من عناصر القائمة متجاورة في الذاكرة، وتقليل الحمل الزائد للذاكرة، لأنه يلزم تخزين بيانات وصفية أقل لكل عنصر من عناصر القائمة.
قد يستخدم جدول التجزئة قوائم مرتبطة لتخزين سلاسل العناصر التي يتم تجزئتها إلى نفس الموضع في جدول التجزئة.
تتشابه الكومة في بعض خصائص الترتيب مع القائمة المتصلة، ولكنها تُنفذ عادةً باستخدام مصفوفة. وبدلاً من الإشارات من عقدة إلى أخرى، تُحسب مؤشرات البيانات التالية والسابقة باستخدام مؤشر البيانات الحالية.
تقوم القائمة ذاتية التنظيم بإعادة ترتيب عقدها بناءً على بعض الأساليب الاستدلالية التي تقلل من أوقات البحث عن استرجاع البيانات من خلال إبقاء العقد التي يتم الوصول إليها بشكل متكرر في رأس القائمة.
ملحوظات
- ↑ عادةً ما تكون كمية بيانات التحكم المطلوبة للمصفوفة الديناميكية على الشكل التالي:، أينهو ثابت خاص بكل مصفوفة،هو ثابت لكل بُعد، وهو عدد الأبعاد.وعادةً ما تكون في حدود 10 بايت.
مراجع
- ^ الغرب، س. (1963)، “المطالبون بالبرديات اليونانية”، Scriptorium ، 17 (2): 314–15 ، دوى : 10.3406/scrip.1963.3188
- ↑ دي فين، ثيودور لو (1901). . شركة سينشري . ص 142-143 – عبر ويكي مصدر . [ مسح ضوئي ]

- ↑ كنوت، دونالد (1998). فن برمجة الحاسوب . المجلد 3: الفرز والبحث ( الطبعة الثانية). أديسون-ويسلي. ص 547. ISBN 978-0-201-89685-5.
- 1 2 "مطلع على نظام التشغيل NT: أساسيات وضع النواة: قوائم ويندوز المرتبطة" . مؤرشف من الأصل بتاريخ 23-09-2015 . تم الاطلاع عليه بتاريخ 31-07-2015 .
- ↑ باتلر، جيمي؛ هوغلوند، غريغ. "فايس - اقبضوا على بائعات الهوى! (بالإضافة إلى تقنيات جديدة لاختراق البرامج الضارة)" (ملف PDF) . مؤرشف من النسخة الأصلية (ملف PDF) بتاريخ 1 أكتوبر 2016. تم الاطلاع عليه بتاريخ 31 أغسطس 2021 .
- ↑ برودنيك، أندريه؛ كارلسون، سفانتي؛ سيدجويك، روبرت ؛ مونرو، جي آي؛ ديمين، إي دي (1999)، المصفوفات القابلة لتغيير الحجم في الوقت والمساحة الأمثلين (تقرير فني CS-99-09) (PDF) ، قسم علوم الحاسوب، جامعة واترلو
- 1 2 3 كريس أوكازاكي (1995). "قوائم الوصول العشوائي الوظيفية البحتة". وقائع المؤتمر الدولي السابع حول لغات البرمجة الوظيفية وهندسة الحاسوب : 86-95 . doi : 10.1145/224164.224187 .
- ↑ فورد، ويليام؛ توب، ويليام (2002). هياكل البيانات باستخدام لغة C++ ومكتبة STL ( الطبعة الثانية). برنتيس هول. الصفحات 466-467 . ISBN 0-13-085850-1.
- 1 2 3 4 5 أوكاساكي، كريس (1995). "قوائم الوصول العشوائي الوظيفية البحتة" . وقائع المؤتمر الدولي السابع حول لغات البرمجة الوظيفية وهندسة الحاسوب - FPCA '95 . مطبعة ACM. الصفحات 86-95 . doi : 10.1145/224164.224187 . ISBN 0-89791-719-7تم الاطلاع عليه بتاريخ 7 مايو 2015 .
{{cite book}}تم|work=تجاهله ( مساعدة )
للمزيد من القراءة
- خوان، أنخيل (2006). "الفصل 20 - هياكل البيانات؛ المعرف 6 - البرمجة بلغة جافا (جزء من كتاب "جافا الكبيرة"، بقلم كاي إس. هورستمان)" (ملف PDF) . ص 3. مؤرشف من النسخة الأصلية (PDF) بتاريخ 6 يناير 2012. تاريخ الاسترجاع: 10 يوليو 2011 .
- بلاك، بول إي. (16 أغسطس 2004). بيترس، فريدا؛ بلاك، بول إي. (محرران). "القائمة المتصلة" . قاموس الخوارزميات وهياكل البيانات . المعهد الوطني للمعايير والتكنولوجيا . تاريخ الاسترجاع: 14 ديسمبر 2004 .
- أنطوناكوس، جيمس ل.؛ مانسفيلد، كينيث سي. الابن (1999). هياكل البيانات العملية باستخدام لغة C/C++ . برنتيس هول. الصفحات 165-190 . ISBN 0-13-280843-9.
- كولينز، ويليام ج. (2005) [2002]. هياكل البيانات وإطار عمل مجموعات جافا . نيويورك: ماكجرو هيل. الصفحات 239-303 . ISBN 0-07-282379-8.
- كورمين، توماس هـ . ليسرسون، تشارلز إي . ريفست، رونالد ل . ستاين، كليفورد (2003). مقدمة في الخوارزميات . مطبعة معهد ماساتشوستس للتكنولوجيا. ص 205 – 213، 501 – 505. ISBN 0-262-03293-7.
- كورمن، توماس هـ .؛ ليسرسون، تشارلز إي .؛ ريفست ، رونالد ل .؛ شتاين، كليفورد (2001). "10.2: القوائم المتصلة". مقدمة في الخوارزميات ( الطبعة الثانية). مطبعة معهد ماساتشوستس للتكنولوجيا. الصفحات 204-209 . ISBN 0-262-03293-7.
- غرين، بيرت ف. الابن (1961). "لغات الحاسوب لمعالجة الرموز". معاملات معهد مهندسي الراديو في العوامل البشرية في الإلكترونيات . 2 (2): 3-8 . رمز Bibcode : 1961IRTHF...2....3G . doi : 10.1109/THFE2.1961.4503292 .
- مكارثي، جون (1960). "الدوال التكرارية للتعبيرات الرمزية وحسابها بواسطة الآلة، الجزء الأول" . مجلة اتصالات رابطة مكائن الحوسبة . 3 (4): 184. doi : 10.1145/367177.367199 . S2CID 1489409 .
- كنوت، دونالد (1997). "2.2.3-2.2.5". الخوارزميات الأساسية ( الطبعة الثالثة). أديسون-ويسلي. الصفحات 254-298 . ISBN 0-201-89683-4.
- نيويل، ألين ؛ شو، إف سي (1957). "برمجة آلة نظرية المنطق". وقائع المؤتمر المشترك الغربي للحاسوب : 230-240 .
- بارلانتي، نيك (2001). "أساسيات القوائم المتصلة" (ملف PDF) . جامعة ستانفورد . تاريخ الاسترجاع: 21-09-2009 .
- سيدجويك، روبرت (1998). الخوارزميات في لغة سي . أديسون ويسلي. الصفحات 90-109 . ISBN 0-201-31452-5.
- شافر، كليفورد أ. (1998). مقدمة عملية في هياكل البيانات وتحليل الخوارزميات . نيوجيرسي: برنتيس هول. ص 77-102 . ISBN 0-13-660911-2.
- شانموغاسوندارام، كوليش (4 أبريل 2005). "شرح القائمة المرتبطة في نواة لينكس" . مؤرشف من الأصل بتاريخ 25 سبتمبر 2009. تم الاطلاع عليه بتاريخ 21 سبتمبر 2009 .
- West، S. (1963)، “Reclamantes in اليونانية البردي”، Scriptorium ، 17 (2): 314–15 ، دوى : 10.3406 / scrip.1963.3188
- ويلكس، موريس فنسنت (1964). "تجربة مع مُترجم ذاتي التجميع للغة بسيطة لمعالجة القوائم". المراجعة السنوية في البرمجة الآلية . 4 (1). دار بيرغامون للنشر: 1. doi : 10.1016/0066-4138(64)90013-8 .
- ويلكس، موريس فنسنت (1964). "القوائم وأهميتها". وقائع المؤتمر الوطني لجمعية آلات الحوسبة، فيلادلفيا 1964 (ص 64). جمعية آلات الحوسبة: F1-1.
روابط خارجية
- وصف من قاموس الخوارزميات وهياكل البيانات
- مقدمة في القوائم المتصلة ، مكتبة علوم الحاسوب بجامعة ستانفورد
- مشاكل القوائم المتصلة ، مكتبة علوم الحاسوب بجامعة ستانفورد
- هياكل البيانات المفتوحة - الفصل 3 - القوائم المتصلة ، بات مورين
- براءة اختراع لفكرة وجود عقد موجودة في عدة قوائم مرتبطة في وقت واحد (لاحظ أن هذه التقنية كانت تستخدم على نطاق واسع لعقود عديدة قبل منح براءة الاختراع).
- تنفيذ قائمة مرتبطة أحادية في لغة C
- تنفيذ قائمة مرتبطة أحادية في لغة C++
- تنفيذ قائمة مرتبطة ثنائياً في لغة C
- تنفيذ قائمة مرتبطة ثنائياً في لغة C++
- القوائم المرتبطة
