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

ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย

ผสานลิสต์เชื่อมโยงที่เรียงแล้วสองลิสต์ในเวลา O(n) แบ่งลิสต์ที่จุดกึ่งกลางด้วยตัวชี้ช้า–เร็ว และค้นหาโหนดลำดับที่ n จากส่วนท้าย

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

รูปแบบสำคัญสามแบบของรายการเชื่อมโยง

บทเรียนนี้ครอบคลุมการดำเนินการพื้นฐานสามอย่างของรายการเชื่อมโยง ซึ่งปรากฏอยู่เสมอในฐานะส่วนประกอบของโจทย์ที่ยากขึ้น ได้แก่ การผสานรายการที่เรียงลำดับแล้วสองรายการ (ใช้ในการเรียงลำดับแบบผสานและการผสาน k รายการ) การแบ่งรายการที่จุดกึ่งกลาง (ใช้ในการเรียงลำดับแบบผสานและการตรวจจับพาลินโดรม) และ การหาโหนดลำดับที่ n จากท้ายรายการ (ใช้ในการลบโหนดลำดับที่ n จากท้ายรายการ)

ทั้งสามอย่างอาศัยเทคนิคที่คุณได้เห็นมาแล้ว ได้แก่ โหนดหัวรายการจำลอง ตัวชี้ช้าและเร็ว และการติดตามขอบเขตอย่างรอบคอบ

การผสานรายการที่เรียงลำดับแล้วสองรายการ

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 ไปยังค่าว่าง เชื่อม 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 เคลื่อนที่ครั้งละ 1 ขั้น ส่วน fast เคลื่อนที่ครั้งละ 2 ขั้น เมื่อ 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 ล่วงหน้า slow ไป n ขั้น จากนั้นเลื่อนทั้งคู่ไปพร้อมกันจนกว่า 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 ขั้นในการลบโหนดลำดับที่ n

จุดละเอียดสำคัญคือการเลื่อนตัวชี้เร็วไป n+1 ขั้น (ไม่ใช่ n ขั้น) จากส่วนหัวจำลอง หลังจากเลื่อน n+1 ขั้น ตัวชี้เร็วจะอยู่ล่วงหน้าตัวชี้ช้า n+1 ตำแหน่ง (ทั้งคู่เริ่มจากส่วนหัวจำลอง) เมื่อตัวชี้เร็วไปถึงไม่มีค่า ซึ่งอยู่ถัดจากโหนดท้ายไปหนึ่งตำแหน่ง ตัวชี้ช้าจะอยู่ก่อนหน้าคำว่าไม่มีค่า n+1 ตำแหน่ง กล่าวคืออยู่ที่ตำแหน่ง (ความยาว - n - 1) เมื่อเริ่มนับจากศูนย์ หรืออยู่ที่โหนดก่อนหน้าโหนดเป้าหมาย การทำเช่นนี้ทำให้ slow.next = slow.next.next ลบโหนดลำดับที่ n จากท้ายได้อย่างเรียบร้อย

# 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) คือเลื่อนตัวชี้สองตัว โดยให้แต่ละตัวเริ่มจากลิสต์หนึ่งรายการ เมื่อตัวชี้ไปถึงไม่มีค่า ให้เปลี่ยนไปชี้ยังส่วนหัวของอีกลิสต์หนึ่ง หลังจากเดินไม่เกินจำนวนโหนดของลิสต์ A + จำนวนโหนดของลิสต์ B ขั้น ตัวชี้ทั้งคู่จะเดินทางด้วยระยะทางรวมเท่ากัน และต้องมาถึงโหนดจุดตัด (หรือทั้งคู่จะอยู่ที่ไม่มีค่าหากไม่มีจุดตัด)

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

ในการสัมภาษณ์ ควรบอกชื่อรูปแบบที่กำลังใช้ก่อนเขียนโค้ด เช่น 'ฉันจะใช้เทคนิคช่องว่างระหว่างตัวชี้สองตัวเพื่อค้นหาโหนดลำดับที่ n จากท้ายในการเดินผ่านรอบเดียว' วิธีนี้แสดงให้เห็นถึงการคิดอย่างเป็นระบบ

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

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

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

ในบทเรียนนี้ คุณได้เรียนรู้ว่า การผสานลิสต์ที่เรียงลำดับแล้วสองรายการใช้ส่วนหัวจำลองและการเปรียบเทียบในทุกขั้นตอน โดยใช้เวลา O(n+m) และหน่วยความจำ O(1) การแบ่งที่จุดกึ่งกลางใช้ตัวชี้ช้าและตัวชี้เร็ว โดยให้ตัวชี้เร็วหยุดที่คู่ที่ถูกต้องคู่สุดท้าย และ การค้นหาโหนดลำดับที่ n จากท้ายจะเลื่อนตัวชี้เร็วล่วงหน้า n+1 ขั้น เพื่อให้ตัวชี้ช้าไปอยู่ที่โหนดก่อนหน้า ถัดไปเราจะสร้างสแตกและคิว แล้วนำไปใช้กับโจทย์สัมภาษณ์คลาสสิก

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

บทเรียน “ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย” ฟรีหรือไม่

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

คุณจะเรียนรู้อะไรในบทเรียน “ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย”

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

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

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

บทเรียน “ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย” ใช้เวลานานแค่ไหน

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

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

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

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

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