0Pricing
Coding Interview Prep · درس

عكس قائمة مرتبطة

اعكس قائمة مرتبطة أحادية بصورة تكرارية عبر إعادة توصيل ثلاثة مؤشرات، وبصورة ذاتية مع تتبع كل خطوة في مخطط يشبه الرسم على السبورة

عكس قائمة مرتبطة درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

لماذا يُعد عكس القائمة أساسياً

يُعد عكس قائمة مرتبطة من أكثر أسئلة مقابلات البرمجة شيوعاً. فهو يختبر قدرتك على معالجة المؤشرات بدقة من دون فقدان تتبّع العقد. وتظهر أشكاله المختلفة بوصفها مسائل مستقلة، وكذلك كخطوات فرعية ضمن خوارزميات أكبر مثل اكتشاف القوائم متناظرة، وإعادة ترتيب القائمة، والعكس ضمن مجموعات من k عناصر.

يستخدم الأسلوب التكراري ثلاثة مؤشرات: prev وcurr وnext_node. أما الأسلوب递归ي فيعبّر عن المنطق نفسه باستخدام المرور عبر مكدس الاستدعاءات. يحقق كلا الأسلوبين زمناً قدره O(n)، ومساحة قدرها O(1) في الأسلوب التكراري.

العكس التكراري باستخدام ثلاثة مؤشرات

في كل خطوة من العكس التكراري: احفظ curr.next حتى لا تفقد بقية القائمة، واعكس curr.next ليشير إلى الخلف نحو prev، ثم حرّك prev إلى curr، وحرّك curr إلى next المحفوظ. عندما تصبح قيمة curr هي None، تنتهي الحلقة ويصبح prev هو الرأس الجديد.

وسيلة مفيدة لتذكّر الخطوات: احفظ، اعكس، حرّك، حرّك.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverse_list(head):
    prev, curr = None, head
    while curr:
        next_node  = curr.next   # Save
        curr.next  = prev        # Flip
        prev       = curr        # Advance prev
        curr       = next_node   # Advance curr
    return prev  # new head

# Test
nodes = [ListNode(i) for i in range(1, 6)]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]
head = reverse_list(nodes[0])
while head:
    print(head.val, end=' ')  # 5 4 3 2 1
    head = head.next

التتبّع خطوة بخطوة

دعونا نتتبّع reverse_list على 1 -> 2 -> 3. في البداية prev=None, curr=1. الخطوة 1: احفظ next=2، واعكس 1.next=None، ثم اجعل prev=1 وcurr=2. الخطوة 2: احفظ next=3، واعكس 2.next=1، ثم اجعل prev=2 وcurr=3. الخطوة 3: احفظ next=None، واعكس 3.next=2، ثم اجعل prev=3 وcurr=None. تنتهي الحلقة؛ أعد prev=3، وهو الرأس الجديد لـ 3 -> 2 -> 1.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverse_list_traced(head):
    prev, curr = None, head
    step = 0
    while curr:
        step += 1
        next_node = curr.next
        curr.next = prev
        print(f'Step {step}: flipped {curr.val}.next -> {prev.val if prev else None}')
        prev = curr
        curr = next_node
    return prev

nodes = [ListNode(i) for i in [1, 2, 3]]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]
head = reverse_list_traced(nodes[0])
print('New head:', head.val)  # 3

العكس باستخدام الاستدعاء الذاتي

يعتمد النهج القائم على الاستدعاء الذاتي على أن reverse_list(head.next) يعيد الرأس الجديد للجزء اللاحق الذي عُكست قائمته بالفعل. ولا يتبقى سوى عكس المؤشر بين head وhead.next: اضبط head.next.next = head (وجّه العقدة الثانية القديمة إلى العقدة الأولى القديمة) وhead.next = None (اقطع الرابط الأمامي القديم). يصعد الرأس الجديد تدريجيًا من حالة الأساس.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverse_list_rec(head):
    # Base case: empty or single node
    if not head or not head.next:
        return head
    new_head = reverse_list_rec(head.next)  # reverse suffix
    head.next.next = head   # former second node points back
    head.next = None        # sever forward link
    return new_head

nodes = [ListNode(i) for i in range(1, 5)]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]
head = reverse_list_rec(nodes[0])
while head:
    print(head.val, end=' ')  # 4 3 2 1
    head = head.next

عكس قائمة فرعية (LeetCode 92)

تطلب مسألة LeetCode 92 «عكس القائمة المترابطة II» عكس القائمة الفرعية من الموضع left إلى الموضع right (مع ترقيم يبدأ من 1) في مرور واحد. تكمن الحيلة في تحديد العقدة التي تسبق القائمة الفرعية (استخدم رأسًا وهميًا حتى يكون ذلك صالحًا دائمًا)، ثم تنفيذ عكس المؤشرات الثلاثة لعدد خطوات يساوي (right - left) تمامًا، وأخيرًا إعادة وصل الجزء المعكوس ببقية القائمة.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverseBetween(head, left, right):
    dummy = ListNode(0, head)
    pre = dummy
    # Advance pre to node just before position 'left'
    for _ in range(left - 1):
        pre = pre.next
    curr = pre.next
    for _ in range(right - left):
        next_node   = curr.next
        curr.next   = next_node.next
        next_node.next = pre.next
        pre.next    = next_node
    return dummy.next

nodes = [ListNode(i) for i in range(1, 6)]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]
head = reverseBetween(nodes[0], 2, 4)
while head:
    print(head.val, end=' ')  # 1 4 3 2 5
    head = head.next

عكس العقد في مجموعات من k (LeetCode 25)

تعكس مسألة LeetCode 25 «عكس العقد في مجموعات من k» كل مجموعة متتالية مكوّنة من k عقد. النهج هو: تحقّق من بقاء k عقد؛ وإذا لم تبقَ، فاتركها كما هي. اعكس العقد k التالية باستخدام الطريقة التكرارية، ثم اعكس القائمة المتبقية استدعائيًا ووصلها. يظل التعقيد الزمني O(n)، مع عمق استدعاءات تكرارية يساوي O(n/k).

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverseKGroup(head, k):
    # Check if k nodes are available
    curr, count = head, 0
    while curr and count < k:
        curr = curr.next
        count += 1
    if count < k:
        return head   # fewer than k nodes left, keep as-is
    # Reverse k nodes
    prev, curr = None, head
    for _ in range(k):
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt
    # head is now the tail of the reversed group
    head.next = reverseKGroup(curr, k)
    return prev

nodes = [ListNode(i) for i in range(1, 6)]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]
head = reverseKGroup(nodes[0], 2)
while head:
    print(head.val, end=' ')  # 2 1 4 3 5
    head = head.next

قائمة مترابطة متناظرة

تطلب مسألة LeetCode 234 «القائمة المترابطة المتناظرة» التحقق مما إذا كانت القائمة المترابطة متناظرة في زمن O(n) ومساحة O(1). الاستراتيجية هي: حدّد نقطة المنتصف باستخدام مؤشري slow وfast، واعكس النصف الثاني في مكانه، ثم قارن نصفي القائمة عقدةً بعقدة، وبعد ذلك أعد القائمة إلى حالتها الأصلية اختياريًا. تجمع هذه الطريقة بين تحديد نقطة المنتصف والعكس، وهما مهارتان أساسيتان.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def isPalindrome(head):
    # Find mid
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    # Reverse second half
    prev, curr = None, slow
    while curr:
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt
    # Compare
    left, right = head, prev
    while right:
        if left.val != right.val:
            return False
        left  = left.next
        right = right.next
    return True

def build(arr):
    d = ListNode(0)
    c = d
    for v in arr:
        c.next = ListNode(v)
        c = c.next
    return d.next

print(isPalindrome(build([1,2,2,1])))  # True
print(isPalindrome(build([1,2,3])))    # False

مقارنة بين العكس التكراري والاستدعاء الذاتي

يستخدم العكس التكراري مساحة O(1)، ولذلك يُفضَّل عمومًا. أما العكس باستخدام الاستدعاء الذاتي فيستخدم مساحة مكدس O(n) بسبب عمق الاستدعاءات، وقد يؤدي ذلك إلى تجاوز سعة المكدس عند التعامل مع قوائم طويلة جدًا (فالحد الافتراضي في Python يقارب 1000 مستوى من الاستدعاء الذاتي).

في مقابلة تقنية، نفّذ النسخة التكرارية أولًا لإظهار وعيك بقيود المساحة، ثم اذكر النسخة القائمة على الاستدعاء الذاتي كبديل أوضح إذا كان طول القائمة محدودًا.

import sys
print('Default recursion limit:', sys.getrecursionlimit())
# For a list of 10,000 nodes the recursive reversal would hit this limit
# Iterative reversal has no such constraint

# Increase if needed (use sparingly):
# sys.setrecursionlimit(20000)

الأخطاء الشائعة عند عكس القائمة

تتسبب ثلاثة أخطاء في معظم مشكلات العكس تقريبًا. أولًا، عدم حفظ next قبل الكتابة فوقه: تؤدي العبارة curr.next = prev إلى تدمير المرجع الأمامي إذا لم يتم حفظ next_node. ثانيًا، عدم إعادة prev: عند نهاية الحلقة تكون قيمة curr هي None، بينما يكون prev هو الرأس الجديد. ثالثًا، استخدام حالة أساس خاطئة في الاستدعاء الذاتي: يؤدي نسيان not head.next إلى عدم معالجة قائمة مكوّنة من عقدة واحدة، ويتسبب في حدوث AttributeError.

# Minimal correct iterative reversal — annotated against common bugs
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reverse_list(head):
    prev, curr = None, head
    while curr:
        next_node = curr.next   # BUG if omitted: lose rest of list
        curr.next = prev
        prev      = curr
        curr      = next_node
    return prev               # BUG if you return curr: it is None

nodes = [ListNode(i) for i in [1, 2, 3]]
nodes[0].next = nodes[1]
nodes[1].next = nodes[2]
h = reverse_list(nodes[0])
while h:
    print(h.val, end=' ')  # 3 2 1
    h = h.next

إعادة ترتيب القائمة (LeetCode 143)

تعيد مسألة LeetCode 143 «إعادة ترتيب القائمة» ترتيب L0 → L1 → L2 → ... → Ln إلى L0 → Ln → L1 → Ln-1 → L2 → Ln-2 في زمن O(n) ومساحة O(1). يتكوّن الحل من ثلاث خطوات: تحديد نقطة المنتصف، وعكس النصف الثاني، ثم تشبيك النصفين بالتناوب. إن إتقان العكس يجعل هذه المسألة التي تبدو معقدة مجرد تركيب مباشر لأدوات مألوفة.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def reorderList(head):
    if not head or not head.next:
        return
    # Find mid
    slow = fast = head
    while fast.next and fast.next.next:
        slow = slow.next
        fast = fast.next.next
    # Reverse second half
    prev, curr = None, slow.next
    slow.next = None
    while curr:
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt
    # Interleave
    first, second = head, prev
    while second:
        tmp1, tmp2 = first.next, second.next
        first.next = second
        second.next = tmp1
        first, second = tmp1, tmp2

nodes = [ListNode(i) for i in range(1, 5)]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]
reorderList(nodes[0])
h = nodes[0]
while h:
    print(h.val, end=' ')  # 1 4 2 3
    h = h.next

الملخّص: العكس لبنة أساسية

نادرًا ما يكون عكس القائمة المترابطة هو الهدف النهائي؛ فهو لبنة أساسية. يعتمد اكتشاف التناظر، والعكس في مجموعات من k، وإعادة ترتيب القائمة، والعكس بين موضعين، كلها على نمط العكس التكراري نفسه باستخدام ثلاثة مؤشرات. وبمجرد أن يصبح هذا النمط تلقائيًا، يمكنكم توجيه تركيزكم الذهني إلى بنية المسألة على المستوى الأعلى.

احرصوا دائمًا على التدرّب على العكس حتى تتمكنوا من كتابته من الذاكرة في أقل من دقيقتين؛ إذ سيظهر بشكل أو بآخر في معظم جولات المقابلات المتعلقة بالقوائم المترابطة.

اختبار سريع

اختبروا فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.

مراجعة الدرس

في هذا الدرس تعلّمتم أن: نمط الحفظ-القلب-التقدّم-التقدّم التكراري يعكس القائمة في زمن O(n) ومساحة O(1)، والنهج القائم على الاستدعاء الذاتي يفترض أن الجزء اللاحق معكوس بالفعل ولا يصلح سوى الرابط الأخير، والعكس خطوة أساسية في اكتشاف التناظر، وإعادة ترتيب القائمة، والعكس في مجموعات من k. في الدرس التالي سنستكشف اكتشاف الدورات باستخدام خوارزمية Floyd.

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

هل درس «عكس قائمة مرتبطة» مجاني؟

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

ماذا ستتعلم في «عكس قائمة مرتبطة»؟

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

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

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

كم من الوقت يستغرق درس «عكس قائمة مرتبطة»؟

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

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

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

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

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