นากกายสีมิ้นต์กำลังสลับโหนดจากลูกโซ่เรียงลำดับสองเส้นเข้าเป็นเส้นเดียวบนโต๊ะสว่าง

LeetCode #21 Merge Two Sorted Lists: เลือกโหนดเล็กแล้วต่อหาง

พี่โชว์Engineering

โจทย์ให้หัวของ singly linked list สองเส้นซึ่งเรียงจากน้อยไปมาก แล้วคืนลิสต์เรียงเส้นเดียวครับ หัวใจไม่ใช่ sort ใหม่ แต่คือเทียบหัวที่ยังเหลือ เลือกค่าต่ำกว่าไปต่อหาง แล้วขยับเฉพาะลิสต์นั้น

วิธีที่ 1: Iterative พร้อม dummy node — ค่าเริ่มต้นที่แนะนำ

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

วิธีที่ 2: Recursion

เลือกหัวที่เล็กกว่า แล้วให้ next ของหัวนั้นเป็นผล merge ของส่วนที่เหลือ เวลา O(n+m), call stack O(n+m) โค้ดสั้นแต่เสี่ยง recursion limit เมื่อ input ยาว และแก้ pointer เดิมเช่นเดียวกับวิธีแรก

วิธีที่ 3: สร้างโหนดใหม่

เดินสองลิสต์เหมือน iterative แต่ copy ค่าเข้า node ใหม่ เวลา O(n+m), พื้นที่ O(n+m) เหมาะเมื่อ caller ยังต้องใช้โครงสร้างเดิมและ ownership ห้าม mutation ข้อเสียคือ allocation เพิ่มและ identity ของโหนดหาย

import unittest


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


def from_list(values):
    dummy = tail = ListNode()
    for value in values:
        tail.next = ListNode(value)
        tail = tail.next
    return dummy.next


def to_list(node):
    values = []
    while node:
        values.append(node.val)
        node = node.next
    return values


def merge_iterative(a, b):
    dummy = tail = ListNode()
    while a and b:
        if a.val <= b.val:
            tail.next, a = a, a.next
        else:
            tail.next, b = b, b.next
        tail = tail.next
    tail.next = a or b
    return dummy.next


def merge_recursive(a, b):
    if not a or not b:
        return a or b
    if a.val <= b.val:
        a.next = merge_recursive(a.next, b)
        return a
    b.next = merge_recursive(a, b.next)
    return b


def merge_copy(a, b):
    dummy = tail = ListNode()
    while a or b:
        if b is None or (a is not None and a.val <= b.val):
            value, a = a.val, a.next
        else:
            value, b = b.val, b.next
        tail.next = ListNode(value)
        tail = tail.next
    return dummy.next


class MergeListsTests(unittest.TestCase):
    def test_methods_and_edges(self):
        cases = [([], [], []), ([], [1], [1]), ([1, 2, 4], [1, 3, 4], [1, 1, 2, 3, 4, 4])]
        for left, right, expected in cases:
            for method in (merge_iterative, merge_recursive, merge_copy):
                with self.subTest(method=method.__name__, left=left, right=right):
                    self.assertEqual(to_list(method(from_list(left), from_list(right))), expected)


if __name__ == "__main__":
    unittest.main()

เลือกตามสถานการณ์

เงื่อนไขIterative reuseRecursionCopy ใหม่
input ใหญ่ดีที่สุดเสี่ยง stackใช้ได้แต่ allocation สูง
memory จำกัดO(1) เพิ่มO(n+m) stackO(n+m) heap
readabilityชัดเมื่อใช้ dummyสั้นที่สุดชัดเรื่อง ownership
input sorted/streamingต้อง sorted; ต่อ remainder ได้ต้อง sorted; ไม่เหมาะ stream ยาวต้อง sorted; ปรับเป็น iterator ได้
อนุญาตแก้ inputใช่ใช่ไม่จำเป็น

best default ภายใต้ constraints ของโจทย์คือ iterative reuse เพราะเวลาเส้นตรงและพื้นที่เพิ่มคงที่

Edge cases

ทดสอบลิสต์ว่างหนึ่งหรือสองเส้น, ค่าซ้ำ, ค่าติดลบ และเส้นที่ค่าทั้งหมดเล็กกว่าอีกเส้น ใช้ <= เพื่อให้โหนดจากซ้ายมาก่อนเมื่อค่าเท่ากัน ซึ่งทำให้ merge มีเสถียรภาพหาก identity สำคัญ

Recap

  • รักษา invariant ว่าช่วงหลัง dummy เรียงแล้ว
  • ต่อ remainder ทั้งก้อนได้เมื่ออีกเส้นหมด
  • iterative reuse เป็น default ที่สมดุลที่สุด
  • copy ใหม่เมื่อห้ามแตะ input เดิม