JavaScript資料結構之棧

snowLu發表於2018-12-26

總結一下這兩天學習js資料結構中的棧

不學不知道,一學嚇一跳。可以利用資料結構的思想來實現一些演算法,能把原本O(n^2)的時間複雜度降低到O(1),雖然只是對一些陣列的api進行封裝

為什麼學習資料結構

1.語言是相通的嗎

經常聽很多前輩說,程式語言是相通的,掌握了一門,其他語言就就很容易掌握,但是個人認為每門語言都有自己的優缺點,都有自己能勝任的地方,也都有自己無能為力的地方。 比如說讓我們前端工程師踏入後端領域的node.js,也只是在很多公司作為中間層來使用,並不能像java,c那樣來真正的代替後端。 比如說機器語言python,這樣近乎萬能的語言,在面對高效能運算的時候,利用的很多python庫,底層也都是c語言實現的。 所以個人認為真正相通的不是語言,而是資料結構和演算法。 資料結構和演算法是脫離程式語言而存在的,不同的語言有不同的實現,但內在的邏輯不會有變化,所體現的程式設計思想不會有變化。

2.一段親身經歷

在之前的面試中,去了一家是線上課程的公司,當時筆試和前兩面都比較順利,到了終面部門負責人的時候,懵圈了。那位大佬是計算機出身的前百度高階工程師,特別注重資料結構和演算法。寒暄了幾句後開始進入了主題,(噩夢的開始)看了我寫的筆試題後,問我為什麼這個快排這樣寫?我當時寫的阮一峰老師的版本, 我沒說話,然後又問我,你不知道這樣寫不僅時間複雜度會加大,連空間複雜度都會消耗嗎?我懵圈的搖了搖頭,然後又問我,你知道堆排序嗎?我又懵圈的搖了搖頭,然後又問我,你知道時間複雜度嗎?我又無奈的搖了搖頭;然後大佬放棄了,不再問了,開始了對我的評價,你連這最基本的資料結構都不知道,怎麼能知道你的程式碼是好是壞呢,如果遇到bug別人半個小時能解決的,你說不定得用兩個小時。。。深刻教育了一番出去和hr談話了,最後的結果雖然是拿到offer了,但是級別定的低,money也給的少。 我一直以為前端和資料結構和演算法無關,能實現業務功能就行,但是經過這次面試後,我打破了之前的觀點,就算寫業務,也有寫的好也有寫的一般的,之前的巢狀迴圈,每一層都把時間複雜度提高了一個檔次,所以決定重頭學起資料結構和演算法。

3.學習資料結構的目標

資料結構的精髓在於總結提煉了許多儲存管理和使用資料的模式,這些模式的背後是最精華的程式設計思想,這些思想的領悟需要時間,不要想當然的認為學會了幾種資料結構就可以在工作中大顯身手,但學會了資料結構,對自身能力的提升是不言而喻的。


接下來開始主題吧

資料結構之---棧

1.棧的定義

棧是一種特殊的線性表,僅能線上性表的一端操作,棧頂允許操作,棧底不允許操作。棧的特性:先進後出(後進先出)。

下圖展示了棧的工作原理

JavaScript資料結構之棧

棧某種意義上講,它像是一個開口的盒子,先放進去的東西總是會被後放進去的東西壓在下面,那麼如果想拿出被壓住的東西,必須要先取出頂部的東西,也就是後放進去的東西。

就像我們日常生活中的羽毛球桶

JavaScript資料結構之棧
每次取羽毛球時,都只能從頂部取,最底下的羽毛球,你是取不到的,用完了羽毛球后,也只能從頂部放回去。(當然,特殊情況不考慮)

2.棧的實現

從資料儲存的角度看,實現棧有兩種方式,一種是以陣列做基礎,一種是以連結串列做基礎,陣列是最簡單的實現方式,本文以基礎的陣列來實現棧。

棧的基本操作包括建立棧、銷燬棧、出棧、入棧、獲取棧頂元素、獲取棧的大小、清空棧。

我們定義以下幾個棧的方法:

  • push 新增一個元素到棧頂(向桶裡放入一個羽毛球)
  • pop 彈出棧頂元素(從桶裡頂部拿出一個羽毛球)
  • top 返回棧頂元素(看一眼桶裡最頂端的羽毛球,但是不拿出來)
  • isEmpty 判斷棧是否為空(看看羽毛球是不是都用完了)
  • size 返回棧裡元素的個數(數一下桶裡還有多少羽毛球)
  • clear 清空棧(把桶裡的羽毛球都倒出來扔掉)

然後我們利用es6的class的實現以上的方法 新建一個stack.js檔案

class Stack {
  constructor() {
    this.items = []; // 使用陣列儲存資料
  }
  push(item) {
    this.items.push(item); // 往棧裡壓入一個元素
  }
  pop() {
    return this.items.pop(); // 把棧頂的元素移除
  }
  top() {
    return this.items[this.items.length - 1]; // 返回棧頂的元素
  }
  isEmpty() {
    return this.items.length === 0; //返回棧是否為空
  }
  size() {
    return this.items.length; // 返回棧的大小
  }
  clear() {
    this.items = []; // 清空棧
  }
}

複製程式碼

看完上面的程式碼,是不是覺得很驚訝,這裡實現的棧,竟然就只是對陣列做了一層封裝而已!

只是做了一層封裝麼?

  • 給你一個陣列,你可以通過索引操作任意一個元素,但是給你一個棧,你能操作任意元素麼?棧提供的方法只允許你操作棧頂的元素,也就是陣列的最後一個元素,這種限制其實提供給我們一種思考問題的方式,這個方式也就是棧的特性,後進先出。
  • 既然棧的底層實現其實就是陣列,棧能做的事情,陣列一樣可以做啊,為什麼弄出一個棧來,是不是多此一舉?封裝是為了更好的利用,站在棧的肩膀上思考問題顯然要比站在陣列的肩膀上思考問題更方便,後面的練習題你將有所體會。
3.棧的應用

3.1.1 判斷括號是否匹配 說說我之前遇到的面試題,給一段字串,判斷裡面的括號是否是成對出現 比如說

()ss()ss(sss(ss)(ss)ss) 合法

()ss()ss(sss(ss)(ss)ss)) 不合法

3.1.2 思路分析 括號有巢狀關係,也有並列關係,如果我們用陣列或者物件的方法也能解決,今天我們試著用棧來解決這個問題。

  • 遍歷字串
  • 如果是左括號,就壓入棧中
  • 如果是右括號,判斷棧是否為空,如果不為空,則把棧頂元素移除(也就是在棧中存放的左括號),這對括號就抵消了;如果不為空,就說明缺少左括號,返回false
  • 迴圈結束後,看棧的大小是否為0,如果不為0,就說明沒有成對出現,為0,就說明全部抵消了。

3.1.3 用棧來分析是不是覺得很簡單呢,下面看程式碼實現

{
       function isDouuble(str) {
          const stack = new Stack();
          const len = str.length;
          for (let i = 0; i < len; i++) {
            const item = str[i];
            if (str[i] === "(") {
              stack.push(item); // 入棧
            } else if (item === ")") {
              if (stack.isEmpty()) {
                return false;
              } else {
                stack.pop(); // 出棧
              }
            }
          }
          return stack.size() === 0;
        }
        console.log(isDouuble("()ss()ss(sss(ss)(ss)ss)")); // true
        console.log(isDouuble("()ss()ss(sss(ss)(ss)ss)(")); // false
        console.log(isDouuble("()ss()ss(sss(ss)(ss)ss))")); // false
        console.log(isDouuble("()ss()ss(sss(ss)(ss)ss))(")); // false
      }
複製程式碼

3.2.1 實現一個min方法的棧

實現一個棧,除了常見的push,pop方法以外,提供一個min方法,返回棧裡最小的元素,且時間複雜度為o(1)

3.2.2 思路分析 可以利用兩個棧來實現,一個棧用來儲存資料,一個棧用來儲存棧裡最小的資料; 利用程式設計中分而治之的思想,就是分開想分開處理

  • 定義兩個棧,dataStack 和 minStack;
  • 對於dataStack棧來說,正常的psuh,pop實現就好;
  • 對於minStatck棧來說,它是要儲存棧裡最小的值,所以當minStack為空的時候,那麼push進來的資料就是最小的;如果不為空,此時minStack棧頂的元素就是最小的,如果push進來的元素比棧頂的元素還小,直接push進來就行,這樣minStack棧的棧頂始終都是棧裡的最小值。

3.2.3 程式碼實現 (時間複雜度為O(1))

 {
        class MinStack {
          constructor() {
            this.dataStack = new Stack(); // 普通的棧
            this.minStack = new Stack(); // 儲存最小值的棧
          }
          // push 和 pop 兩個棧都要操作,保持大小統一
          push(item) {
            this.dataStack.push(item); // 常規操作
            if (this.minStack.isEmpty() || item < this.minStack.top()) {
              this.minStack.push(item); // 保證minStack棧頂是最小的值
            } else {
              this.minStack.push(this.minStack.top()); // 保持兩個棧的大小一樣
            }
          }
          pop() {
            this.minStack.pop();
            return this.dataStack.pop(); // 返回真實的數字
          }
          min() {
            return this.minStack.top(); // 返回最小的數字
          }
        }

        const minstack = new MinStack();
        minstack.push(3);
        minstack.push(2);
        minstack.push(6);
        minstack.push(8);
        console.log(minstack.min()); // 2
        console.log(minstack.pop()); // 8
        minstack.push(1);
        console.log(minstack.min()); // 1
      }
複製程式碼
4.棧的小結

棧的底層是不是使用了陣列這不重要,重要的是棧的這種後進先出的特性,重要的是我們只能操作棧頂元素的的限制,一定要忽略掉棧的底層如何實現,而只去關心棧的特性。

相關文章