JS版資料結構第一篇(棧)

走音發表於2019-04-16

前端入行門檻低,人員參差不齊

前端就是寫頁面的

前端的人都不懂資料結構和演算法


背景

相信大家在社群經常會聽到類似以上的話

由於前端上手比較快,而且平時開發時大部分寫的都是業務邏輯以及互動,常常導致我們被一些後端人員'鄙視',這無疑是對我們前端開發人員是不公平的,前端技術更新迭代很快,而且知識點瑣碎,想要學好前端是需要一定的持續性學習能力以及創造性和好奇心的,學好前端並不是一件很容易的事情。

我們都知道 程式設計=演算法+資料結構

無論是前端還是後端,作為開發人員對這些基礎知識的掌握程度決定了你以後的技術道路發展的上限,演算法和資料結構對程式設計師的重要程度不言而喻。

拋開一些偏見,我們不能否認的是:

  1. 有很多非科班出身的前端同學對資料結構的理解並不是很深,甚至一些資料結構型別的定義都不清楚。
  2. 網上利用JS實現的資料結構的資料有限。

針對這種實際情況,我將分六篇部落格介紹最常見的幾種資料結構,並結合LeetCode上面的經典原題利用js方法進行實際題目的求解,包括:

  • 佇列
  • 連結串列
  • 矩陣
  • 二叉樹

如果你也是一名想夯實一下資料結構基礎的前端開發人員,在網上又找不到合適的資源的話,那麼小弟的部落格一定會對你有所幫助。

棧(stack)

棧的定義

作為資料結構中最簡單的一種想必大家對棧都有所瞭解,我們先來看一下百度百科上'棧'的定義

JS版資料結構第一篇(棧)

其實說白了就是我們平時講的'先進後出',只能從一端(棧頂)新增或刪除資料。定義還是很抽象,我還是用一個例子比喻一下我理解中的‘棧’。

棧的理解

JS版資料結構第一篇(棧)

我這裡用抖音上最近比較火的彩虹酒來舉例:

如果你去酒吧點了這杯彩虹酒,酒吧小哥哥會先拿來一個乾淨的空杯子(此時是空棧的狀態)

杯口稱為棧頂,杯底稱為棧底,給你製作這杯酒時他會

  1. 先倒入紅色的'烈焰紅脣',
  2. 然後是黃色的‘杏仁’雞尾酒,
  3. 最後倒入'藍色夏威夷',
這就是一個入棧的過程,

而當你拿到這杯酒時,

  1. 將先會品嚐到藍色妖姬帶來的清爽,
  2. 然後是杏仁的清香,
  3. 最後陶醉在紅色海洋裡,
這也就是一個出棧的過程。

注意:調酒師給你製作酒時他只會從杯子口將酒倒入杯中,你也不會在杯底打一個洞然後將酒倒入你的嘴裡。

只能從杯口(棧頂)進只能從杯口(棧頂)出,這也就是上面定義中指的'受限'。

js實現一個簡單的棧

其實對於js來講實現棧再簡單不過了,我們只需要定義一個陣列,結合Array.prototype.push方法以及Array.prototype.pop方法就實現了。

以下六行程式碼對應圖下的六個過程

var arr = [2,7,1]
arr.push(8) 
arr.push(2)
arr.pop()
arr.pop()
arr.pop()複製程式碼


JS版資料結構第一篇(棧)

怎麼樣,是不是很簡單?

接下來我們用LeetCode上的例題看一下利用棧的資料結構可以解決怎樣的問題。

例題一:棒球比賽

我們先來看一下題目  原題地址

你現在是棒球比賽記錄員。
給定一個字串列表,每個字串可以是以下四種型別之一:
1.整數(一輪的得分):直接表示您在本輪中獲得的積分數。
2. "+"(一輪的得分):表示本輪獲得的得分是前兩輪有效 回合得分的總和。
3. "D"(一輪的得分):表示本輪獲得的得分是前一輪有效 回合得分的兩倍。
4. "C"(一個操作,這不是一個回合的分數):表示您獲得的最後一個有效 回合的分數是無效的,應該被移除。

每一輪的操作都是永久性的,可能會對前一輪和後一輪產生影響。
你需要返回你在所有回合中得分的總和。

示例:

輸入: ["5","2","C","D","+"]
輸出: 30
解釋: 
第1輪:你可以得到5分。總和是:5。
第2輪:你可以得到2分。總和是:7。
操作1:第2輪的資料無效。總和是:5。
第3輪:你可以得到10分(第2輪的資料已被刪除)。總數是:15。
第4輪:你可以得到5 + 10 = 15分。總數是:30。複製程式碼

分析一下這道題目:

先傳入一個陣列,然後根據陣列中每一項字串的不同型別計算出真實得分,最後將結果求和。

我們可以根據‘棧’的資料結構分以下幾個步驟進行求解:

  1. 先新建一個空陣列A(即空棧) 儲存真實的得分情況
  2. 然後遍歷傳入的陣列B並根據它每一項的不同型別計算出每一輪的得分情況
  • 如果該項是前三種型別(‘正數’,'+',‘D’) 則按要求計算該輪得分並插入到空陣列A中(即入棧)
  • 如果該項是第四種型別('C'),則將陣列A最後一項丟擲(即出棧)
   3. 對陣列A進行遍歷求和

程式碼如下:

export default (arr) => {  
    // 新建空棧,儲存處理後的結果  
        let result = []  
    // 上一輪的資料  
        let pre1  
    // 上上輪的資料  
        let pre2  
    // 對傳進來的陣列進行遍歷,遍歷的目的是處理得分  
    arr.forEach(item => {    
        switch (item) {      
            case 'C':        
                if (result.length) {          
                    result.pop()        
                }        
                break      
            case 'D':        
                pre1 = result.pop()        
                result.push(pre1, pre1 * 2)       
                break      
            case '+':       
                pre1 = result.pop()       
                pre2 = result.pop()      
                result.push(pre2, pre1, pre2 + pre1)      
                break     
            default:
                // *1是為了將item轉為number型別       
                result.push(item * 1)    }  })
     //利用reduce進行求和 
     return result.reduce((prev, cur) =>  prev + cur )}複製程式碼

例題二:最大矩形

原題地址

給定一個僅包含 0 和 1 的二維二進位制矩陣,找出只包含 1 的最大矩形,並返回其面積。

示例:

輸入:
[
  ["1","0","1","0","0"],
  ["1","0","1","1","1"],
  ["1","1","1","1","1"],
  ["1","0","0","1","0"]
]
輸出: 6複製程式碼

實現思路:

將二維陣列arr1的每一項2個及以上連續1的索引範圍轉換成新的二維陣列arr2,如

[  
    ['1', '1', '0', '1', '1'],
    ['1', '1', '1', '1', '1'],
    ['1', '1', '0', '1', '1']
]複製程式碼

可以轉換為:

[ 
    [[0,1],[3,4]],
    [[0,4]],
    [[0,1],[3,4]]
]複製程式碼

這樣的話我們先寫一個函式,功能是查詢二維陣列裡面每相鄰兩項的交集,接受兩個引數,分別是需要求交集的陣列和已經遍歷的行數。

函式內部這樣實現的:

函式先將接收的陣列的最後兩項推出(出棧兩次),然後將兩項遍歷取交集,

  • 如果有交集則將取到的交集入棧,再次遞迴,傳入此時的陣列和n
  • 沒有交集則將陣列刪除最後一項(出棧),然後將新的陣列傳入函式
每次取到交集時要根據交集陣列的長度和已便利的行數計算此時矩形面積,最後將最大的矩形面積返回。

更多的註釋寫在程式碼裡,我把原始碼發出來供大家參考

export default arr => {
  // 用來儲存處理過後的二維陣列  
  let changeArr = []
  // 將傳入的二維陣列根據每一項的連續1的索引位置遍歷  
  arr.forEach((item, i) => {
    //每一項的索引    
    let itemArr = []    
    let str = item.join('')    
    let reg = /1{2,}/g
    // 連續1的匹配結果    
    let execRes = reg.exec(str)
    //如果不為null就繼續匹配    
    while (execRes) {
      // 將第i項匹配到的第j組1的起止位置推入      
      itemArr.push([execRes.index, execRes.index + execRes[0].length - 1])      
      execRes = reg.exec(str)    
    }
    // 將第i項的匹配結果推入    
    changeArr.push(itemArr)  })
    // 用來儲存面積  
    let area = []  
    // 取交集函式  
    let find = (arr, n = 1) => {    
        // 深拷貝arr陣列 防止由於引用導致每次執行函式被改變    
        let copyArr = [].concat(arr)    
        // 陣列最後一項    
        let last = copyArr.pop()    
        // 陣列倒數第二項    
        let next = copyArr.pop()
        // 最後一項和倒數第二項的每一項   
        let lastItem, nextItem
        // 取到的交集陣列    
        let recuArr = []
        // 作為每次取到交集的臨時變數    
        let item = []
        // 已遍歷的行數    
        n++
        // 將每相鄰兩項的每個區間分別取交集    
        for (let i = 0; i < last.length; i++) {      
            lastItem = last[i]      
            for (let j = 0; j < next.length; j++) {        
            nextItem = next[j]        
            // 開始取交集       
            // 若有交集        
            if (!(Math.max(...lastItem) <= Math.min(...nextItem) ||                   
                    Math.max(...nextItem) <= Math.min(...lastItem))) {           
                item = [Math.max(lastItem[0], nextItem[0]), Math.min(lastItem[1], nextItem[1])]          
                recuArr.push(item)
            // 求此時交集的面積並推入area           
            area.push((item[1] - item[0] + 1) * n)        }      }    }    
            // 若遍歷完了所有情況都沒有交集,則返回false
            if (recuArr.length === 0) {
              return false
              } else {
              // 有交集,繼續遞迴遍歷
              if (copyArr.length > 0) {
                copyArr.push(recuArr)
                find(copyArr, n)
              }
            }
      }
      //將陣列一直遞減,每一行都作為第一行找一次交集
      while (changeArr.length > 1) {
          find(changeArr)
          changeArr.pop()
      }
      //最後將儲存面積的矩形陣列的最大值返回
      return area.length === 0 ? 0 : Math.max(...area)
    }複製程式碼

參考

  1. 百度百科
  2. 今日頭條視訊架構前端負責人銀國徽老師的《js資料結構與演算法》
  3. leetCode

總結

作為一種比較簡單的資料結構,棧在js中很常見,也比較容易理解,我們可以利用js原生的陣列API就可以實現。

下一篇我將介紹佇列的使用方法。



相關文章