在JavaScript / TypeScript中使用棧——幾種棧的使用方法

這裏主要介紹幾種棧本身的使用方法,不包括一些作爲容器其他用法,比如進行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,此時相當於掉了個個,最先入棧的元素重新被壓到棧頂了,會在刪除時最先出棧,就實現了先進先出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章