งูต้นฉบับจัดลูกบอลสามสีจากกองสลับกันลงรางสามช่วงโดยมีลูกศรสองด้าน

LeetCode #75 Sort Colors: Counting เทียบ Dutch National Flag แบบเขียนทับในที่เดิม

พี่โชว์Engineering

Sort Colors ให้เรียง array ที่มีเพียง 0, 1, 2 ในที่เดิม โดยโจทย์ชวนให้ไม่ใช้ sort สำเร็จรูป ประเด็นสัมภาษณ์คือการเลือกความเรียบง่ายของ counting หรือ invariant ของ Dutch National Flag

วิธี 1: Counting สองรอบ

นับจำนวนแต่ละค่า แล้วเขียน 0, 1, 2 กลับลง array เวลา O(n) พื้นที่ O(1) โค้ดตรงและเสี่ยง bug ต่ำ แต่เดินข้อมูลสองรอบ

วิธี 2: Dutch National Flag รอบเดียว

รักษาสามโซน: ก่อน low เป็น 0, หลัง high เป็น 2 และ mid..high ยังไม่ตรวจ ถ้าเจอ 0 สลับซ้าย ถ้าเจอ 2 สลับขวาโดยยังไม่ขยับ mid เพราะค่าที่สลับกลับมายังไม่ถูกตรวจ

วิธี 3: sorted copy

nums[:] = sorted(nums) อ่านง่ายที่สุด แต่ใช้ O(n log n) และพื้นที่ตาม implementation จึงเหมาะเป็น oracle ในเทสต์มากกว่าเฉลยตามข้อกำหนด

from __future__ import annotations

import unittest


def sort_counting(nums: list[int]) -> None:
    counts = [0, 0, 0]
    for value in nums:
        counts[value] += 1
    index = 0
    for value, count in enumerate(counts):
        nums[index:index + count] = [value] * count
        index += count


def sort_dutch_flag(nums: list[int]) -> None:
    low = mid = 0
    high = len(nums) - 1
    while mid <= high:
        if nums[mid] == 0:
            nums[low], nums[mid] = nums[mid], nums[low]
            low += 1
            mid += 1
        elif nums[mid] == 2:
            nums[mid], nums[high] = nums[high], nums[mid]
            high -= 1
        else:
            mid += 1


def sort_builtin(nums: list[int]) -> None:
    nums[:] = sorted(nums)


class SortColorsTests(unittest.TestCase):
    def test_all_methods_and_edges(self):
        for solve in (sort_counting, sort_dutch_flag, sort_builtin):
            for case in ([2, 0, 2, 1, 1, 0], [], [1], [2, 2, 0]):
                values = list(case)
                solve(values)
                self.assertEqual(values, sorted(case))


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

Decision table

เงื่อนไขCountingDutch flagBuilt-in sort
เวลาO(n)O(n)O(n log n)
extra memoryO(1)O(1)สูงกว่า
จำนวนรอบ21ไม่กำหนด
อ่านง่ายดีที่สุดปานกลางสั้นที่สุด
mutationมีมีมีผ่าน slice
streamingนับได้ แต่ต้องเขียนปลายทางรอบสองไม่เหมาะไม่เหมาะ

Counting เป็น default ที่อธิบายและดูแลง่าย ถ้า requirement ระบุ one pass ค่อยใช้ Dutch flag และเขียน invariant ไว้ข้าง loop

Recap

  • ยืนยันว่า input มีเพียง 0, 1, 2
  • ตอนสลับเลข 2 อย่าเลื่อน mid ทันที
  • เทสต์ array ว่าง ค่าเดียว และทุกค่าเหมือนกัน
  • บอกผู้เรียกชัดว่าฟังก์ชัน mutate input