0Pricing
DSA Interview Prep · บทเรียน

การกลับลำดับลิสต์เชื่อมโยง

กลับลำดับลิสต์เชื่อมโยงเดี่ยวทั้งแบบวนซ้ำด้วยการจัดการการเชื่อมใหม่ของตัวชี้สามตัว และแบบเรียกซ้ำ โดยติดตามแต่ละขั้นบนแผนภาพสไตล์กระดานไวท์บอร์ด

การกลับลำดับลิสต์เชื่อมโยง เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA 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: บันทึกโหนดถัดไปเป็น 2 กลับลิงก์ของ 1 ให้ชี้เป็น None กำหนดโหนดก่อนหน้าเป็น 1 และโหนดปัจจุบันเป็น 2 ขั้นที่ 2: บันทึกโหนดถัดไปเป็น 3 กลับลิงก์ของ 2 ให้ชี้ไปยัง 1 กำหนดโหนดก่อนหน้าเป็น 2 และโหนดปัจจุบันเป็น 3 ขั้นที่ 3: บันทึกโหนดถัดไปเป็น None กลับลิงก์ของ 3 ให้ชี้ไปยัง 2 กำหนดโหนดก่อนหน้าเป็น 3 และโหนดปัจจุบันเป็น None ลูปจบลง แล้วคืนค่าโหนดก่อนหน้าเป็น 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' ให้กลับลำดับรายการย่อยตั้งแต่ตำแหน่งซ้ายถึงตำแหน่งขวา (เริ่มนับจาก 1) โดยใช้การเดินผ่านรายการเพียงรอบเดียว เคล็ดลับคือค้นหาโหนดก่อนรายการย่อย (ใช้หัวรายการจำลองเพื่อให้กรณีนี้ใช้ได้เสมอ) จากนั้นกลับลำดับด้วยตัวชี้สามตัวเป็นจำนวนขั้นเท่ากับผลต่างระหว่างตำแหน่งขวากับตำแหน่งซ้ายพอดี แล้วเชื่อมส่วนที่กลับลำดับแล้วกลับเข้ากับรายการโดยรอบ

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) กลยุทธ์คือหาจุดกึ่งกลางด้วยตัวชี้ช้าและเร็ว กลับลำดับครึ่งหลังในตำแหน่งเดิม เปรียบเทียบโหนดของทั้งสองครึ่งทีละโหนด แล้วเลือกคืนสภาพรายการตามต้องการ วิธีนี้เชื่อมทักษะพื้นฐานสองอย่างเข้าด้วยกัน ได้แก่ การหาจุดกึ่งกลางและการกลับลำดับ

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) เนื่องจากความลึกของการเรียก ซึ่งอาจทำให้สแตกทำงานล้นเมื่อรายการยาวมาก (ขีดจำกัดเริ่มต้นของไพธอนอยู่ที่ประมาณ 1,000 ระดับการเรียกซ้ำ)

ในการสัมภาษณ์งาน ควรเขียนแบบวนซ้ำก่อนเพื่อแสดงให้เห็นว่าเข้าใจข้อจำกัดด้านพื้นที่ จากนั้นจึงกล่าวถึงแบบเรียกซ้ำว่าเป็นทางเลือกที่อ่านง่ายกว่า หากกำหนดความยาวรายการไว้ได้

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)

ข้อผิดพลาดที่พบบ่อยในการกลับลำดับ

ข้อผิดพลาดเกือบทั้งหมดในการกลับลำดับเกิดจากสามประการ ประการแรกคือไม่บันทึกโหนดถัดไปก่อนเขียนทับ: curr.next = prev จะทำลายการอ้างอิงไปข้างหน้า หากไม่ได้บันทึก next_node ไว้ก่อน ประการที่สองคือไม่คืนค่าโหนดก่อนหน้า: เมื่อจบลูป 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 การจัดเรียงรายการใหม่ และการกลับลำดับระหว่างตำแหน่งต่าง ๆ ล้วนใช้รูปแบบการวนซ้ำด้วยตัวชี้สามตัวแบบเดียวกัน เมื่อรูปแบบนี้กลายเป็นสิ่งที่ทำได้โดยอัตโนมัติ คุณจะมีสมาธิเหลือสำหรับพิจารณาโครงสร้างของโจทย์ในระดับที่สูงขึ้น

ควรฝึกการกลับลำดับอยู่เสมอจนสามารถเขียนได้จากความจำภายในเวลาไม่ถึงสองนาที เพราะรูปแบบนี้จะปรากฏในรูปแบบใดรูปแบบหนึ่งแทบทุกการสัมภาษณ์งานเกี่ยวกับรายการเชื่อมโยง

ตรวจสอบความเข้าใจอย่างรวดเร็ว

ทดสอบความเข้าใจแนวคิดเรื่องโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้

ทบทวนบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า: รูปแบบบันทึก-กลับลิงก์-เลื่อน-เลื่อนแบบวนซ้ำกลับลำดับรายการได้ในเวลา O(n) และใช้พื้นที่ O(1) วิธีแบบเรียกซ้ำเชื่อว่าส่วนต่อท้ายกลับลำดับไว้แล้ว และแก้ไขเพียงลิงก์สุดท้าย และ การกลับลำดับเป็นขั้นตอนย่อยสำคัญในการตรวจจับพาลินโดรม การจัดเรียงรายการใหม่ และการกลับลำดับเป็นกลุ่ม k ต่อไปเราจะสำรวจการตรวจจับวงจรด้วยอัลกอริทึมของฟลอยด์

คำถามที่พบบ่อย

บทเรียน “การกลับลำดับลิสต์เชื่อมโยง” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การกลับลำดับลิสต์เชื่อมโยง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การกลับลำดับลิสต์เชื่อมโยง”

กลับลำดับลิสต์เชื่อมโยงเดี่ยวทั้งแบบวนซ้ำด้วยการจัดการการเชื่อมใหม่ของตัวชี้สามตัว และแบบเรียกซ้ำ โดยติดตามแต่ละขั้นบนแผนภาพสไตล์กระดานไวท์บอร์ด คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “การกลับลำดับลิสต์เชื่อมโยง” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม

ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. คลาสโหนดและการสร้างลิสต์
  2. การกลับลำดับลิสต์เชื่อมโยง
  3. การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd
  4. ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย
← กลับไปที่ DSA Interview Prep