นากต้นฉบับตามวงแหวนของแผ่นค่าที่หมุนแล้วไปยังแผ่นต่ำสุดบนฉากมิ้นต์

LeetCode #153 หาเลขน้อยสุดใน rotated array: อ่านรอยต่อด้วย binary search

พี่โชว์Engineering

LeetCode #153 ให้ array จำนวนเต็มที่เดิมเรียงเพิ่มแบบไม่ซ้ำ แล้วถูกหมุน 0 ถึง n-1 ตำแหน่ง เราต้องหาค่าน้อยสุดในเวลาเป้าหมาย O(log n) เช่น [4,5,6,7,0,1,2] ตอบ 0

WHY ก่อน HOW: ข้อมูลยังประกอบด้วยช่วงเรียงสองช่วง เพียงเอาหางมาต่อหน้าหรือกลับกัน จุดต่ำสุดจึงเป็น “รอยต่อ” เพียงแห่งเดียว เหมือนชั้นหนังสือเรียงหมายเลขแล้วหมุนวงกลม เราไม่ต้องเปิดทุกเล่มถ้าใช้เลขปลายขวาบอกว่ากำลังอยู่ฝั่งไหน

วิธี 1: เดินหาค่าน้อยสุด

อ่านทุกสมาชิกแล้วเก็บ minimum ใช้เวลา O(n) พื้นที่ O(1) อ่านง่ายที่สุดและทำกับ iterable/stream ได้โดยไม่ต้อง random access วิธีนี้ยังเป็น oracle ดีมากสำหรับ test แต่ไม่ผ่านเป้าหมาย O(log n) ของโจทย์

วิธี 2: เรียงสำเนาแล้วหยิบตัวแรก

sorted(nums)[0] ใช้เวลา O(n log n) และพื้นที่ O(n) อ่านสั้นแต่ทิ้งโครงสร้าง “เดิมเรียงแล้วหมุน” ไปทั้งหมด ถ้าใช้ nums.sort() จะเปลี่ยน input ซึ่งไม่จำเป็น จึงเหมาะแค่ baseline สำหรับข้อมูลจิ๋ว ไม่ใช่วิธีสัมภาษณ์ที่ดี

วิธี 3: Binary search เทียบกับปลายขวา

ตั้ง left=0, right=n-1 แล้วดู mid ถ้า nums[mid] > nums[right] จุดต่ำสุดต้องอยู่ขวาของ mid; ไม่เช่นนั้น mid อาจเป็นคำตอบ จึงเก็บมันไว้ด้วย right=mid เมื่อสองขอบมาชนกัน เราพบรอยต่อ ใช้เวลา O(log n) พื้นที่ O(1) และไม่แก้ input นี่คือ best default ภายใต้ constraints ที่สมาชิกไม่ซ้ำ

from __future__ import annotations

import unittest


def find_min_linear(nums: list[int]) -> int:
    return min(nums)


def find_min_sort_copy(nums: list[int]) -> int:
    return sorted(nums)[0]


def find_min_binary(nums: list[int]) -> int:
    left, right = 0, len(nums) - 1
    while left < right:
        mid = left + (right - left) // 2
        if nums[mid] > nums[right]:
            left = mid + 1
        else:
            right = mid
    return nums[left]


class FindMinimumTests(unittest.TestCase):
    def test_examples_and_edges(self):
        cases = [
            ([3, 4, 5, 1, 2], 1),
            ([4, 5, 6, 7, 0, 1, 2], 0),
            ([11, 13, 15, 17], 11),
            ([9], 9),
            ([-1, 0, 1, -3, -2], -3),
        ]
        for solve in (find_min_linear, find_min_sort_copy, find_min_binary):
            for values, expected in cases:
                original = values.copy()
                with self.subTest(method=solve.__name__, values=values):
                    self.assertEqual(solve(values), expected)
                    self.assertEqual(values, original)

    def test_every_rotation_against_linear_oracle(self):
        base = [-9, -2, 0, 4, 12, 30]
        for pivot in range(len(base)):
            rotated = base[pivot:] + base[:pivot]
            self.assertEqual(find_min_binary(rotated), find_min_linear(rotated))


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

Decision table

เงื่อนไขLinear scanSort สำเนาBinary search
Input เล็ก / เน้น readabilityชัดที่สุดสั้นแต่เกินงานต้องอธิบาย invariant
Input ใหญ่O(n)O(n log n)O(log n)
Memory จำกัดO(1)O(n)O(1)
ข้อมูลยัง sorted-rotated และไม่ซ้ำไม่ใช้ประโยชน์ไม่ใช้ประโยชน์เหมาะสุด
Streaming inputทำได้ต้องเก็บครบต้อง random access
ห้ามแก้ inputได้ได้เมื่อ sort สำเนาได้

Edge cases และ trade-offs

สมาชิกเดียวตอบตัวเอง; array ที่ไม่ถูกหมุนตอบสมาชิกแรก; pivot อาจอยู่ติดปลาย; จำนวนลบไม่เปลี่ยนหลักการ เงื่อนไขไม่ซ้ำสำคัญมาก หากมีค่าซ้ำ เช่น [2,2,2,0,1,2] กรณี nums[mid] == nums[right] บอกฝั่งไม่ได้ ต้องลดขอบทีละหนึ่งและ worst case กลับเป็น O(n) ซึ่งเป็นอีกโจทย์หนึ่ง

Recap

  • ยืนยันก่อนว่าสมาชิกไม่ซ้ำและ array มาจากลำดับเรียงที่หมุน
  • เก็บ mid ไว้เมื่อมันอาจเป็น minimum ด้วย right = mid
  • ใช้ linear เป็น oracle สำหรับ property tests ขนาดเล็ก
  • เลือก binary search เป็น default ภายใต้ constraints ของโจทย์