الدمج والتقسيم والعثور على العنصر N من النهاية
ادمج قائمتين مرتبطتين مرتبتين في O(n)، واقسم قائمة عند نقطة المنتصف باستخدام المؤشرين البطيء والسريع، واعثر على العقدة رقم n من الذيل
الدمج والتقسيم والعثور على العنصر N من النهاية درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ثلاثة أنماط أساسية في القوائم المترابطة
يغطي هذا الدرس ثلاث عمليات أساسية في القوائم المترابطة، تظهر باستمرار بوصفها لبنات أساسية في المسائل الأصعب: دمج قائمتين مرتبتين (ويُستخدم في الترتيب بالدمج والدمج متعدد القوائم)، وتقسيم القائمة عند نقطة المنتصف (ويُستخدم في الترتيب بالدمج واكتشاف التناظر)، والعثور على العقدة رقم n من النهاية (ويُستخدم في remove-nth-from-end).
تعتمد العمليات الثلاث على تقنيات سبق أن رأيتموها: العقدة ذات الرأس الوهمي، ومؤشرا slow وfast، والتتبع الدقيق للحدود.
دمج قائمتين مرتبتين
تطلب مسألة LeetCode 21 «دمج قائمتين مترابطتين» دمج قائمتين مترابطتين مرتبتين وإعادة قائمة مرتبة واحدة. استخدموا رأسًا وهميًا ومؤشر ذيل curr. في كل خطوة، قارنوا رأسي القائمتين ووصلوا العقدة الأصغر بـ curr. وعند انتهاء إحدى القائمتين، صِلوا ما تبقى من الأخرى. الزمن: O(n+m)، والمساحة: O(1) (إعادة توصيل الروابط في مكانها).
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def mergeTwoLists(l1, l2):
dummy = ListNode(0)
curr = dummy
while l1 and l2:
if l1.val <= l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 or l2 # attach remaining nodes
return dummy.next
def build(arr):
d = ListNode(); c = d
for v in arr:
c.next = ListNode(v); c = c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val); h=h.next
return r
print(to_list(mergeTwoLists(build([1,2,4]), build([1,3,4]))))تتبّع خطوة الدمج خطوة بخطوة
تتبّعوا mergeTwoLists([1,2,4], [1,3,4]): قارنوا 1 و1 — اختاروا l1(1)، وحرّكوا l1 إلى 2. قارنوا 2 و1 — اختاروا l2(1)، وحرّكوا l2 إلى 3. قارنوا 2 و3 — اختاروا l1(2)، وحرّكوا l1 إلى 4. قارنوا 4 و3 — اختاروا l2(3)، وحرّكوا l2 إلى 4. قارنوا 4 و4 — اختاروا l1(4)، وحرّكوا l1 إلى None. صِلوا ما تبقى من l2(4). النتيجة: [1,1,2,3,4,4].
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def mergeTwoLists(l1, l2):
dummy = ListNode(0)
curr = dummy
step = 0
while l1 and l2:
step += 1
if l1.val <= l2.val:
print(f'Step {step}: pick l1({l1.val})')
curr.next = l1; l1 = l1.next
else:
print(f'Step {step}: pick l2({l2.val})')
curr.next = l2; l2 = l2.next
curr = curr.next
curr.next = l1 or l2
return dummy.next
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
mergeTwoLists(build([1,2,4]),build([1,3,4]))العثور على نقطة المنتصف باستخدام المؤشرين البطيء والسريع
لتقسيم قائمة عند نقطة المنتصف، استخدموا نمط المؤشرين البطيء والسريع. يتقدم slow خطوة واحدة، بينما يتقدم fast خطوتين. عندما يصل fast إلى None (أو إلى العقدة الأخيرة)، يكون slow عند نقطة المنتصف. وفي القائمة ذات الطول الزوجي، يعطي هذا النمط العقدة الأولى من العقدتين الوسطيتين، وهو الأسلوب المتعارف عليه لتقسيم القوائم في الترتيب بالدمج.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def split_at_mid(head):
'''Returns (first_half_head, second_half_head).'''
slow, fast = head, head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
mid = slow.next # second half starts here
slow.next = None # sever the list
return head, mid
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val); h=h.next
return r
head=build([1,2,3,4,5])
first, second = split_at_mid(head)
print(to_list(first), to_list(second)) # [1,2,3] [4,5]الفرز بالدمج على قائمة مرتبطة
LeetCode 148 «فرز القائمة»: فرز قائمة مرتبطة في زمن O(n log n) وبمساحة O(log n). النهج هو تقسيم القائمة عند نقطة المنتصف، ثم فرز كل نصف递كاريًا ودمجهما. يُعد فرز القوائم المرتبطة بالدمج طبيعيًا؛ لأن التقسيم عند نقطة المنتصف يستغرق O(n) (وليس O(1) كما هو الحال في المصفوفات)، لكن التعقيد الإجمالي يظل O(n log n)، مع استخدام مساحة O(log n) فقط لمكدس الاستدعاءات.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def sortList(head):
if not head or not head.next:
return head
# Split
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
mid = slow.next
slow.next = None
# Recurse
left = sortList(head)
right = sortList(mid)
# Merge
dummy = ListNode(0)
curr = dummy
while left and right:
if left.val <= right.val:
curr.next = left; left = left.next
else:
curr.next = right; right = right.next
curr = curr.next
curr.next = left or right
return dummy.next
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val);h=h.next
return r
print(to_list(sortList(build([4,2,1,3])))) # [1,2,3,4]العثور على العقدة رقم n من النهاية
LeetCode 19 «إزالة العقدة رقم n من نهاية القائمة»: العثور على العقدة رقم n من الذيل في مرور واحد. استخدم مؤشرين يفصل بينهما بالضبط n عقد. قدّم fast بمقدار n خطوة على slow. ثم حرّك المؤشرين معًا حتى يصل fast إلى العقدة الأخيرة. عندها يكون slow عند العقدة رقم (n+1) من النهاية، أي العقدة السابقة للعقدة المراد حذفها.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def removeNthFromEnd(head, n):
dummy = ListNode(0, head)
fast = dummy
for _ in range(n + 1): # advance fast n+1 steps
fast = fast.next
slow = dummy
while fast: # advance both until fast is None
slow = slow.next
fast = fast.next
slow.next = slow.next.next # remove nth node
return dummy.next
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val);h=h.next
return r
print(to_list(removeNthFromEnd(build([1,2,3,4,5]), 2))) # [1,2,3,5]لماذا نستخدم n+1 خطوة في Remove Nth
تكمن الدقة الأساسية في تقديم fast بمقدار n+1 خطوة (وليس n) بدءًا من الرأس الوهمي. بعد n+1 خطوة، يصبح fast متقدمًا على slow بمقدار n+1 موضعًا (ويبدأ كلاهما من الرأس الوهمي). عندما يصل fast إلى None (أي موضع واحد بعد الذيل)، يكون slow متراجعًا عن None بمقدار n+1 موضعًا؛ وهذا يعني أن slow في الموضع (length - n - 1) بدءًا من الصفر، أو عند العقدة السابقة للعقدة المستهدفة. يتيح ذلك حذف العقدة رقم n من النهاية بسلاسة باستخدام slow.next = slow.next.next.
# Visual: list = [1,2,3,4,5], n=2
# dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None
# After n+1=3 forward steps from dummy, fast=3
# dummy(slow) 1 2 3(fast) 4 5 None
# Advance both until fast=None:
# Step 1: slow=1, fast=4
# Step 2: slow=2, fast=5
# Step 3: slow=3, fast=None
# slow is at 3, slow.next=4 (the 2nd from end) -> delete
print('slow.next (to delete): 4')
print('Result: [1, 2, 3, 5]')تقاطع قائمتين مرتبطتين
LeetCode 160 «تقاطع قائمتين مرتبطتين»: العثور على العقدة التي تتقاطع عندها قائمتان للمرة الأولى. الحيلة التي تستخدم مساحة O(1) هي تحريك مؤشرين، واحد لكل قائمة. عندما يصل أحد المؤشرين إلى None، أعد توجيهه إلى رأس القائمة الأخرى. بعد len(A) + len(B) خطوة كحد أقصى، يكون المؤشران قد قطعا المسافة الإجمالية نفسها، ولا بد أن يكونا عند عقدة التقاطع (أو كلاهما عند None إذا لم يوجد تقاطع).
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def getIntersectionNode(headA, headB):
a, b = headA, headB
while a is not b:
a = a.next if a else headB
b = b.next if b else headA
return a # None if no intersection
# Build: A: 4->1->\ B: 5->6->1->\ both -> 8->4->5
shared = [ListNode(v) for v in [8, 4, 5]]
shared[0].next = shared[1]; shared[1].next = shared[2]
A = ListNode(4); A.next = ListNode(1); A.next.next = shared[0]
B = ListNode(5); B.next = ListNode(6); B.next.next = ListNode(1); B.next.next.next = shared[0]
print(getIntersectionNode(A, B).val) # 8دمج k قوائم مرتبة (التقسيم والغزو)
LeetCode 23 «دمج k قوائم مرتبة»: عند إعطائك k قوائم مرتبة، ادمجها في قائمة واحدة. النهج الأمثل هو دمج أزواج القوائم بشكل متكرر باستخدام التقسيم والغزو، مع خفض عدد القوائم إلى النصف في كل جولة. بالنسبة إلى k قوائم يبلغ متوسط طول كل منها n، يستغرق ذلك زمنًا قدره O(n k log k)، مقارنةً بـ O(n k²) عند الدمج التسلسلي. كما أن نهج الكومة الدنيا يستغرق O(n k log k).
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def mergeKLists(lists):
def merge_two(l1, l2):
dummy = ListNode(0); curr = dummy
while l1 and l2:
if l1.val <= l2.val:
curr.next = l1; l1 = l1.next
else:
curr.next = l2; l2 = l2.next
curr = curr.next
curr.next = l1 or l2
return dummy.next
if not lists: return None
while len(lists) > 1:
merged = []
for i in range(0, len(lists), 2):
l1 = lists[i]
l2 = lists[i+1] if i+1 < len(lists) else None
merged.append(merge_two(l1, l2))
lists = merged
return lists[0]
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val);h=h.next
return r
lists=[build([1,4,5]),build([1,3,4]),build([2,6])]
print(to_list(mergeKLists(lists))) # [1,1,2,3,4,4,5,6]القائمة المرتبطة الفردية والزوجية
LeetCode 328 «القائمة المرتبطة الفردية والزوجية»: تجميع جميع العقد ذات الفهارس الفردية أولًا، ثم العقد ذات الفهارس الزوجية (مع فهرسة تبدأ من 1). النهج هو الحفاظ على سلسلتين منفصلتين (فردية وزوجية)، ثم وصلهما عند الانتهاء. يكفي مرور واحد على القائمة، ما يمنحنا زمنًا قدره O(n) ومساحة قدرها O(1). وهذا مثال واضح على تحريك مؤشرين في الوقت نفسه بخطوتين مختلفتين.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def oddEvenList(head):
if not head:
return head
odd = head
even = head.next
even_head = even
while even and even.next:
odd.next = even.next
odd = odd.next
even.next = odd.next
even = even.next
odd.next = even_head
return head
def build(arr):
d=ListNode();c=d
for v in arr: c.next=ListNode(v);c=c.next
return d.next
def to_list(h):
r=[]
while h: r.append(h.val);h=h.next
return r
print(to_list(oddEvenList(build([1,2,3,4,5])))) # [1,3,5,2,4]جمع المفاهيم معًا
تشترك الأنماط الثلاثة في هذا الدرس — دمج القوائم المرتبة، والتقسيم عند نقطة المنتصف، والعثور على العقدة رقم n من النهاية — في فكرة أساسية: استخدام متغيرات مؤشرات إضافية لتتبع المواضع دون ذاكرة إضافية. يسهّل الرأس الوهمي الدمج والحذف؛ وتحدد الفجوة بين slow وfast موضعًا نسبيًا معينًا؛ بينما يؤدي تقديم أحد المؤشرين أولًا إلى إنشاء الفصل المطلوب.
في المقابلة، اذكر النمط الذي تستخدمه قبل كتابة الشيفرة: «سأستخدم أسلوب الفجوة بين مؤشرين للعثور على العقدة رقم n من النهاية في مرور واحد». يوضح ذلك تفكيرًا منظمًا.
اختبار سريع
اختبر مدى فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep التي تناولها هذا الدرس.
مراجعة الدرس
تعلمت في هذا الدرس أن: دمج قائمتين مرتبتين يستخدم رأسًا وهميًا ومقارنة في كل خطوة بزمن O(n+m) ومساحة O(1)، وأن التقسيم عند نقطة المنتصف يستخدم مؤشري slow وfast مع توقف fast عند آخر زوج صالح، وأن العثور على العقدة رقم n من النهاية يتطلب تقديم fast بمقدار n+1 خطوة حتى يصل slow إلى العقدة السابقة. بعد ذلك سنبني المكدسات والطوابير ونطبقها على مسائل المقابلات الكلاسيكية.
تعلم Coding Interview Prep مع معلم ذكاء اصطناعي — مجانًا
اكتب وقم بتشغيل أكوادك الفعلية في المتصفح، واحصل على مساعدة فورية من معلم ذكاء اصطناعي متاح 24/7، واستمر من حيث توقفت على الويب أو في التطبيق.
- الدورات
- 90
- الدروس
- 360
الأسئلة الشائعة
هل درس «الدمج والتقسيم والعثور على العنصر N من النهاية» مجاني؟
نعم — نص درس «الدمج والتقسيم والعثور على العنصر N من النهاية» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «الدمج والتقسيم والعثور على العنصر N من النهاية»؟
ادمج قائمتين مرتبطتين مرتبتين في O(n)، واقسم قائمة عند نقطة المنتصف باستخدام المؤشرين البطيء والسريع، واعثر على العقدة رقم n من الذيل تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟
لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «الدمج والتقسيم والعثور على العنصر N من النهاية»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟
نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- فئة Node وإنشاء القوائم
- عكس قائمة مرتبطة
- اكتشاف الدورات باستخدام خوارزمية Floyd
- الدمج والتقسيم والعثور على العنصر N من النهاية