棧
前面,我們學習了如何創建和使用計算機科學中最常用的數據結構——數組。
我們知道可以在數組的任意位置添加或刪除元素,但有時我們還需要一種能在添加和刪除元素時有更多控制的數據結構。有兩種類似數組的數據結構在添加和刪除時有更多控制,它們就是棧和隊列。
棧數據結構
棧是一種遵循后進先出(或先進后出)原則的有序集合。新添加的元素或待刪除的元素在棧的一端,稱作棧頂,另一端叫棧底。在棧里,新元素都靠近棧頂,舊元素都接近棧底。
現實生活中有很多棧的例子,比如桌上的一摞書或一疊盤子
棧也被用於瀏覽器歷史記錄,即瀏覽器的返回按鈕
創建一個基於數組的棧
定義一個類來表示棧:
class Stack{
constructor(){
this.items = [] // {1}
}
}
我們需要一個數據結構來存儲棧中的元素。這里選用數組(行{1})來存儲棧中的元素。
由於數組可以在任意位置添加或刪除元素,而棧遵循后進先出(LIFO)原則,所以需要對元素的插入和刪除做限制,接下來我們給棧定義一些方法:
- push():添加一個或多個元素到棧頂
- pop():移除棧頂元素,同時返回被移除的元素
- peek():返回棧頂元素(不做其他處理)
- clear():移除棧中的所有元素
- size():返回棧中元素個數
- isEmpty():如果棧中沒有元素則返回 true,否則返回 false
向棧添加元素
push(...values) {
this.items.push(...values)
}
從棧移除元素
pop() {
return this.items.pop()
}
只能通過 push 和 pop 方法添加和刪除棧中元素,這樣一來,我們的棧自然遵循 LIFO 原則。
查看棧頂元素
peek() {
return this.items[this.items.length - 1]
}
清空棧元素
clear(){
this.items = []
}
也可以多次調用 pop 方法。
棧中元素個數
size() {
return this.items.length
}
棧是否為空
isEmpty() {
return this.size() === 0
}
使用 Stack 類
class Stack {
constructor() {
this.items = []
}
push(...values) {
this.items.push(...values)
}
pop() {
return this.items.pop()
}
peek() {
return this.items[this.items.length - 1]
}
clear() {
this.items = []
}
size() {
return this.items.length
}
isEmpty() {
return this.size() === 0
}
}
const stack = new Stack()
stack.push(1)
stack.push(2, 3, 4)
console.log(stack.items) // [ 1, 2, 3, 4 ]
console.log(stack.pop()) // 4
console.log(stack.items) // [ 1, 2, 3 ]
console.log(stack.peek()) // 3
console.log(stack.size()) // 3
stack.clear()
console.log(stack.isEmpty()) // true
創建一個基於對象的 Stack 類
創建 Stack 最簡單的方式是使用數組來存儲元素,我們還可以使用對象來存儲元素。
class Stack {
constructor() {
this.count = 0
this.items = {}
}
push(...values) {
values.forEach(item => {
this.items[this.count++] = item
})
}
pop() {
if (this.isEmpty()) {
return undefined
}
this.count--
let result = this.items[this.count]
delete this.items[this.count]
return result
}
peek() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.count - 1]
}
clear() {
this.items = {}
this.count = 0
}
size() {
return this.count
}
isEmpty() {
return this.size() === 0
}
}
const stack = new Stack()
stack.push(1)
stack.push(2, 3, 4)
console.log(stack.items) // { '0': 1, '1': 2, '2': 3, '3': 4 }
console.log(stack.pop()) // 4
console.log(stack.items) // { '0': 1, '1': 2, '2': 3 }
console.log(stack.peek()) // 3
console.log(stack.size()) // 3
stack.clear()
console.log(stack.isEmpty()) // true
Tip:clear() 方法還可以使用下面邏輯移除棧中所有元素
clear() {
while (!this.isEmpty()) {
this.pop()
}
}
創建 toString 方法
toString() 方法返回一個表示該對象的字符串。
對於基於數組的棧,我們可以這樣寫:
toString() {
return this.items.toString()
}
const stack = new Stack()
stack.push('a', 'b', 'c')
console.log(stack.toString()) // a,b,c
基於對象的棧稍微麻煩點,我們可以這樣:
toString() {
// 轉為類數組
const arrayLike = Object.assign({}, this.items, { length: this.count })
// 轉為數組,然后使用數組的 toString
return Array.from(arrayLike).toString()
}
保護數據內部元素
在創建別的開發者也能使用的數據結構時,我們希望保護內部元素,只有通過我們暴露的方法才能修改內部結構。
對於 Stack 類,要確保元素只能被添加到棧頂,可惜現在我們在 Stack 中聲明的 items 並沒有被保護。
使用者可以輕易的獲取 items 並對其直接操作,就像這樣:
const stack = new Stack()
// 在棧底插入元素
stack.items.unshift(2)
下划線命名約定
我們可以用下划線命名約定來標記一個屬性為私有屬性:
class Stack {
constructor() {
this._count = 0
this._items = {}
}
}
注:這種方式只是一種約定,只能依靠使用我們代碼的開發者所具備的常識。
使用 Symbol
Symbol 可以保證屬性名獨一無二,我們可以將 items 改寫為:
const unique = Symbol("Stack's items")
class Stack {
constructor() {
this[unique] = []
}
push(...values) {
this[unique].push(...values)
}
// ...省略其他方法
}
這樣,我們不太好直接獲取 items,但我們還是可以通過 getOwnPropertySymbols() 獲取 items 的新名字,從而對內部數據進行直接修改:
const stack = new Stack()
stack.push(1)
console.log(stack) // Stack { [Symbol(Stack\'s items)]: [ 1 ] }
let symbol = Object.getOwnPropertySymbols(stack)[0]
console.log('symbol: ', symbol);
stack[symbol].unshift(2) // {1}
console.log(stack) // Stack { [Symbol(Stack\'s items)]: [ 2, 1 ] }
在行{1},我們給棧底添加元素,打破了棧只能在棧頂添加元素的原則。
用 WeakMap
將對象的 items 存在 WeakMap 中,請看示例:
const weakMap = new WeakMap()
class Stack {
constructor() {
weakMap.set(this, [])
}
push(...values) {
weakMap.get(this).push(...values)
}
toString() {
return weakMap.get(this).toString()
}
// ...省略其他方法
}
const stack = new Stack()
stack.push(1, 2)
console.log(stack.toString()) // 1,2
現在 items 在 Stack 中是真正的私有屬性,我們無法直接獲取 items。但擴展類時無法繼承私有屬性,因為該屬性不在 Stack 中。
類私有域
類屬性在默認情況下是公共的,可以被外部類檢測或修改。在ES2020 實驗草案 中,增加了定義私有類字段的能力,寫法是使用一個#作為前綴。
class Stack {
#items
constructor() {
this.#items = []
}
push(...values) {
this.#items.push(...values)
}
toString(){
return this.#items.toString()
}
// ...省略其他方法
}
const stack = new Stack()
stack.push(1, 2)
console.log(stack.toString()) // 1,2
Tip:這段代碼可以在 chrome 74+ 運行
在瀏覽器控制台訪問 #items 報錯,進一步證明該私有變量無法在外訪問:
> stack.#items
VM286:1 Uncaught SyntaxError: Private field '#items' must be declared in an enclosing class
用棧解決問題
十進制轉二進制
比如10轉為二進制是1010,方法如下:
Step1 10/2=5 余0
Step2 5/2=2 余1 (2.5向下取整為2)
Step3 2/2=1 余0
Step4 1/2=0 余1 (0.5向下取整為0)
將余數依次放入棧中:0 1 0 1
最后,將余數依次移除則是結果:1010
實現如下:
/**
*
* 將十進制轉為二進制
* @param {正整數} decimal
* @return {String}
*/
function decimalToBianary(decimal) {
// 存放余數
const remainders = new Stack()
let number = decimal
let result = ''
while (number > 0) {
remainders.push(number % 2)
number = Math.floor(number / 2)
}
while (!remainders.isEmpty()) {
result += remainders.pop()
}
return result
}
console.log(decimalToBianary(10)) // 1010
注:這里只考慮正整數轉為二進制,不考慮小數,例如 10.1。
平衡圓括號
判斷輸入字符串是否滿足平衡圓括號,請看以下示例:
() -> true
{([])} -> true
{{([][])}()} -> true
[{()] -> false
- 空字符串視為平衡
- 字符只能是這6個字符:{ [ ( ) ] }。例如 (0) 則視為不平衡。
function blanceParentheses(symbols) {
// 處理空字符
if (symbols.length === 0) {
return true
}
// 包含 {}[]() 之外的字符
if ((/[^\{\}\[\]\(\)]/g).test(symbols)) {
return false
}
let blance = true
let symbolMap = {
'(': ')',
'[': ']',
'{': '}',
}
const stack = new Stack()
for (let item of symbols) {
// 入棧
if (symbolMap[item]) {
stack.push(item)
// 不是入棧就是出棧,出棧字符不匹配則說明不平衡
} else if (symbolMap[stack.pop()] !== item) {
blance = false
break
}
}
return blance && stack.isEmpty();
}
console.log(blanceParentheses(`{([])}`)) // true
console.log(blanceParentheses(`{{([][])}()}`)); // true
console.log(blanceParentheses(`[{()]`)) // false
console.log(blanceParentheses(`(0)`)) // false
console.log(blanceParentheses(`()[`)) // false
漢諾塔
從左到右有三根柱子 A B C,柱子 A 有 N 個碟子,底部的碟子最大,越往上碟子越小。需要將 A 中的所有碟子移到 C 中,每次只能移動一個,移動過程中必須保持上面的碟子比下面的碟子要小。問需要移動多少次,如何移動?
可以使用遞歸,大致思路:
- 將 A 中 N - 1 的碟子移到 B 中
- 將 A 中最后一個碟子移到 C 中
- 將 B 中所有碟子移到 C 中
Tip: 遞歸是一種解決問題的方法,每個遞歸函數必須有基線條件,即不再遞歸調用的條件(停止點)。后續會有章節詳細講解遞歸。
/**
*
* 漢諾塔
* @param {Number} count 大於0的整數
* @param {Stack} from
* @param {Stack} to
* @param {Stack} helper
* @param {Array} steps 存儲詳細步驟
*/
function hanoi(count, from, to, helper, steps) {
if (count === 1) {
const plate = from.pop()
to.push(plate)
steps.push(Array.of(plate, from.name, to.name))
return
}
// 將 from 中 count - 1 個移到 helper
hanoi(count - 1, from, helper, to, steps)
// 將 from 中最后一個移到 to
hanoi(1, from, to, helper, steps)
// 將 helper 中的移到 to
hanoi(count - 1, helper, to, from, steps)
}
// 測試漢諾塔
function testHanoi(plateCount) {
const fromStack = new Stack()
const toStack = new Stack()
const helperStack = new Stack()
fromStack.name = 'A'
toStack.name = 'C'
helperStack.name = 'B'
const result = []
let i = plateCount
while (i > 0) {
fromStack.push(`碟子${i--}`)
}
hanoi(plateCount, fromStack, toStack, helperStack, result)
result.forEach((item, i) => {
console.log(`step${i + 1} ${item[0]} ${item[1]} -> ${item[2]}`);
})
}
testHanoi(2)
console.log()
testHanoi(3)
step1 碟子1 A -> B
step2 碟子2 A -> C
step3 碟子1 B -> C
step1 碟子1 A -> C
step2 碟子2 A -> B
step3 碟子1 C -> B
step4 碟子3 A -> C
step5 碟子1 B -> A
step6 碟子2 B -> C
step7 碟子1 A -> C
Tip: hanoi() 方法還可以精簡:
function hanoi(count, from, to, helper, steps) {
if (count >= 1) {
// 將 from 中 count - 1 個移到 helper
hanoi(count - 1, from, helper, to, steps)
// 將 from 中最后一個移到 to
const plate = from.pop()
to.push(plate)
steps.push(Array.of(plate, from.name, to.name))
// 將 helper 中的移到 to
hanoi(count - 1, helper, to, from, steps)
}
}
Stack 完整代碼
基於數組的棧
/**
* 棧(基於數組的棧)
* @class StackOfArray
*/
class StackOfArray {
constructor() {
this.items = []
}
push(...values) {
this.items.push(...values)
}
pop() {
return this.items.pop()
}
peek() {
return this.items[this.items.length - 1]
}
clear() {
this.items = []
}
size() {
return this.items.length
}
isEmpty() {
return this.size() === 0
}
toString() {
return this.items.toString()
}
}
基於對象的棧
/**
* 棧(基於對象的棧)
* @class StackOfObject
*/
class StackOfObject {
constructor() {
this.count = 0
this.items = {}
}
push(...values) {
values.forEach(item => {
this.items[this.count++] = item
})
}
pop() {
if (this.isEmpty()) {
return undefined
}
this.count--
let result = this.items[this.count]
delete this.items[this.count]
return result
}
peek() {
if (this.isEmpty()) {
return undefined
}
return this.items[this.count - 1]
}
clear() {
this.items = {}
this.count = 0
}
size() {
return this.count
}
isEmpty() {
return this.size() === 0
}
toString() {
// 轉為類數組
const arrayLike = Object.assign({}, this.items, { length: this.count })
// 轉為數組,然后使用數組的 toString
return Array.from(arrayLike).toString()
}
}