LinkedList وواجهة List
LinkedList وواجهة List
تعرفت سابقًا على أن ArrayList تخزّن العناصر في مصفوفة متجاورة. تسلك LinkedList المسار المعاكس تمامًا: يعيش كل عنصر في عقدة خاصة به تحمل مرجعًا إلى العقدة التالية والسابقة. فهم هذا الاختلاف الداخلي هو جوهر قرار الاختيار بين الاثنتين.
واجهة List — العقد المشترك
تُطبّق كلٌّ من ArrayList وLinkedList الواجهة java.util.List<E>. تُحدّد هذه الواجهة العمليات التي يجب أن تدعمها كل مجموعة مرتّبة قابلة للفهرسة:
add(E e)/add(int index, E e)— إلحاق أو إدراجget(int index)— الاسترجاع بالموضعset(int index, E e)— الاستبدال بالموضعremove(int index)/remove(Object o)— الحذف بالفهرس أو القيمةsize()،contains(Object o)،indexOf(Object o)subList(int from, int to)،sort(Comparator<? super E> c)
البرمجة بالاعتماد على واجهة List بدلًا من الفئة المحددة هي من أساسيات مبادئ Java. تتيح لك استبدال التنفيذ لاحقًا دون لمس بقية الكود:
List وليس ArrayList. توقيعات الدوال التي تقبل List<String> تعمل مع أي تنفيذ. أما التوقيعات التي تشترط ArrayList<String> فتُقيّدك بتنفيذ محدّد — وهو خطأ في الغالب.
كيف تعمل LinkedList داخليًا
LinkedList هي قائمة مرتبطة مزدوجة. كل عقدة تبدو تقريبًا هكذا:
يحتفظ كائن القائمة بمراجع إلى العقدة الأولى والأخيرة فقط. للوصول إلى العقدة رقم k، يجب على Java المشي عبر السلسلة من أحد الطرفين — بتعقيد O(n). أما الإدراج أو الحذف عند عقدة معروفة فهو O(1): مجرد إعادة ربط ثلاثة مؤشرات.
تُطبّق LinkedList أيضًا Deque<E>، ولهذا تكشف عن دوال مثل addFirst وaddLast وpeekFirst وpollLast وسواها — دوال لا تملكها ArrayList.
ArrayList مقابل LinkedList — جدول المفاضلة
هذه هي تعقيدات Big-O التي تقود قرار الاختيار:
- الوصول العشوائي (
get(i)): ArrayList بتعقيد O(1) مقابل LinkedList بتعقيد O(n). ArrayList تفوز بوضوح — تقفز مباشرة إلى فهرس المصفوفة الداخلية. - الإلحاق في النهاية (
add(e)): كلاهما O(1) مُبسَّط. تُجري ArrayList تغيير الحجم أحيانًا؛ تُخصّص LinkedList عقدة جديدة دائمًا. - الإدراج/الحذف في المنتصف (
add(i, e)/remove(i)): ArrayList بتعقيد O(n) لأن العناصر تتحرّك؛ LinkedList بتعقيد O(n) للوصول إلى الفهرس ثم O(1) لإعادة الربط. لا فائز واضح إلا إذا كان لديك مكرّر عند نقطة الإدراج. - الإدراج/الحذف في المقدمة: ArrayList بتعقيد O(n) (كل عنصر يتحرّك يمينًا)؛ LinkedList بتعقيد O(1). هذه الحالة الوحيدة التي تتفوّق فيها LinkedList على ArrayList بوضوح.
- الذاكرة: ArrayList أكثر إحكامًا (مصفوفة واحدة). كل عقدة في LinkedList تُخصّص مرجعَين إضافيَّين، مما يرفع التكلفة لكل عنصر 3-4 أضعاف.
ArrayList خيارًا افتراضيًا؛ والجأ إلى LinkedList فقط عند الحاجة إلى إدراج/حذف O(1) في المقدمة أو عند الحاجة إلى واجهة Deque.
التكرار الفعّال على LinkedList
لا تستخدم أبدًا حلقات تعتمد على الفهرس مع LinkedList. كل استدعاء لـ get(i) يسير عبر السلسلة من الرأس، مما يحوّل تكرارًا بتعقيد O(n) إلى O(n²).
get(index) داخل حلقة على LinkedList أبدًا. هذا أحد أكثر أخطاء الأداء شيوعًا لدى مطوّري Java. استخدم دائمًا حلقة for المحسّنة أو Iterator / ListIterator صريحًا.
استخدام LinkedList كـ Deque
عندما تحتاج إلى طابور (FIFO) أو مكدّس (LIFO) مبني على هيكل مرتبط، فإن LinkedList خيار جاهز:
الخلاصة
تُحدّد واجهة List العقد؛ وتُمثّل ArrayList وLinkedList تنفيذَين ذوَي بنية داخلية مختلفة جدًا. افضّل ArrayList للقوائم متعددة الأغراض — أداؤها مع الذاكرة المؤقتة أفضل وهي أسرع للوصول العشوائي. استخدم LinkedList عند الحاجة إلى إدراج/حذف ثابت O(1) في المقدمة، أو عند الرغبة في واجهة Deque. أعلن المتغيّرات دائمًا كـ List (أو Deque) لتبقى قادرًا على تبديل التنفيذ.