【資料結構基礎】棧簡介(使用ES6)

前端達人發表於2019-05-19

資料結構這詞大家都不陌生吧,這可是計算機專業人員的必修課專業之一,如果想成為專業的開發人員,必須深入理解這門課程,在這系列文章裡,筆者將使用ES6,讓大家熟悉資料結構這門專業課的內容。

到底什麼是資料結構?資料結構是計算機儲存、組織資料的方式。資料結構是指相互之間存在一種或多種特定關係的資料元素的集合。通常情況下,精心選擇的資料結構可以帶來更高的執行或者儲存效率。資料結構往往同高效的檢索演算法和索引技術有關(來源百度百科)。更多關於資料結構的介紹,大家可以先看看筆者的這篇文章《JavaScript 資料結構:什麼是資料結構?》

本篇文章筆者將從資料結構最基礎的結構開始介紹——stack(棧),筆者將從以下幾個方面進行介紹:

  • 什麼是棧?
  • 如何用JS簡單的實現棧?
  • 建立更高效的基於物件Stack類
  • 實際應用舉例
  • 練習題

本篇文章閱讀時間預計10分鐘。

什麼是棧?

棧是一種高效的資料結構(後進先出(LIFO)原則的有序集合),因為資料只能在棧頂新增或刪除,所以這樣的操作很快,而且容易實現。棧的使用遍佈程式語言實現的方方面面。程式語言中的編譯器也會使用到堆疊,計算機記憶體也會使用堆疊來儲存變數和方法呼叫,瀏覽器的後退功能。除了計算機方面有諸多棧的應用,現實中也有實際例子,比如我們從一摞書中拿一本書,吃自助餐從一摞盤子裡拿最上面的盤子,等等:

【資料結構基礎】棧簡介(使用ES6)

如何用JS簡單的實現棧?

我們如何使用JS模擬一個簡單的棧呢,首先我們建立一個stack-array.js檔案,宣告一個StackArray類,程式碼如下:

class StackArray {
    constructor() {
        this.items = []; // {1}
    }
}複製程式碼

接下來該怎麼做?我們需要一個能夠儲存堆疊元素的資料結構,我們可以使用陣列結構來完成,同時還需要我們在堆疊中新增和移除資料元素,由於堆疊後進先出的原則,我們的新增和刪除方法稍微特別些,Stack這個類的實現包含以下幾個方法:

  1. push(element(s)): 此方法將新新增的元素新增至堆疊的頂部
  2. pop():此方法刪除棧頂的元素,同時返回已刪除的元素
  3. peek(): 返回堆疊的頂部元素
  4. isEmpty(): 判斷堆疊是否為空,如果為空,返回True, 否則返回False。
  5. clear(): 清空堆疊所有的元素。
  6. size(): 此方法返回堆疊元素的數量,類似陣列的長度。
  7. toArray(): 以陣列的形式返回堆疊的元素。
  8. toString():以字串的形式輸出堆疊內容。

push()

此方法負責向堆疊新增新元素,其中最重要的特點就是:只能將新元素新增到棧頂,即堆疊的末尾,我們可以使用陣列的push方法正好符合這個需求,程式碼如下:

push(element) {
    this.items.push(element);
} 複製程式碼

pop()

接下來我們來實現pop()方法,此方法實現刪除棧頂的元素,由於遵循LIFO原則,刪除的是最後的元素,我們可以使用陣列自帶的pop方法,程式碼如下:

pop() {
    return this.items.pop();
} 複製程式碼

peek()、isEmpty()、size()、clear()

核心的新增和刪除已經完成,現在我們來實現相關的輔助方法, peek()方法讓我們獲取堆疊最後的一個元素,實現程式碼如下:

peek() {
    return this.items[this.items.length - 1];
} 複製程式碼

isEmpty()的方法也十分簡單,判斷堆疊陣列的長度是否為0即可,程式碼如下:

isEmpty() {
    return this.items.length === 0;
} 複製程式碼

size()方法更簡單,使用陣列的length方法即可,程式碼如下

size() {
    return this.items.length;
} 複製程式碼

最後實現最簡單的清空方法clear(),將堆疊變數重新置空賦值即可:

clear() {
    this.items = [];
}複製程式碼

最終完成的stack-array.js程式碼如下:

export default class StackArray {
    constructor() {
        this.items = [];
    }

    push(element) {
        this.items.push(element);
    }

    pop() {
        return this.items.pop();
    }

    peek() {
        return this.items[this.items.length - 1];
    }

    isEmpty() {
        return this.items.length === 0;
    }

    size() {
        return this.items.length;
    }

    clear() {
        this.items = [];
    }

    toArray() {
        return this.items;
    }

    toString() {
        return this.items.toString();
    }
}複製程式碼

接下來我們建立一個stackdemo.js的檔案,引入我們的stack-array.js檔案,我們一起來實踐下如何使用我們建立好的StackArray類,程式碼如下:

import StackArray from 'stack-array.js';
const stack = new StackArray();

console.log('stack.isEmpty() => ', stack.isEmpty()); // outputs true

stack.push(5);
stack.push(8);

console.log('stack after push 5 and 8 => ', stack.toString());

console.log('stack.peek() => ', stack.peek()); // outputs 8

stack.push(11);

console.log('stack.size() after push 11 => ', stack.size()); // outputs 3
console.log('stack.isEmpty() => ', stack.isEmpty()); // outputs false

stack.push(15);

stack.pop();
stack.pop();

console.log('stack.size() after push 15 and pop twice => ', stack.size()); // outputs 2複製程式碼

我們可以新建一個stackdemo.html,引入stackdemo.js(<script type="module" src="stackdemo.js"></script>),開啟stackdemo.html即可,堆疊的執行示意如下圖所示:

執行push()方法後的效果:

【資料結構基礎】棧簡介(使用ES6)

執行pop()方法後的效果:

【資料結構基礎】棧簡介(使用ES6)

建立更高效的基於物件Stack類

上一小節我們基於陣列快速實現了棧,我們清楚陣列是有序陣列,如果儲存大資料,內容過多的話,長度過大的話,會消耗更多的計算機記憶體,演算法的複雜度就會增加(O(n),後面的文章將會介紹),為了解決這個問題,我們使用更原始的方法進行實現。首先我們在stack.js檔案裡宣告stack類,程式碼如下:

class Stack {
    constructor() {
        this.count = 0;
        this.items = {};
    }
    // methods
} 複製程式碼

push()

在JS中,物件是一組鍵值對,我們可以將使用count變數作為items物件的鍵,元素是其值,添完新元素後,count變數加1,程式碼實現如下:

push(element) {
    this.items[this.count] = element;
    this.count++;
}複製程式碼

比如我們可以向空的棧裡新增新元素5和8,程式碼如下:

const stack = new Stack();
stack.push(5);
stack.push(8);複製程式碼

如果輸出Stack物件的items和count,效果如下:

items = {
    0: 5,
    1: 8
};
count = 2;複製程式碼

isEmpty()

判斷棧是否空,我們只需要判斷count變數是否為0即可,程式碼如下:

isEmpty() {
    return this.count === 0;
}複製程式碼

pop()

改寫這個方法,我們首先需要驗證堆疊是否為空,如果未空返回undefined,如果不為空,我們將變數count的值減1,同時刪除對應的屬性,程式碼如下:

pop() {
  if (this.isEmpty()) { 
    return undefined;
  }
  this.count--; 
  const result = this.items[this.count];
  delete this.items[this.count]; 
  return result; 
}複製程式碼

接下來我們改寫其他的方法,完整程式碼如下:

export default class Stack {
  constructor() {
    this.count = 0;
    this.items = {};
  }

  push(element) {
    this.items[this.count] = element;
    this.count++;
  }

  pop() {
    if (this.isEmpty()) {
      return undefined;
    }
    this.count--;
    const result = this.items[this.count];
    delete this.items[this.count];
    return result;
  }

  peek() {
    if (this.isEmpty()) {
      return undefined;
    }
    return this.items[this.count - 1];
  }

  isEmpty() {
    return this.count === 0;
  }

  size() {
    return this.count;
  }

  clear() {
    /* while (!this.isEmpty()) {
        this.pop();
      } */
    this.items = {};
    this.count = 0;
  }

  toString() {
    if (this.isEmpty()) {
      return '';
    }
    let objString = `${this.items[0]}`;
    for (let i = 1; i < this.count; i++) {
      objString = `${objString},${this.items[i]}`;
    }
    return objString;
  }
}複製程式碼

雖然我們類完成了,大家是不是覺得有點問題,由於我們建立的類的屬性對於任何開發人員都是公開的,我們希望只能在棧頂新增元素,不希望在其他位置新增元素,但是我們在Stack類中宣告的items和count屬性不受保護,這是JS的規則問題,難道我們沒有辦法改變了嗎?答案是可以,我們可以ES6加入的新型別Symbol資料型別作為物件的屬性具有私有性的特點(關於Symbol資料型別,筆者的這篇文章有過介紹《【ES6基礎】Symbol介紹:獨一無二的值》),改寫基於stack-array.js版本的程式碼,程式碼如下:

const _items = Symbol('stackItems');

class Stack {
    constructor() {
        this[_items] = [];
    }

    push(element) {
        this[_items].push(element);
    }

    pop() {
        return this[_items].pop();
    }

    peek() {
        return this[_items][this[_items].length - 1];
    }

    isEmpty() {
        return this[_items].length === 0;
    }

    size() {
        return this[_items].length;
    }

    clear() {
        this[_items] = [];
    }

    print() {
        console.log(this.toString());
    }

    toString() {
        return this[_items].toString();
    }
}

const stack = new Stack();
const objectSymbols = Object.getOwnPropertySymbols(stack);
console.log(objectSymbols.length); // 1
console.log(objectSymbols); // [Symbol()]
console.log(objectSymbols[0]); // Symbol()
stack[objectSymbols[0]].push(1);
stack.print(); // 5, 8, 1複製程式碼

實際應用舉例

堆疊在實際的問題中有著各種各樣的運用,比如我們會經常使用各種軟體的撤銷操作功能,尤其是Java和C#程式語言使用堆疊來變數儲存和方法呼叫,並且可以丟擲堆疊溢位異常,尤其是在使用遞迴演算法時。接下來,我們親自動手實現個10進位制轉2進位制的功能。

我們已經熟悉十進位制。 然而,二進位制表示在電腦科學中非常重要,因為計算機中的所有內容都由二進位制數字(0和1)表示。 如果沒有在十進位制和二進位制數之間來回轉換的能力,與計算機進行通訊將會十分困難。要將10進位制轉換成2進位制,我們需要將要轉換數字除以2,再將結果除以2,如此迴圈直到結果為0為止,具體示意如圖所示:

【資料結構基礎】棧簡介(使用ES6)

基於上圖邏輯釋義,完成的功能程式碼如下:

function decimalToBinary(decNumber) {
        const remStack = new Stack();
        let number = decNumber;
        let rem;
        let binaryString = '';

        while (number > 0) {
            rem = Math.floor(number % 2); 
            remStack.push(rem); 
            number = Math.floor(number / 2); 
        }

        while (!remStack.isEmpty()) { 
            binaryString += remStack.pop().toString();
        }

        return binaryString;
    }複製程式碼

從上述程式碼我們定義了一個堆疊,使用迴圈處理待處理的數字,將取模的餘數推入堆疊,然後逐個彈出,拼接成字串進行輸出。接著我們測試下,十進位制轉二進位制是否符合預期,如下段測試程式碼:

console.log(decimalToBinary(233)); // 11101001
console.log(decimalToBinary(10)); // 1010
console.log(decimalToBinary(1000)); // 1111101000”複製程式碼

練習題

剛才我們實踐了十進位制轉二進位制,為了讓其更有通用性,因為在實際應用中,不僅僅有二進位制的轉換需求,比如還有8進位制、16進位制等,現在筆者要給大家留作業了,實現函式baseConverter(decNumber, base),第一引數是要轉換的10進位制數,第二個引數是需要轉換的進位制,讓其具備10進位制數任意轉換成2~36進位制的需求,歡迎大家在留言區貼程式碼。

小節

本篇文章,我們瞭解了什麼是資料結構,並深入學習了堆疊這個資料結構,以及如何用JS程式碼實現堆疊,並講解了不同的實現方式,同時瞭解棧在計算機領域的應用,並一起實踐了一個十進位制數轉二進位制的練習,接下來本系列文章,筆者將帶著大家一起深入學習和堆疊類似的佇列結構,唯一不同的就是先進先出(FIFO)。

更多精彩內容,請微信關注”前端達人”公眾號!

【資料結構基礎】棧簡介(使用ES6)


相關文章