[M] 155. 最小堆疊 (Min Stack)
題目
- LeetCode 連結
- 主題:Stack
- 難度:Medium
題目描述
設計一個支援以下操作且具備常數時間複雜度的「最小堆疊」(MinStack):
push(int val)
:將元素val
推入堆疊。pop()
:移除堆疊頂部的元素。top()
:獲取堆疊頂部的元素。getMin()
:檢索堆疊中的最小元素。
實作 MinStack
類別:
MinStack()
:初始化堆疊物件。void push(int val)
:將元素val
推入堆疊。void pop()
:移除堆疊頂部的元素。int top()
:獲取堆疊頂部的元素。int getMin()
:檢索堆疊中的最小元素。
要求每個函數操作都必須具備 O(1) 的時間複雜度。
範例
輸入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
輸出:
[null,null,null,null,-3,null,0,-2]
解釋:
MinStack minStack = new MinStack();
minStack.push(-2); // 將 -2 推入堆疊
minStack.push(0); // 將 0 推入堆疊
minStack.push(-3); // 將 -3 推入堆疊
minStack.getMin(); // 返回 -3,因為它是堆疊中的最小值
minStack.pop(); // 移除堆疊頂部的 -3
minStack.top(); // 返回 0,因為它現在是堆疊頂部的元素
minStack.getMin(); // 返回 -2,因為它現在是堆疊中的最小值
限制條件
-2^31 <= val <= 2^31 - 1
pop
、top
和getMin
操作都會在非空堆疊上執行。- 最多會進行 3 * 10⁴ 次的
push
、pop
、top
和getMin
操作。
問題釐清
- top 與 pop 的差別在於只單純得到 stack 頂端的值,而不移除?
- 是否需考慮 stack 中沒值時的狀況?(題目限制中不需考慮)
- 題目要求每個操作都要能是
O(1)
,這對 push、pop、top 不成問題,但如果要記錄當前最小值的話且要是O(1)
,是否代表可以另外宣告一個變數紀錄?
提出測試案例
- 測試題目範例
- 設計在 push 時最小值有替換,並在 pop 時有 pop 到最小值的狀況
提出思路
如果用另一個變數紀錄最小值可行的話,實作上會像以下的方式,直接在程式中以註解表示:
class MinStack {
constructor() {
// declare a min value
// declare a stack
}
push(val: number): void {
// 檢查是否需要更新當前最小值
// 若需要的話,將舊的最小值先推入 stack,再替換新的最小值
// 將新值推入 stack
}
pop(): void {
// 檢查當前 pop 出來的值是否與最小值相等
// 若是,則需要再 pop 前一個次小值到 min 中,才能維持最小值正確性
}
top(): number {
// return the last value from stack
}
getMin(): number {
// return min value
}
}
![min stack explanation](/_next/image?url=%2Fblog%2Fleetcode%2Fmin_stack.png&w=1200&q=75)
推入順序為 2, 1, 0, 4, 5, -1
這個做法一開始想到 pop 時比較有點卡住的是,如果今天「把最小值 pop 出去後,要怎麼得到次小值」,而解法上後來實際畫圖後發現其實若不管當前 stack 長度限制的話,在進行 push 時也需要把次小值先推入 stack 中與新的最小值相鄰,這樣當 pop 出的值是最小時,再次 pop 就能得到次小值。
實作
class MinStack {
private min: number = Number.MAX_SAFE_INTEGER;
private stack: number[] = [];
push(val: number): void {
// 檢查是否需要更新當前最小值
if (val <= this.min) {
// 若需要的話,將舊的最小值先推入 stack,再替換新的最小值
this.stack.push(this.min);
this.min = val;
}
// 將新值推入 stack
this.stack.push(val);
}
pop(): void {
// 檢查當前 pop 出來的值是否與最小值相等
if (this.stack.pop() === this.min) {
// 若是,則需要再 pop 前一個次小值到 min 中,才能維持最小值正確性
this.min = this.stack.pop() as number;
}
}
top(): number {
// return the last value from stack
return this.stack[this.stack.length - 1];
}
getMin(): number {
// return min value
return this.min;
}
}
撰寫測試
除了題目範例外,這裡另外做一個比較複雜的,確保在有 pop 到最小值時,執行第二次 getMin 與 top 能得到預期的值:
describe('MinStack', () => {
it('should return correct value with complex scenario', () => {
const minStack = new MinStack();
minStack.push(2);
minStack.push(1);
minStack.push(0);
minStack.push(4);
minStack.push(5);
minStack.push(-1);
expect(minStack.getMin()).toBe(-1);
minStack.pop();
expect(minStack.getMin()).toBe(0);
expect(minStack.top()).toBe(5);
});
});
複雜度
因為沒有在 getMin 時另外去搜尋最小值所以每個操作的時間複雜度都是 O(1)
,空間複雜度大約是 O(n)
。
另外看到有另一個類似但稍微不同的解法是宣告另一個最小堆疊,用來記錄每次比較後的最小值,而因為也是堆疊結構,就算當前的最小值在主堆疊上被 pop 出去,也能從最小堆疊中取出次小值,但在空間複雜度相較下會多一點。
程式碼
詳細程式碼可以參考此 GitHub 連結。