DSA Interview Prep · درس

الدمج والتقسيم والعثور على العنصر N من النهاية

ادمج قائمتين مرتبطتين مرتبتين في O(n)، واقسم قائمة عند نقطة المنتصف باستخدام المؤشرين البطيء والسريع، واعثر على العقدة رقم n من الذيل

الدرس 4 من 413 خطوة

الدمج والتقسيم والعثور على العنصر N من النهاية درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA 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 إلى العقدة السابقة. بعد ذلك سنبني المكدسات والطوابير ونطبقها على مسائل المقابلات الكلاسيكية.

البدء مجانًا

تعلم Python مع معلم ذكاء اصطناعي — مجانًا

اكتب وقم بتشغيل أكوادك الفعلية في المتصفح، واحصل على مساعدة فورية من معلم ذكاء اصطناعي متاح 24/7، واستمر من حيث توقفت على الويب أو في التطبيق.

الدورات
30
الدروس
120

الأسئلة الشائعة

هل درس «الدمج والتقسيم والعثور على العنصر N من النهاية» مجاني؟

نعم — نص درس «الدمج والتقسيم والعثور على العنصر N من النهاية» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «الدمج والتقسيم والعثور على العنصر N من النهاية»؟

ادمج قائمتين مرتبطتين مرتبتين في O(n)، واقسم قائمة عند نقطة المنتصف باستخدام المؤشرين البطيء والسريع، واعثر على العقدة رقم n من الذيل تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟

لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «الدمج والتقسيم والعثور على العنصر N من النهاية»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟

نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. فئة Node وإنشاء القوائم
  2. عكس قائمة مرتبطة
  3. اكتشاف الدورات باستخدام خوارزمية Floyd
  4. الدمج والتقسيم والعثور على العنصر N من النهاية
← العودة إلى DSA Interview Prep