فئة Node وإنشاء القوائم
عرّف dataclass باسم Node، وأنشئ القوائم بربط العقد يدويًا، واكتب مساعدين للإدراج والحذف والطباعة لتصوير تغيّرات المؤشرات
فئة Node وإنشاء القوائم درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ما القائمة المرتبطة؟
القائمة المرتبطة هي سلسلة من العقد، تخزّن كل عقدة قيمة ومؤشراً إلى العقدة التالية. بخلاف المصفوفات، تكون العقد موزعة في الذاكرة، لذلك لا يوجد وصول O(1) يعتمد على الفهرس. في المقابل، تحصل على إدراج وحذف بتعقيد O(1) في أي موضع معروف، من دون إزاحة العناصر.
في Python نمثّل كل عقدة بفئة صغيرة تحتوي على val وnext. يؤدي ربط العقد معاً إلى تكوين القائمة؛ وتكون قيمة next في العقدة الأخيرة هي None للإشارة إلى النهاية.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Build: 1 -> 2 -> 3 -> None
head = ListNode(1)
head.next = ListNode(2)
head.next.next = ListNode(3)
# Traverse and print
curr = head
while curr:
print(curr.val, end=' -> ')
curr = curr.next
print('None')إنشاء القوائم من المصفوفات
في المقابلات، غالباً ما تُعطى قائمة ويُطلب منك إنشاء ما يعادلها كقائمة مرتبطة، أو العكس. يجدر بك حفظ الدالتين المساعدتين build وto_list: إذ تربط build العقد انطلاقاً من مصفوفة، بينما تمرّ to_list عبر القائمة لجمع القيم بغرض التحقق بسهولة.
يستغرق إنشاء قائمة مرتبطة من n عنصراً زمناً قدره O(n) ومساحة قدرها O(n). ويسهّل استخدام عقدة رأس وهمية التعامل مع الحالات الحدّية التي قد تتغير فيها العقدة الأولى.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def build(arr):
dummy = ListNode(0)
curr = dummy
for val in arr:
curr.next = ListNode(val)
curr = curr.next
return dummy.next
def to_list(head):
result = []
while head:
result.append(head.val)
head = head.next
return result
head = build([1, 2, 3, 4, 5])
print(to_list(head)) # [1, 2, 3, 4, 5]الإدراج في الرأس والذيل
يتم إدراج عقدة جديدة في الرأس بتعقيد O(1): أنشئ العقدة، ووجّه next فيها إلى الرأس القديم، ثم أعد العقدة الجديدة بوصفها الرأس. أما الإدراج في الذيل فيتطلب المرور حتى العقدة الأخيرة (بتعقيد O(n))، ثم ربط العقدة الجديدة بها.
تلغي عقدة الرأس الوهمية الحالة الخاصة للقائمة الفارغة في عمليتي الإدراج، لأن dummy.next يكون دائماً هو الرأس الحقيقي.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def insert_head(head, val):
return ListNode(val, head) # O(1)
def insert_tail(head, val):
new_node = ListNode(val)
if not head:
return new_node
curr = head
while curr.next:
curr = curr.next
curr.next = new_node
return head
head = None
for v in [1, 2, 3]:
head = insert_tail(head, v)
head = insert_head(head, 0)
curr = head
while curr:
print(curr.val, end=' -> ')
curr = curr.next
print('None') # 0 -> 1 -> 2 -> 3 -> Noneحذف عقدة حسب القيمة
لحذف العقدة الأولى التي تحمل قيمة معينة، احتفظ بمؤشر prev يسبق curr بخطوة واحدة. عندما تكون curr.val == target، اضبط prev.next = curr.next لتجاوز العقدة. وتكون عقدة الرأس الوهمية مفيدة بشكل خاص هنا، لأنها تلغي الحالة الخاصة لحذف عقدة الرأس الفعلية، إذ يمكن أن يبدأ prev دائماً من العقدة الوهمية.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def delete_val(head, target):
dummy = ListNode(0)
dummy.next = head
prev, curr = dummy, head
while curr:
if curr.val == target:
prev.next = curr.next
break
prev, curr = curr, curr.next
return dummy.next
def to_list(h):
r = []
while h:
r.append(h.val)
h = h.next
return r
head = None
for v in [1, 2, 3, 2, 4]:
dummy2 = ListNode(v)
dummy2.next = head
head = dummy2 # build in reverse for speed
head = delete_val(head, 2)
print(to_list(head))تصوّر تغييرات المؤشرات
من الأخطاء الشائعة فقدان تتبّع عقدة عند تحديث المؤشرات. احفظ دائماً next قبل الكتابة فوقه: saved = curr.next، ثم أعد إسناده. ارسم القائمة على شكل مربعات متصلة بأسهم، وحاكِ كل تحديث للمؤشرات على الورق قبل كتابة الشيفرة. يمنع هذا الأسلوب البصري أخطاء المؤشرات الفارغة غير المقصودة أثناء المقابلات.
تذكّر: في Python، لا يؤثر تغيير إسناد curr.next في curr نفسه، لكن فقدان المرجع إلى curr.next قبل حفظه يعني أنك لن تعود قادراً على المرور إلى الأمام.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Demonstrate safe pointer update
def swap_first_two(head):
if not head or not head.next:
return head
first = head
second = head.next
# Save third before losing the reference
third = second.next
# Rewire
second.next = first
first.next = third
return second
from functools import reduce
nodes = [ListNode(i) for i in range(1, 5)]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i+1]
head = swap_first_two(nodes[0])
curr = head
while curr:
print(curr.val, end=' ')
curr = curr.next
# 2 1 3 4القوائم المرتبطة أحادية الربط مقابل ثنائية الربط
تخزّن القائمة المرتبطة أحادية الربط مؤشراً next فقط؛ ويكون المرور فيها باتجاه واحد. أما القائمة المرتبطة ثنائية الربط فتخزّن كلاً من prev وnext، مما يتيح المرور إلى الخلف بتعقيد O(1) والحذف بتعقيد O(1) عند توفر مرجع مباشر إلى العقدة، من دون الحاجة إلى حلقة تتبّع prev.
تُنفّذ collections.deque في Python باستخدام قائمة مرتبطة ثنائية الربط، ولذلك فهي تدعم appendleft وpopleft بتعقيد O(1). في المقابلات ستنفّذ قوائم مرتبطة أحادية الربط؛ أما القوائم المرتبطة ثنائية الربط فتظهر في تصميم ذاكرة التخزين المؤقت LRU.
class DLNode:
def __init__(self, val=0):
self.val = val
self.prev = None
self.next = None
# Build doubly linked: 1 <-> 2 <-> 3
a, b, c = DLNode(1), DLNode(2), DLNode(3)
a.next = b; b.prev = a
b.next = c; c.prev = b
# Traverse forward
curr = a
while curr:
print(curr.val, end=' <-> ')
curr = curr.next
print('None')
# Traverse backward from c
curr = c
while curr:
print(curr.val, end=' <-> ')
curr = curr.prev
print('None')دوال مساعدة للطول والذيل والطباعة
ثلاث دوال مساعدة ينبغي أن تكون جاهزة لديك في أي مقابلة حول القوائم المرتبطة: تحسب length(head) عدد العقد بتعقيد O(n)، وتعيد tail(head) العقدة الأخيرة بتعقيد O(n)، بينما تنسّق print_list(head) القائمة لأغراض تصحيح الأخطاء. يتيح لك تجهيز هذه الدوال التركيز على الخوارزمية الأساسية بدلاً من إعادة تنفيذ المنطق المساعد.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def length(head):
count = 0
while head:
count += 1
head = head.next
return count
def tail(head):
while head and head.next:
head = head.next
return head
def print_list(head):
parts = []
while head:
parts.append(str(head.val))
head = head.next
print(' -> '.join(parts) + ' -> None')
# Build and test
nodes = [ListNode(i) for i in [10, 20, 30, 40]]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i+1]
head = nodes[0]
print('Length:', length(head))
print('Tail:', tail(head).val)
print_list(head)إعداد المؤشرين في القوائم المرتبطة
تُعد تقنية المؤشرين مهمة للقوائم المرتبطة بقدر أهميتها للمصفوفات، لكن المؤشرين هنا هما عقدتان في قائمة مرتبطة وليسا فهرسين. تشمل الإعدادات الشائعة مؤشراً بطيئاً وآخر سريعاً (يتحرك السريع بسرعة ضعف الآخر) للعثور على نقاط المنتصف واكتشاف الحلقات، وزوجاً من السابق والحالي للحذف والعكس.
هيّئ المؤشرين دائماً بشكل صريح، وتعامل بحذر مع فحص الوصول إلى القيمة الفارغة — إذ يمنع fast and fast.next أخطاء المؤشرات الفارغة عندما يكون fast قريباً من النهاية.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Find middle node using slow-fast pointers
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # for even length, returns second of two middle nodes
nodes = [ListNode(i) for i in range(1, 6)]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i+1]
print(find_middle(nodes[0]).val) # 3 (middle of 1->2->3->4->5)نمط الرأس الوهمي
يُعد نمط الرأس الوهمي (العقدة الحارسة) من أكثر الحيل فائدة في مسائل القوائم المرتبطة. بإضافة عقدة وهمية في البداية تحمل القيمة 0، لن تحتاج أبداً إلى حالة خاصة للقائمة الفارغة أو لتغيير الرأس الحقيقي. تكون النتيجة دائماً dummy.next. يظهر هذا النمط في دمج القوائم المرتبة، وحذف العنصر رقم n من النهاية، وتقسيم القائمة، وغير ذلك الكثير.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Remove all nodes with val == target (may include head)
def remove_all(head, target):
dummy = ListNode(0)
dummy.next = head
curr = dummy
while curr.next:
if curr.next.val == target:
curr.next = curr.next.next # skip the node
else:
curr = curr.next
return dummy.next
def to_list(h):
r = []
while h:
r.append(h.val)
h = h.next
return r
nodes = [ListNode(v) for v in [1, 2, 6, 3, 4, 5, 6]]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i+1]
head = remove_all(nodes[0], 6)
print(to_list(head)) # [1, 2, 3, 4, 5]التعقيد الزمني والمكاني
لدى معظم عمليات القوائم المرتبطة التعقيدات التالية. الوصول حسب الفهرس: O(n) — إذ يجب المرور بدءاً من الرأس. الإدراج أو الحذف عند عقدة معروفة: O(1) — يكفي إعادة توصيل المؤشرات. الإدراج أو الحذف في الموضع k: O(k) — يجب المرور أولاً. البحث: O(n) — إذ قد يتطلب المرور عبر القائمة بأكملها في أسوأ الحالات. تكون المساحة O(1) لجميع العمليات داخل المكان، باستثناء هياكل البيانات الإضافية.
قارن ذلك بالمصفوفات: توفر المصفوفات وصولاً بتعقيد O(1)، لكنها تتطلب O(n) للإدراج أو الحذف بسبب إزاحة العناصر. تكون القوائم المرتبطة أفضل عندما يكثر الإدراج والحذف في مواضع اعتباطية.
نصائح مقابلات القوائم المرتبطة
قبل كتابة أي شيفرة لقائمة مرتبطة، ارسم القائمة بصرياً باستخدام مربعات وأسهم. اذكر حالات الحواف بصوت عالٍ: القائمة الفارغة، العقدة الوحيدة، والطول الزوجي مقابل الفردي. استخدم رأساً وهمياً لتبسيط الشروط الحدّية. تحقّق دائماً مبكراً من if not head. بعد كتابة الشيفرة، تتبّع الحل على قائمة من ثلاث عقد لاكتشاف أخطاء المؤشرات قبل أن يكتشفها المحاور.
تأتي معظم أخطاء القوائم المرتبطة من أحد ثلاثة مصادر: نسيان حفظ next قبل الكتابة فوقه، أو وجود خطأ بمقدار واحد في شرط الإنهاء، أو عدم التعامل مع حالة تغيّر الرأس — وتلغي العقدة الوهمية السبب الثالث تماماً.
اختبار سريع
اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
مراجعة الدرس
تعلّمت في هذا الدرس أن: القائمة المرتبطة تُبنى من كائنات Node التي تحتوي على الحقلين val وnext، وأن نمط الرأس الوهمي يلغي حالات الحواف التي يتغير فيها الرأس، وأن إعداد المؤشرين البطيء والسريع هو أساس العثور على نقطة المنتصف واكتشاف الحلقات. ننتقل بعد ذلك إلى عكس قائمة مرتبطة، وهو أحد أكثر مسائل المؤشرات شيوعاً في المقابلات.
الأسئلة الشائعة
هل درس «فئة Node وإنشاء القوائم» مجاني؟
نعم — نص درس «فئة Node وإنشاء القوائم» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «فئة Node وإنشاء القوائم»؟
عرّف dataclass باسم Node، وأنشئ القوائم بربط العقد يدويًا، واكتب مساعدين للإدراج والحذف والطباعة لتصوير تغيّرات المؤشرات تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.
كم من الوقت يستغرق درس «فئة Node وإنشاء القوائم»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- فئة Node وإنشاء القوائم
- عكس قائمة مرتبطة
- اكتشاف الدورات باستخدام خوارزمية Floyd
- الدمج والتقسيم والعثور على العنصر N من النهاية