งูสีเขียวต้นฉบับกำลังซ้อนกล่องเป็นกอง โดยติดหินก้อนเล็กสุดไว้คู่กับทุกชั้นบนโต๊ะสว่าง

LeetCode 155: Min Stack เก็บค่าต่ำสุดอย่างไรให้ทุกคำสั่ง O(1)

พี่โชว์Engineering

โจทย์ Min Stack ให้สร้าง stack ที่รองรับ push, pop, top และ getMin โดยทุกคำสั่งต้องเวลา O(1) ครับ Stack ปกติรู้เฉพาะของบนสุด ถ้าของต่ำสุดอยู่กลางกองแล้วถูกดันของใหม่ทับ เราต้องจำประวัติให้พอกลับมาได้หลัง pop

WHY: ค่าต่ำสุดเป็น State ตามระดับความลึก

การเก็บ current minimum ตัวเดียวพอจนกว่าค่านั้นถูก pop หลังจากนั้นเราไม่รู้ค่าต่ำสุดก่อนหน้า เหมือนจดสถิติต่ำสุดของวันไว้ใบเดียวแล้วฉีกทิ้งโดยไม่มีสมุดย้อนหลัง วิธี O(1) จึงต้องเก็บข้อมูล minimum ตามจังหวะ push

วิธี 1: สแกนตอน getMin

เก็บ stack เดียว push/pop/top O(1), getMin O(n), พื้นที่ O(n) implementation ง่ายแต่ไม่ผ่าน constraints เหมาะเฉพาะระบบจริงที่แทบไม่ถาม min และยอมแลก latency ได้

วิธี 2: Dual Stack

stack แรกเก็บค่าจริง stack ที่สองเก็บ minimum เมื่อค่าที่ push น้อยกว่าหรือเท่าค่าบนสุด เวลา O(1) ทุกคำสั่ง พื้นที่เพิ่มสูงสุด O(n) เข้าใจง่าย แต่ต้องระวังค่า min ซ้ำ: ใช้ <= ตอน push และ pop ทั้งสองเมื่อเท่ากัน

วิธี 3: Pair Stack

ทุกชั้นเก็บคู่ (value, minimum_so_far) เวลา O(1), พื้นที่ O(n) ที่มี overhead คงที่ต่อ item ไม่มี synchronization ระหว่างสอง stack จึงเป็น best default ด้าน readability

วิธี 4: Encoded Difference

เก็บส่วนต่าง value - current_min และ update min ตามเครื่องหมาย ใช้หนึ่ง stack กับตัวแปร min เวลา O(1), พื้นที่ O(n) ลดโครงสร้างคู่ แต่เหตุผลซับซ้อนและภาษา fixed-width อาจ overflow เหมาะเมื่อวัดแล้ว memory overhead เป็นคอขวดจริง

import unittest


class PairMinStack:
    def __init__(self):
        self.items = []

    def push(self, value):
        minimum = value if not self.items else min(value, self.items[-1][1])
        self.items.append((value, minimum))

    def pop(self):
        return self.items.pop()[0]

    def top(self):
        return self.items[-1][0]

    def get_min(self):
        return self.items[-1][1]


class DualMinStack:
    def __init__(self):
        self.values = []
        self.minimums = []

    def push(self, value):
        self.values.append(value)
        if not self.minimums or value <= self.minimums[-1]:
            self.minimums.append(value)

    def pop(self):
        value = self.values.pop()
        if value == self.minimums[-1]:
            self.minimums.pop()
        return value

    def top(self):
        return self.values[-1]

    def get_min(self):
        return self.minimums[-1]


class EncodedMinStack:
    def __init__(self):
        self.differences = []
        self.minimum = None

    def push(self, value):
        if not self.differences:
            self.differences.append(0)
            self.minimum = value
            return
        difference = value - self.minimum
        self.differences.append(difference)
        if difference < 0:
            self.minimum = value

    def pop(self):
        difference = self.differences.pop()
        value = self.minimum if difference < 0 else self.minimum + difference
        if difference < 0:
            self.minimum = self.minimum - difference
        if not self.differences:
            self.minimum = None
        return value

    def top(self):
        difference = self.differences[-1]
        return self.minimum if difference < 0 else self.minimum + difference

    def get_min(self):
        return self.minimum


class MinStackTests(unittest.TestCase):
    def test_implementations_with_duplicate_minimum(self):
        for stack_type in (PairMinStack, DualMinStack, EncodedMinStack):
            stack = stack_type()
            for value in (3, -2, -2, 5):
                stack.push(value)
            self.assertEqual(stack.get_min(), -2)
            self.assertEqual(stack.pop(), 5)
            self.assertEqual(stack.pop(), -2)
            self.assertEqual(stack.get_min(), -2)
            self.assertEqual(stack.top(), -2)
            self.assertEqual(stack.pop(), -2)
            self.assertEqual(stack.get_min(), 3)


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

Decision Table

เงื่อนไขScan on minDual stackPair stackEncoded diff
input ใหญ่และ getMin บ่อยไม่ผ่าน O(n)ดีดีที่สุดดี
memory จำกัดข้อมูลน้อยสุดmin ซ้ำเพิ่มคู่ทุกชั้นโครงสร้างเดียว
readabilityง่ายแต่ผิด constraintชัดชัดที่สุดยาก
streaming operationsได้ได้ได้ได้
sorted inputไม่ช่วยข้อกำหนดทั่วไปไม่ต้องพึ่งไม่ต้องพึ่งไม่ต้องพึ่ง
แก้ input เดิมไม่มี input collectionไม่มีไม่มีไม่มี
ภาษา integer overflow ได้ปลอดภัยกว่าปลอดภัยกว่าปลอดภัยกว่าต้องระวังมาก

Edge Cases และ Best Default

ค่า minimum ซ้ำ, เลขติดลบ, push หลัง stack ว่าง และการ pop จนว่างต้องทดสอบ โจทย์รับประกันว่าจะไม่เรียก top/getMin ตอนว่าง แต่ production API ควรกำหนด exception หรือ Optional ชัดเจน

pair stack เป็น best default เพราะ invariant อยู่ใน item เดียว: ชั้นใดอยู่บนสุด minimum ของชั้นนั้นคือคำตอบ Dual stack ประหยัดกว่าเมื่อ minimum เปลี่ยนน้อย ส่วน encoded difference ควรใช้หลัง profile เท่านั้น

Recap

  • current minimum เดี่ยวกู้ค่าก่อนหน้าไม่ได้
  • dual stack ต้องจัดการ minimum ซ้ำ
  • pair stack ชัดและ O(1) ทุกคำสั่ง
  • encoded difference เสี่ยง overflow ในภาษาบางชนิด
  • เลือก pair stack เป็นค่าเริ่มต้นเพื่อความอ่านง่าย