นากกายกำลังเทียบรางลูกปัดเรียงสองรางและชี้ตำแหน่งกึ่งกลางร่วม

LeetCode 4 Median of Two Sorted Arrays: แบ่งสองฝั่งให้ซ้ายไม่เกินขวา

พี่โชว์Engineering

โจทย์ Median of Two Sorted Arrays ให้ array เรียงสองชุดและต้องหามัธยฐานของข้อมูลรวม โดย constraint ต้องการเวลาเชิงลอการิทึม ความยากคือหาตำแหน่งกลางโดยไม่ merge ทุกสมาชิกครับ

สรุปโจทย์ด้วยคำของเรา

เราต้องแบ่งข้อมูลรวมเป็นครึ่งซ้ายและครึ่งขวาให้จำนวนสมาชิกสมดุล และค่าสูงสุดฝั่งซ้ายไม่เกินค่าต่ำสุดฝั่งขวา ถ้าจำนวนรวมเป็นคี่ คำตอบคือหัวของครึ่งขวาหรือท้ายของครึ่งซ้ายตามนิยาม partition; ถ้าเป็นคู่ให้เฉลี่ยสองค่ากลาง

วิธีที่ 1: merge จนถึงกลาง

ใช้ pointer สองตัวแบบ merge sort แล้วหยุดเมื่อถึงตำแหน่งกลาง ไม่จำเป็นต้องสร้าง list ทั้งก้อน

  • เวลา O(m + n) ในกรณีแย่สุด
  • หน่วยความจำเพิ่ม O(1)
  • อ่านง่ายและเหมาะเป็น oracle ใน test แต่ไม่ผ่านเป้าหมาย logarithmic ของโจทย์

วิธีที่ 2: ตัด k-th ทีละครึ่ง

นิยามฟังก์ชันหาอันดับที่ k แล้วตัด prefix ของ array ที่มีค่าจุดตรวจเล็กกว่า ทำซ้ำจน k เหลือหนึ่ง

  • เวลา O(log(m + n))
  • หน่วยความจำ O(1) ถ้าเขียน iterative
  • generalize ไปหา percentile ได้ แต่ index arithmetic มีหลายจุดให้พลาด

วิธีที่ 3: binary partition บน array ที่สั้นกว่า

เดาตำแหน่งตัดใน array สั้น แล้วคำนวณจุดตัดของอีก array ให้ครึ่งซ้ายมีจำนวนตามต้องการ ถ้า leftA > rightB ขยับซ้าย ถ้า leftB > rightA ขยับขวา เมื่อทั้งสองเงื่อนไขผ่าน เราพบ partition กลาง

  • เวลา O(log(min(m, n)))
  • หน่วยความจำเพิ่ม O(1)
  • เป็น best default ตาม constraint แต่ต้องจัด sentinel และ array ว่างให้ครบ
import unittest


def median_merge(a, b):
    total = len(a) + len(b)
    target = total // 2
    i = j = 0
    previous = current = 0
    for _ in range(target + 1):
        previous = current
        if j >= len(b) or (i < len(a) and a[i] <= b[j]):
            current = a[i]
            i += 1
        else:
            current = b[j]
            j += 1
    return float(current) if total % 2 else (previous + current) / 2


def median_partition(a, b):
    if len(a) > len(b):
        a, b = b, a
    total = len(a) + len(b)
    half = (total + 1) // 2
    low, high = 0, len(a)

    while low <= high:
        cut_a = (low + high) // 2
        cut_b = half - cut_a
        left_a = float("-inf") if cut_a == 0 else a[cut_a - 1]
        right_a = float("inf") if cut_a == len(a) else a[cut_a]
        left_b = float("-inf") if cut_b == 0 else b[cut_b - 1]
        right_b = float("inf") if cut_b == len(b) else b[cut_b]

        if left_a <= right_b and left_b <= right_a:
            if total % 2:
                return float(max(left_a, left_b))
            return (max(left_a, left_b) + min(right_a, right_b)) / 2
        if left_a > right_b:
            high = cut_a - 1
        else:
            low = cut_a + 1
    raise ValueError("inputs must be sorted and not both empty")


class MedianTests(unittest.TestCase):
    def check(self, a, b, expected):
        self.assertEqual(median_merge(a, b), expected)
        self.assertEqual(median_partition(a, b), expected)

    def test_odd_and_even(self):
        self.check([1, 3], [2], 2.0)
        self.check([1, 2], [3, 4], 2.5)

    def test_empty_duplicates_and_unbalanced(self):
        self.check([], [5], 5.0)
        self.check([1, 1], [1, 1], 1.0)
        self.check([1], [2, 3, 4, 5, 6], 3.5)


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

Decision table

สถานการณ์Merge ถึงกลางK-th eliminationBinary partition
input เล็กอ่านง่ายที่สุดเกินจำเป็นใช้ได้
input ใหญ่มากช้าตามจำนวนสมาชิกดีดีที่สุด
memory จำกัดO(1) หากไม่สร้าง listO(1)O(1)
readability สำคัญดีปานกลางต้องอธิบาย invariant
input เป็น stream เรียงใช้ได้และหยุดกลางต้อง random accessต้อง random access
ห้ามแก้ inputผ่านผ่านผ่าน
input ยังไม่เรียงต้อง sort ก่อนใช้ไม่ได้ตรง ๆใช้ไม่ได้ตรง ๆ

Best default: binary partition บน array สั้นกว่า เพราะตรง constraint O(log(min(m,n))) และไม่สร้างข้อมูลรวม

Edge cases

  • array หนึ่งว่าง แต่อีก array ต้องไม่ว่าง
  • จำนวนรวมเป็นคู่กับคี่
  • ค่าซ้ำ, ค่าติดลบ และขนาดต่างกันมาก
  • จุดตัดอยู่ก่อนสมาชิกแรกหรือหลังสมาชิกสุดท้าย จึงต้องมี sentinel
  • input ไม่เรียงทำให้ invariant พัง ควรถือเป็น precondition ไม่ตรวจด้วย scan เพราะจะทำลาย complexity

Recap

  • แบ่งครึ่งซ้ายให้มี (m+n+1)//2 สมาชิก
  • search บน array ที่สั้นกว่า
  • partition ถูกเมื่อ leftA <= rightB และ leftB <= rightA
  • ใช้ merge เป็น oracle ใน test และ partition เป็นคำตอบหลัก