資料結構這詞大家都不陌生吧,這可是計算機專業人員的必修課專業之一,如果想成為專業的開發人員,必須深入理解這門課程,在這系列文章裡,筆者將使用ES6,讓大家熟悉資料結構這門專業課的內容。
到底什麼是資料結構?資料結構是計算機儲存、組織資料的方式。資料結構是指相互之間存在一種或多種特定關係的資料元素的集合。通常情況下,精心選擇的資料結構可以帶來更高的執行或者儲存效率。資料結構往往同高效的檢索演算法和索引技術有關(來源百度百科)。更多關於資料結構的介紹,大家可以先看看筆者的這篇文章《JavaScript 資料結構:什麼是資料結構?》。
本篇文章筆者將從資料結構最基礎的結構開始介紹——stack(棧),筆者將從以下幾個方面進行介紹:
- 什麼是棧?
- 如何用JS簡單的實現棧?
- 建立更高效的基於物件Stack類
- 實際應用舉例
- 練習題
本篇文章閱讀時間預計10分鐘。
什麼是棧?
棧是一種高效的資料結構(後進先出(LIFO)原則的有序集合),因為資料只能在棧頂新增或刪除,所以這樣的操作很快,而且容易實現。棧的使用遍佈程式語言實現的方方面面。程式語言中的編譯器也會使用到堆疊,計算機記憶體也會使用堆疊來儲存變數和方法呼叫,瀏覽器的後退功能。除了計算機方面有諸多棧的應用,現實中也有實際例子,比如我們從一摞書中拿一本書,吃自助餐從一摞盤子裡拿最上面的盤子,等等:
如何用JS簡單的實現棧?
我們如何使用JS模擬一個簡單的棧呢,首先我們建立一個stack-array.js檔案,宣告一個StackArray類,程式碼如下:
class StackArray {
constructor() {
this.items = []; // {1}
}
}複製程式碼
接下來該怎麼做?我們需要一個能夠儲存堆疊元素的資料結構,我們可以使用陣列結構來完成,同時還需要我們在堆疊中新增和移除資料元素,由於堆疊後進先出的原則,我們的新增和刪除方法稍微特別些,Stack這個類的實現包含以下幾個方法:
- push(element(s)): 此方法將新新增的元素新增至堆疊的頂部
- pop():此方法刪除棧頂的元素,同時返回已刪除的元素
- peek(): 返回堆疊的頂部元素
- isEmpty(): 判斷堆疊是否為空,如果為空,返回True, 否則返回False。
- clear(): 清空堆疊所有的元素。
- size(): 此方法返回堆疊元素的數量,類似陣列的長度。
- toArray(): 以陣列的形式返回堆疊的元素。
- 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()方法後的效果:
執行pop()方法後的效果:
建立更高效的基於物件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為止,具體示意如圖所示:
基於上圖邏輯釋義,完成的功能程式碼如下:
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)。
更多精彩內容,請微信關注”前端達人”公眾號!