用JavaScript實現棧與隊列

翻譯:瘋狂的技術宅javascript

原文code.tutsplus.com/articles/da…java

說明:本專欄文章首發於公衆號:jingchengyideng 。程序員

棧和隊列是web開發中最經常使用的兩種數據結構。絕大多數用戶,甚至包括web開發人員,都不知道這個驚人的事實。若是你是一個程序員,那麼請聽我講兩個啓發性的例子:使用堆棧來組織數據,來實現文本編輯器的「撤消」操做;使用隊列處理數據,實現web瀏覽器的事件循環處理事件(單擊click、懸停hoover等)。web

等等,先想象一下咱們做爲用戶和程序員,天天使用棧和隊列的次數,這太驚人了吧!因爲它們在設計上有廣泛性和類似性,我決定從這裏開始爲你們介紹數據結構。瀏覽器

在計算機科學中,棧是一種線性數據結構。若是你理解起來有困難,就像最初很是困惑的我同樣,不妨這樣認爲:一個棧能夠對數據按照順序進行組織和管理。bash

要理解這種順序,咱們能夠把棧這種結構想象爲自助餐廳的一堆盤子,當一個盤子被疊加到一堆盤子上時,原有的盤子保留了它們原來的順序;同時,當一個新盤子被添加時,它會朝棧的底部方向堆積。每當咱們添加一個新盤子時,被稱做入棧,這個新盤子處於棧的頂部,也被稱做棧頂。數據結構

這個添加盤子的過程會保留每一個盤子被添加到棧中的順序,每次從棧中取出一個盤子時也是同樣的。我可能用了太多的篇幅來描述自助餐廳中的盤子是怎樣被添加和刪除的過程。編輯器

爲了是你們理解棧更多的技術細節,讓咱們回顧一下前面關於文本編輯器的「撤消」操做。每次將文本添加到文本編輯器事,該文本被壓入棧中。其中第一次添加的文本表明棧的底部(棧底);最後一次的修改表示棧的頂部(棧頂)。若是用戶但願撤銷最後一次修改,則刪除處於棧的頂部的那段文本,這個過程能夠不斷重複,一直到棧中沒有更多內容,這時咱們會獲得一個空白文件。ide

棧的操做

如今咱們對棧的模型有了基本概念,下一步就要定義棧的兩個操做:函數

  • push(data) 添加數據
  • pop() 刪除最後添加的數據

棧的實現

如今讓咱們開始爲棧編寫代碼吧!

棧的屬性

爲了實現棧結構,咱們將會建立一個名爲 Stack 的構造函數。棧的每一個實例都有兩個屬性:_size 和 _storage。

function Stack() {
    this._size = 0;
    this._storage = {};
}
複製代碼

this._storage 屬性使棧的每個實例都具備本身的用來存儲數據的容器; this._size 屬性反映了當前棧中數據的個數。若是建立了一個新的棧的實例,而且有一個數據被存入棧中,那麼 this._size 的值將被增長到1。若是又有數據入棧,this._size 的值將增長到2。若是一個數據從棧中被取出,this._size 的值將會減小爲1。

棧的方法(操做)

咱們須要定義能夠向棧中添加(入棧)和從棧中取出(出棧)數據的方法。讓咱們從添加數據開始。

方法1/2: push(data)

(每個棧的實例都具備這個方法,因此咱們把它添加到棧結構的原型中)

咱們對這個方法有兩個要求:

  1. 每當添加數據時, 咱們但願可以增長棧的大小。
  2. 每當添加數據時,咱們但願可以保留它的添加順序。
Stack.prototype.push = function(data) {
    // increases the size of our storage
    var size = this._size++;
 
    // assigns size as a key of storage
    // assigns data as the value of this key
    this._storage[size] = data;
};
複製代碼

咱們實現push(data)方法時要包含如下邏輯:聲明一個變量 size 並賦值爲 this._size++。指定 size 爲 this._storage 的鍵;並將數據賦給相應鍵的值。

若是咱們調用push(data)方法5次,那麼棧的大小將是5。第一次入棧時,將會把數據存入this._storage 中鍵名爲1對應的空間,當第5次入棧時,將會把數據存入this._storage 中鍵名爲5對應的空間。如今咱們的數據有了順序!

方法2/2: pop()

咱們已經實現了把數據送入棧中,下一步咱們要從棧中彈出(刪除)數據。從棧中彈出數據並非簡單的刪除數據,它只刪除最後一次添加的數據。

如下是這個方法的要點:

  1. 使用棧當前的大小得到最後一次添加的數據。
  2. 刪除最後一次添加的數據。
  3. 使 _this._size 計數減一。
  4. 返回剛剛刪除的數據。
Stack.prototype.pop = function() {
    var size = this._size,
        deletedData;
 
    deletedData = this._storage[size];
 
    delete this._storage[size];
    this.size--;
 
    return deletedData;
};
複製代碼

pop()方法知足以上四個要點。首先,咱們聲明瞭兩個變量:size 用來初始化棧的大小;deletedData 用來保存棧中最後一次添加的數據。第二,咱們刪除了最後一次添加的數據的鍵值對。第三,咱們把棧的大小減小了1.第四,返回從棧中刪除的數據。

若是咱們測試當前實現的pop()方法,會發現它適用下面的案例:若是向棧內push數據,棧的大小會增長1,若是從棧中pop()數據,棧的大小會減小1!

爲了處理這個用例,咱們將向pop()中添加if語句。

Stack.prototype.pop = function() {
    var size = this._size,
        deletedData;
 
    if (size) {
        deletedData = this._storage[size];
 
        delete this._storage[size];
        this._size--;
 
        return deletedData;
    }
};
複製代碼

經過添加if語句,可使代碼在存儲中有數據時才被執行。

棧的完整實現

咱們已經實現了完整的棧結構。無論以怎樣的順序調用任何一個方法,代碼均可以工做!下面使代碼的最終版本:

function Stack() {
    this._size = 0;
    this._storage = {};
}
 
Stack.prototype.push = function(data) {
    var size = ++this._size;
    this._storage[size] = data;
};
 
Stack.prototype.pop = function() {
    var size = this._size,
        deletedData;
 
    if (size) {
        deletedData = this._storage[size];
 
        delete this._storage[size];
        this._size--;
 
        return deletedData;
    }
};
複製代碼

從棧到隊列

當咱們想要按順序添加數據或刪除數據時,可使用棧結構。根據它的定義,棧能夠只刪除最近添加的數據。若是想要刪除最先的數據該怎麼辦呢?這時咱們但願使用名爲queue的數據結構。

隊列

與棧相似,隊列也是一個線性數據結構。與棧不一樣的是,隊列只刪除最早添加的數據。

爲了幫助你明白隊列是如何工做的,讓咱們花點時間舉個例子。咱們能夠把隊列想象成爲熟食店的售票系統。每一個顧客拿一張票,當他們的號碼被呼叫時接受服務。持第一張票的顧客首先接受服務。

再進一步想象一下,這張票上有一個數字「1」。下一張票上有數字「2」。獲得二張票的顧客將會第二個接受服務。(若是咱們的售票系統像棧同樣運行,最早進入堆棧的客戶將會最後一個接受服務!)

隊列的一個更實際的例子是Web瀏覽器的事件循環。當觸發不一樣事件時,例如單擊某個按鈕,點擊事件將被添加到事件循環隊列中,並按照它們進入隊列的順序進行處理。

如今咱們具備了隊列的概念,接下來就要定義它的操做。你會注意到,隊列的操做和棧很是類似。區別就在被刪除的數據在什麼地方。

  • enqueue(data) 將數據添加到隊列中。
  • dequeue 刪除最先加入隊列的數據。

隊列的實現

如今讓咱們開始寫隊列的代碼吧!

隊列的屬性

在實現隊列的代碼中,咱們將會建立一個名爲 Queue 的構造方法。接下來添加三個屬性:_oldestIndex, _newestIndex, 和 _storage。在下一小節中,_oldestIndex 和 _newestIndex 的做用將變得更加清晰。

function Queue() {
    this._oldestIndex = 1;
    this._newestIndex = 1;
    this._storage = {};
}
複製代碼

隊列的方法

如今咱們將建立隊列會用到的三個方法:size(), enqueue(data), 和 dequeue(data)。我將描述每一個方法的做用,寫出每一個方法的代碼,而後解釋這些代碼。

方法1/3:size( )

這個方法有兩個做用:

  1. 返回當前隊列的長度。
  2. 保持隊列中鍵的正確範圍。
Queue.prototype.size = function() {
    return this._newestIndex - this._oldestIndex;
};
複製代碼

實現 size() 可能顯得微不足道,但你會很快發現並非這樣的。爲了理解其緣由,咱們必須快速從新審視 size() 在棧結構中的實現。

回想一下棧的概念模型,假設咱們把5個盤子添加到一個棧上。棧的大小是5,每一個盤子都有一個數字,從1(第一個添加的盤子)到5(最後一個添加的盤子)。若是咱們取走三個盤子,就只剩下兩個盤子。咱們能夠簡單地用5減去3,獲得正確的大小,也就是2。這是關於棧大小最重要的一點:當前大小至關於從棧頂部的盤子(2)到棧中其餘盤子(1)的計數。換句話說,鍵的範圍老是從當前大小到1之間。

如今,讓咱們將棧大小的實現應用到隊列中。假設有五個顧客從咱們的售票系統中取到了票。第一個顧客有一張顯示數字1的票,第五個客戶有一張顯示數字5的票。如今有了一個隊列,拿着第一張票的第一位顧客。

假設第一個客戶接受了服務,這張票會從隊列中被移除。與棧相似,咱們能夠經過從5減去1來得到隊列的正確大小。那麼服務隊列中還有4張票。如今出現了一個問題:隊列的大小不能對應正確的票號。若是咱們從五減去一個,獲得大小是4,可是不能使用4來肯定當前隊列中剩餘票的編號範圍。咱們並不能肯定隊列中票號的順序究竟是1到4仍是2到5。

這就是 oldestIndex 和 newestIndex 這兩個屬性 在隊列中的用途。全部這一切彷佛使人困惑——到如今我仍然會偶爾以爲困惑。下面的例子能夠幫助我門理順全部的邏輯。

假設咱們的熟食店有兩個售票系統:

  1. _newestindex 表明顧客售票系統的票。
  2. _oldestindex 表明員工售票系統的票。

對於兩個售票系統來講,這是最難掌握的概念:當兩個系統中的數字相同時,隊列中的每一個客戶都被處理了,隊列是空的。咱們將使用下面的場景來增強這種邏輯:

  1. 當顧客買票時,顧客的票號從_newestIndex 獲得,票的編號是1。顧客售票系統的下一張票號碼是2。
  2. 員工不買票,員工售票系統中當前票的編號是1。
  3. 咱們在顧客系統中獲得當前的票號2,減去員工系統中的號碼1,獲得的結果是1。這個數字1表示仍然在隊列中沒有被刪除的票的數量
  4. 員工從它們的售票系統中取票,這張票表明正在被服務的顧客的票號,從_oldestIndex中獲得,數字爲1。
  5. 重複第4步,如今差爲0,隊列中沒有其餘的票了。

如今屬性 _newestindex能夠告訴咱們被分配在隊列中票號的最大值(鍵),屬性 _oldestindex 能夠告訴咱們最早進入隊列中票號(鍵)。

探討完了size(),接下來看enqueue(data)方法。

方法2/3:enqueue(data)

對於 enqueue 方法,有兩個功能:

  1. 使用_newestIndex 的值做爲 this._storage 的鍵,並使用要添加的數據做爲該鍵的值。
  2. 將_newestIndex 的值增長1。

基於這兩個功能,咱們將編寫 enqueue(data) 方法的代碼:

Queue.prototype.enqueue = function(data) {
    this._storage[this._newestIndex] = data;
    this._newestIndex++;
};
複製代碼

該方法的主體只有兩行代碼。 在第一行,用 this._newestIndex 爲this._storage 建立一個新的鍵,併爲其分配數據。 this._newestIndex 始終從1開始。在第二行代碼中,咱們將 this._newestIndex 的值增長1,將其更新爲2。

以上是方法 enqueue(data) 的全部代碼。下面咱們來實現方法 dequeue( )。

方法2/3:dequeue( )

如下是此方法的兩個功能點:

  1. 刪除隊列中最舊的數據。
  2. 屬性 _oldestIndex 加1。
Queue.prototype.dequeue = function() {
    var oldestIndex = this._oldestIndex,
        deletedData = this._storage[oldestIndex];
 
    delete this._storage[oldestIndex];
    this._oldestIndex++;
 
    return deletedData;
};
複製代碼

在 dequeue( )的代碼中,咱們聲明兩個變量。 第一個變量 oldestIndex 給 this._oldestIndex 賦值。第二個變量 deletedData 被賦予 this._storage[oldestIndex] 的值。

下一步,刪除隊列中最先的索引。以後將 this._oldestIndex 的值加1。最後返回剛剛被刪除的數據。

與棧的 pop() 方法第一次實現中出現的問題相似,dequeue() 在隊列中沒有數據的狀況下不該該被執行。咱們須要一些代碼來處理這種狀況。

Queue.prototype.dequeue = function() {
    var oldestIndex = this._oldestIndex,
        newestIndex = this._newestIndex,
        deletedData;
 
    if (oldestIndex !== newestIndex) {
        deletedData = this._storage[oldestIndex];
        delete this._storage[oldestIndex];
        this._oldestIndex++;
 
        return deletedData;
    }
};
複製代碼

每當 oldestIndex 和 newestIndex 的值不相等時,咱們就執行前面的邏輯。

隊列的完整實現代碼

到此爲止,咱們實現了一個完整的隊列結構的邏輯。下面是所有代碼。

function Queue() {
    this._oldestIndex = 1;
    this._newestIndex = 1;
    this._storage = {};
}
 
Queue.prototype.size = function() {
    return this._newestIndex - this._oldestIndex;
};
 
Queue.prototype.enqueue = function(data) {
    this._storage[this._newestIndex] = data;
    this._newestIndex++;
};
 
Queue.prototype.dequeue = function() {
    var oldestIndex = this._oldestIndex,
        newestIndex = this._newestIndex,
        deletedData;
 
    if (oldestIndex !== newestIndex) {
        deletedData = this._storage[oldestIndex];
        delete this._storage[oldestIndex];
        this._oldestIndex++;
 
        return deletedData;
    }
};
複製代碼

結束語

在本文中,咱們探討了兩個線性數據結構:棧和隊列。棧按照順序存儲數據,並刪除最後添加的數據;隊列按順序存儲數據,但刪除最早的添加數據。

若是這些數據結構的實現看起來微不足道,請提醒本身數據結構的用途。它們並無被設計得過於複雜,它們是用來幫助咱們組織數據的。在這種狀況下,若是您發現有須要按順序組織數據的場合,請考慮使用棧或隊列。

原文首發於京程一燈公衆號:jingchengyideng

相關文章
相關標籤/搜索