願每次回憶,對生活都不感到負疚。——郭小川
棧,英文 Last In First Out 簡稱 LIFO,遵從後進先出的原則,與 “隊列” 相反,在棧的頭部添加元素、刪除元素,如果棧中沒有元素就稱爲空棧。
作者簡介:五月君,Nodejs Developer,熱愛技術、喜歡分享的 90 後青年,公衆號「Nodejs技術棧」,Github 開源項目 https://www.nodejs.red
棧簡介
在現實生活場景中也很多例子,例如盤子疊放,從上面一個一個放置,取時也是從上面一個一個拿走,不可能從下面直接抽着拿,如下圖所示
這也是棧的典型應用,通過這個例子也可總結出棧的兩個特性:
- 僅能從棧頂端存取數據
- 數據存取遵從後進先出原則
棧的運行機制
關於棧的概念通過前面的學習,應該有了初步的認知,這裏從零實現一個棧進一步對棧的運行機制做一個分析,下面看下我們實現棧需要哪些步驟:
- Constructor(capacity): 初始化棧內存空間,設定棧的容量
- isEmpty(): 檢查棧是否爲空,是否有元素
- isOverflow(): 檢查棧空間是否已滿,如果滿了是不能在入棧的
- enStack(element): 棧頂位置入棧,先判斷棧是否已滿
- deStack(): 棧頂位置出棧,先判斷棧元素是否爲空
- len(): 棧空間已有元素長度
- clear(): 清空棧元素,內存空間還是保留的
- destroy(): 銷燬棧,同時內存也要回收(通常高級語言都會有自動回收機制,例如 C 語言這時就需要手動回收)
- traversing(): 遍歷輸出棧元素
初始化棧空間
在構造函數的 constructor 裏進行聲明,傳入 capacity 初始化棧空間同時初始化棧的頂部(top)爲 0,底部則無需關注永遠爲 0。
/**
*
* @param { Number } capacity 棧空間容量
*/
constructor(capacity) {
if (!capacity) {
throw new Error('The capacity field is required!');
}
this.capacity = capacity;
this.stack = new Array(capacity);
this.top = 0; // 初始化棧頂爲 0
}
棧空間是否爲空檢查
定義 isEmpty() 方法返回棧空間是否爲空,根據 top 棧頂位置進行判斷。
isEmpty() {
return this.top === 0 ? true : false;
}
棧空間是否溢出檢查
定義 isOverflow() 方法返回棧空間是否溢出,根據棧頂位置和棧的空間容量進行判斷。
isOverflow() {
return this.top === this.capacity;
}
入棧
定義 enStack(element) 方法進行入棧操作,element 爲入棧傳入的參數,入棧之前先判斷,棧是否已滿,棧未滿情況下可進行入棧操作,最後棧位置做 ++ 操作。
/**
* 入棧
* @param { * } element 入棧元素
*/
enStack(element) {
if (this.isOverflow()) {
throw new Error('棧已滿');
}
this.stack[this.top] = element;
this.top++;
}
出棧
定義 enStack(element) 方法進行出棧操作,首先判斷棧空間是否爲空,未空的情況進行出棧操作,注意這裏的棧位置,由於元素進棧之後會進行 ++ 操作,那麼在出棧時當前棧位置肯定是沒有元素的,需要先做 -- 操作。
deStack() {
if (this.isEmpty()) {
throw new Error('棧已爲空');
}
this.top--;
return this.stack[this.top];
}
棧元素長度
這個好判斷,根據棧的 top 位置信息即可
len() {
return this.top;
}
清除棧元素
這裏有幾種實現,你也可以把 stack 的空間進行初始化,或者把 top 棧位置設爲 0 也可。
clear() {
this.top = 0;
}
棧銷燬
在一些高級語言中都會有垃圾回收機制,例如 JS 中只要當前對象不再持有引用,下次垃圾回收來臨時將會被回收。不清楚的可以看看我之前寫的 Node.js 內存管理和 V8 垃圾回收機制
destroy() {
this.stack = null;
}
棧元素遍歷
定義 traversing(isBottom) 方法對棧的元素進行遍歷輸出,默認爲頂部遍歷,也可傳入 isBottom 參數爲 true 從底部開始遍歷。
traversing(isBottom = false){
const arr = [];
if (isBottom) {
for (let i=0; i < this.top; i++) {
arr.push(this.stack[i])
}
} else {
for (let i=this.top-1; i >= 0; i--) {
arr.push(this.stack[i])
}
}
console.log(arr.join(' | '));
}
做一些測試
做下測試分別看下入棧、出棧、遍歷操作,其它的功能大家在練習的過程中可自行實踐。
const s1 = new StackStudy(4);
s1.enStack('Nodejs'); // 入棧
s1.enStack('技');
s1.enStack('術');
s1.enStack('棧');
s1.traversing() // 棧 | 術 | 技 | Nodejs
console.log(s1.deStack()); // 出棧 -> 棧
s1.traversing() // 術 | 技 | Nodejs
s1.traversing(true) // 從棧底遍歷:Nodejs | 技 | 術
下面通過一張圖展示以上程序的入棧、出棧過程
棧的運行機制源碼地址如下:
https://github.com/Q-Angelo/project-training/tree/master/algorithm/stack.js
JavaScript 數組實現棧
JavaScript 中提供的數組功能即可實現一個簡單的棧,使用起來也很方便,熟悉相關 API 即可,下面我們來看下基於 JS 數組的入棧、出棧過程實現。
以上圖片展示了棧的初始化、入棧、出棧過程,下面我們採用 JavaScript 原型鏈的方式實現。
初始化隊列
初始化一個存儲棧元素的數據結構,如果未傳入默認賦值空數組。
function StackStudy(elements) {
this.elements = elements || [];
}
添加棧元素
實現一個 enStack 方法,向棧添加元素,注意只能是棧頭添加,使用 JavaScript 數組中的 push 方法。
StackStudy.prototype.enStack = function(element) {
this.elements.push(element);
}
移除棧元素
實現一個 deStack 方法,棧尾部彈出元素,使用 JavaScript 數組中的 pop 方法(這一點是和隊列不同的)。
StackStudy.prototype.deStack = function() {
return this.elements.pop();
}
通過 JavaScript 數組實現是很簡單的,源碼如下:
https://github.com/Q-Angelo/project-training/tree/master/algorithm/stack-js.js
棧的經典應用
通過對前面的講解,相信已經對棧有了一定的瞭解,那麼它可以用來做什麼呢,本節舉幾個典型的應用案例。
十進制轉換爲二進制、八進制、十六進制
現在生活中我們使用最多的是十進制來表示,也是人們最易懂和記得的,但是計算機在處理的時候就要轉爲二進制進行計算,在十進制與二進制的轉換過程之間一般還會用八進制或者十六進制作爲二進制的縮寫。
因此,這裏主要講解十進制、八進制、十六進制、二進制轉換過程中在棧中的實際應用。首先你需要先了解這幾種數據類型之間的轉換規則,也不難通過一張圖來告訴你。
上圖中我們用十進制整除需要轉換的數據類型(二進制、八進制、十六進制),將餘數放入棧中,明白這個原理在用代碼實現就很簡單了。
編碼
const StackStudy = require('./stack.js');
const str = '0123456789ABCDEF';
function dataConversion(num, type) {
let x = num;
const s1 = new StackStudy(20);
while (x != 0) {
s1.enStack(x % type);
x = Math.floor(x / type);
}
while (!s1.isEmpty()) {
console.log(str[s1.deStack()]);
}
console.log('--------------------');
return;
}
引用我們在棧的運行機制裏面講解的代碼,編寫 dataConversion 方法,入棧、出棧進行遍歷輸出。代碼中定義的變量 str 是爲了十六進制會出現字母的情況做的處理。
測試
以下運行結果完全符合我們的預期,大家也可用電腦自帶的計算器功能進行驗證。
// 測試八進制
dataConversion(1024, 8); // 2000
// 測試十六進制
dataConversion(1024, 16); // 400
// 測試十六進制帶字母的情況
dataConversion(3000, 16); // BB8
// 測試二進制
dataConversion(1024, 2); // 10000000000
十進制轉換爲二進制、八進制、十六進制源碼地址:
https://github.com/Q-Angelo/project-training/tree/master/algorithm/stack-data-conversion.js
平衡園括號
這個也是另外一個實際問題,在一些運算中你可能寫過如下表達式,爲了保證操作順序,使用了括號,但是要注意括號必須是平衡的,每個左括號要對應一個右括號,否則程序將無法正常運行
((1 + 2) * 3 * (4 + 5)) * 6
以上示例組成的平衡表達式
(()())
非平衡表達式
(()()
通過“棧”解決平衡園括號問題實現步驟
- 初始化一個空棧 {1}
- 遍歷需要檢測的符號 {2}
- 遍歷需要檢測的平衡符號都有哪些 {3}
- 如果字符屬於入棧的符號([ { (...)將其入棧 {3.1}
- 如果字符屬於閉合的符號,先判斷棧空間是否爲空,空的情況下中斷操作,否則進行出棧,如果出棧的字符也不是閉合符號對應的開放符號,檢測失敗,中斷操作跳出循環 {3.2}
- 每一次循環完成判斷當前是否中斷,如果已經中斷操作,將不合法的字符入棧,中斷最外層字符檢測循環 {4}
- 最後檢測棧是否爲空,如果爲空則通過,否則不通過輸出 {5}
編碼實現
可以參照 “通過“棧”解決平衡園括號問題實現步驟” 有助於理解以下代碼
const Stack = require('./stack');
const detectionStr = '[]()'; // 定義需要檢測的平衡符號,如何還有別的符號按照這種格式定義
function test(str) {
let isTermination = false; // 是否終止,默認 false
let stack = new Stack(20); // 初始化棧空間 {1}
for (let i=0; i<str.length; i++) { // {2}
const s = str[i];
for (let d=0; d<detectionStr.length; d+=2) { // {3}
const enStackStr = detectionStr[d]; // 入棧字符
const deStackStr = detectionStr[d+1]; // 出棧字符
switch (s) {
case enStackStr : // 入棧 {3.1}
stack.enStack(s);
break;
case deStackStr : // 出棧 {3.2}
if (stack.isEmpty()) {
isTermination = true
} else {
const currElement = stack.deStack();
if (!currElement.includes(enStackStr)) {
isTermination = true
}
}
break;
}
if (isTermination) break;
}
if (isTermination) { // 存在不匹配符號,提前終止 {4}
stack.enStack(s);
break;
}
}
if (stack.isEmpty()) { // {5}
console.log('檢測通過');
} else {
console.log('檢測不通過,檢測不通過符號:');
stack.traversing()
}
return stack.isEmpty();
}
編碼測試
test('((()()[]])') // 檢測不通過,檢測不通過符號:](
test('()()[])') // 檢測不通過,檢測不通過符號:)
test('[()()[]') // 檢測不通過,檢測不通過符號:[
test('()()][]') // 檢測通過
平衡園括號問題源碼地址:
https://github.com/Q-Angelo/project-training/tree/master/algorithm/stack-balance-symbol.js