這裏主要介紹幾種棧本身的使用方法,不包括一些作爲容器其他用法,比如進行DFS,用來保存中間結點等等;也不包括遞歸棧,雖然有時候也可以把使用遞歸方法看做使用了棧,但摳字眼沒什麼意思。
此外,本文不會詳細介紹語法,請讀者擔待。
用數組實現棧
這個並不屬於正文,而且是很簡單的內容,但是我覺得還是有必要在這裏稍微提一下;看似很簡單的東西也會有坑在裏面。比如三合一問題,用一個數組實現三個有容量上限的棧:
class TripleInOne {
private data: number[] = [];
private left: number[] = [];
private right: number[] = [];
private ptr: number[] = [];
constructor(stackSize: number) {
this.left.push(0, stackSize, stackSize * 2);
this.right.push(stackSize, stackSize * 2, stackSize * 3);
this.ptr.push(...this.left);
}
push(stackNum: number, value: number): void {
if (this.ptr[stackNum] < this.right[stackNum]) {
this.data[this.ptr[stackNum]++] = value;
}
}
pop(stackNum: number): number {
return this.isEmpty(stackNum) ? -1 : this.data[--this.ptr[stackNum]];
}
peek(stackNum: number): number {
return this.isEmpty(stackNum) ? -1 : this.data[this.ptr[stackNum] - 1];
}
isEmpty(stackNum: number): boolean {
return this.ptr[stackNum] <= this.left[stackNum];
}
}
解決這個問題並不困難,主要麻煩的是指針的移動(尤其是在C++這種語言不能使用STL的時候)。比如這種實現,每次添加後指針都會向後移動,所以在刪除時要先向前移動,再進行刪除,同時peek操作也要對應- 1
,才能獲取到正確的棧頂值。
最值棧
比較著名的就是最小棧了,能夠在O(1)的時間內獲得棧內的最小值。這裏給出一種可能的實現。
class MinStack {
private data: number[] = [];
private min = [Number.MAX_VALUE];
push(x: number): void {
this.data.push(x);
this.min.push(Math.min(this.getMin(), x));
}
pop(): void {
this.data.pop();
this.min.pop();
}
top(): number {
return this.data[this.data.length - 1];
}
getMin(): number {
return this.min[this.min.length - 1];
}
}
最大棧同理,換成max就是了。
至於你問我最小棧的應用是什麼,很抱歉我也搞不清楚……但是這個題頻繁出現,在幾本經典的面試書籍上都出現過,所以還是有必要搞清楚的。
單調棧
這個也是一個很有用的數據結構,可以用於尋找一些具有單調性質的內容。比如:
給定一個整數數組,你需要尋找一個連續的子數組,如果對這個子數組進行升序排序,那麼整個數組都會變爲升序排序。
這個其實就適合用單調棧處理,找到數組中現在已經有序的部分,然後一減就是亂序的部分。
以遞增棧爲例,這個棧內部的元素是單調遞增的。爲了確保棧內元素是遞增的,每次遇到比棧頂元素小的新元素,就應該不斷出棧直到新元素不再小於棧頂元素;這個道理還是很明顯的。
const length = nums.length;
const stack = [];
for (let i = 0; i < length; ++i) {
while (stack.length && stack[stack.length - 1] > nums[i]) stack.pop()!;
stack.push(i);
}
同時,單調棧有兩種形式。還是以遞增棧爲例,遞增棧有兩種:
- “物理”遞增:棧內存儲的是值,值本身遞增;
- “邏輯”遞增:棧內存儲的是下標,下標對應的值遞增。
存儲下標可以用於解決一些距離、長度的問題。比如之前提到的那個問題,存下標就比較合適。這裏也舉個例子:
const length = nums.length;
const stack = [];
for (let i = 0; i < length; ++i) {
while (stack.length && nums[stack[stack.length - 1]] > nums[i]) stack.pop()!;
stack.push(i);
}
最大頻率棧
這個其實是一個很特殊的棧:入棧和普通棧一樣,但退棧時是移除棧中出現最頻繁的元素;如果最頻繁的元素不止一個,則移除並返回最接近棧頂的元素。
不知道你們有沒有想到OS的多級反饋隊列調度算法,我覺得這個和那個幾乎是一樣的思路。引用一下百度百科裏的圖:
多級反饋隊列調度算法的具體內容不仔細說了,大概就是每次從優先級最高的隊列裏選擇進程執行,如果沒有執行完就降低優先級,放到下一級隊列裏。
在這裏,頻率就相當於是優先級,出現幾次就在第幾層,而且頻率越高優先級越高。所以,我們只需要記錄一下當前“待處理進程”的優先級,然後維護這麼一個多級的棧即可:
class FreqStack {
private freqs = new Map<number, number>();
private stacks = new Map<number, number[]>();
private maxFreq = 0;
push(x: number): void {
const beforeFreq = this.freqs.get(x),
currentFreq = beforeFreq ? beforeFreq + 1 : 1;
// update freq
this.freqs.set(x, currentFreq);
// push to stack
const stack = this.stacks.get(currentFreq);
if (stack) stack.push(x);
else this.stacks.set(currentFreq, [x]);
// update max freq
this.maxFreq = Math.max(this.maxFreq, currentFreq);
}
pop(): number {
const res = this.stacks.get(this.maxFreq)!.pop()!;
this.freqs.set(res, this.freqs.get(res)! - 1);
if (!this.stacks.get(this.maxFreq)!.length) --this.maxFreq;
return res;
}
}
這個代碼還是用cpp寫起來舒服。js甚至不如Java,Java 8之後好歹還有個給map初始值的方法,js只能硬寫。爲了能清晰地說明,我覺得還是放個cpp的版本比較好:
class FreqStack
{
private:
unordered_map<int, int> freq;
unordered_map<int, stack<int>> stacks;
int maxfreq = 0;
public:
void push(int x)
{
++freq[x];
maxfreq = max(maxfreq, freq[x]);
stacks[freq[x]].push(x);
}
int pop()
{
int x = stacks[maxfreq].top();
stacks[maxfreq].pop();
--freq[x];
if (stacks[maxfreq].empty()) --maxfreq;
return x;
}
};
用棧實現隊列
隊列是先進先出(FIFO)的數據結構。
當然了,對於js來說,這個問題毫無意義,因爲js的數組本身是個雙端隊列,根本無所謂。
class MyQueue {
private data: number[] = [];
push(x: number): void {
this.data.push(x);
}
pop(): number {
return this.data.shift()!;
}
peek(): number {
return this.data[0];
}
empty(): boolean {
return this.data.length === 0;
}
}
單棧
用單棧實現隊列,其實沒什麼好說的,就是一個模擬的過程,無非是添加O(1)刪除O(n),添加的時候放到棧頂,刪除的時候把棧底元素退出去,然後再把其他元素壓回來;或者添加O(n)刪除O(1),添加的時候放到棧底,刪除的時候把棧頂元素退棧。這兩種寫法取決於添加操作多,還是刪除操作多。這裏提供一種添加O(n)刪除O(1)的寫法:
class MyQueue {
private stack: number[] = [];
push(x: number): void {
const tmp: number[] = [];
while (this.stack.length) {
tmp.push(this.stack.pop()!);
}
this.stack.push(x);
while (tmp.length) {
this.stack.push(tmp.pop()!);
}
}
pop(): number {
return this.stack.pop()!;
}
peek(): number {
return this.stack[this.stack.length - 1];
}
empty(): boolean {
return this.stack.length === 0;
}
}
雙棧
如果要用棧實現隊列,從我個人角度,我覺得單棧比雙棧的思路要自然。不過,雙棧的寫法看起來確實更清晰一些。
思路比較簡單,準備兩個棧,一個用於添加(appStack),一個用於刪除(delStack)。添加新元素的時候直接向appStack中添加,刪除的時候從delStack中刪除,如果delStack中沒有元素,就把appStack中現有的元素添加到delStack中,然後再刪除,就這麼簡單。代碼如下:
class CQueue {
private appStack: number[] = [];
private delStack: number[] = [];
appendTail(value: number): void {
this.appStack.push(value);
}
deleteHead(): number {
if (!this.delStack.length) {
while (this.delStack.length) {
this.delStack.push(this.delStack.pop()!);
}
}
return this.delStack.length ? this.delStack.pop()! : -1;
}
}
裏面唯一可能的問題就是,從appStack添加到delStack這一步,是怎麼保證有序的。我們設想一下壓棧的過程,appStack中最先入棧的元素位於棧底,我們再按順序把appStack中的元素壓入delStack,此時相當於掉了個個,最先入棧的元素重新被壓到棧頂了,會在刪除時最先出棧,就實現了先進先出。