你們好,我是前端圖圖。今天就來聊聊棧的數據結構,由於最近在學數據結構和算法,我也把寫文章看成一次覆盤。下面廢話很少說,開始吧!前端
棧是一種後進先出
原則的有序集合(就是後面進去的先出來的意思)。添加或待刪除的元素都保存在棧的同一端,叫做棧頂(棧的末尾),另外一端叫做棧底。在棧裏面,新元素都在棧頂,舊的元素都在棧底。git
在生活中有許多用來描述棧的例子。例如:疊放的書籍、盤子。算法
在計算機中,棧被用在編譯器和內存中保存變量、方法調用等等,也被用在瀏覽器的歷史記錄(瀏覽器的返回按鈕)。數組
下面用ES6
的類來建立一個棧。瀏覽器
class Stack {
constructor() {
this.items = [];
}
}
複製代碼
對於棧來講,可使用數組,也可使用對象。只要聽從LIFO(後進先出)
原則就行。markdown
下面是棧的一些方法。數據結構
push(ele)
:添加一個或多個元素到棧頂。pop()
:移除棧頂的元素並返回移除的元素。peek()
:獲取棧頂的元素。isEmpty()
:校驗是否爲空棧,若是爲空就返回true
,不然返回false
。clear()
:清空棧。size()
:返回棧中的元素數量。push
方法負責向棧內添加元素,這個方法只添加元素到棧頂(也就是棧的末尾)。數據結構和算法
push(ele) {
this.items.push(ele);
}
複製代碼
這裏是數組來保存棧的元素,因此直接使用數組的push
方法。ui
pop
方法負責移除棧裏的元素。棧聽從LIFO
原則,因此移除最後添加的元素。this
pop() {
return this.items.pop();
}
複製代碼
用push
和pop
這兩個方法操做棧裏的元素,這樣天然聽從LIFO
原則了。
若是想知道棧裏最後添加的元素是什麼,用peek
方法便可。
peek() {
return this.items[this.items.length - 1];
}
複製代碼
length - 1
便可訪問數組最後的一個元素。
上面的圖中,數組中包含了三個元素1, 2, 3
,數組的長度是3
。而length - 1(3-1)
就是2
。
isEmpty
方法校驗棧是否爲空棧,爲空就返回true
,不然返回false
。
isEmpty() {
return this.items.length === 0;
}
複製代碼
這裏簡單的判斷一下數組的長度是否爲0
就能夠了。
size
方法返回數組的長度就能夠獲取元素的個數了。
size() {
return this.items.length;
}
複製代碼
clear
方法移除棧中的全部元素,最簡單的方式就是把items
初始化成一個空數組。
clear() {
this.items = [];
}
複製代碼
這樣就完成了棧的方法。
首先初始化一個Stack
類,而後查看棧是否爲空。
const stack = new Stack();
console.log(stack.isEmpty()); // true 爲true就表明棧是空的
複製代碼
而後,向棧裏添加元素。
stack.push(1);
stack.push(2);
console.log(stack.peek()); // 2
複製代碼
這裏添加了1
和2
,固然你能夠添加任何類型的元素。而後調用了peek
方法,輸出的是2
,由於它是棧裏最後一個元素。
再往棧裏添加一個元素。
stack.push(10);
console.log(stack.size()); // 3
console.log(stack.isEmpty()); // false
複製代碼
咱們往棧裏添加了10
。調用size
方法,輸出的是3
,棧裏有三個元素。調用isEmpty
方法,輸出的是false
。
下面展現了到如今爲止對棧的操做,以及棧的當前狀態。
在調用pop
方法以前,棧裏有三個元素,調用兩次後,如今棧只剩下1
了。
stack.pop();
stack.pop();
console.log(stack.size()); // 1
複製代碼
class Stack {
constructor() {
this.items = [];
}
push(ele) {
this.items.push(ele);
}
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 = [];
}
}
const stack = new Stack();
console.log(stack.isEmpty()); // true
stack.push(1); // 向棧裏添加了元素1
stack.push(2); // 向棧裏添加了元素2
console.log(stack.peek()); // 此時棧裏最後一個元素爲2
stack.push(10); // 又往棧裏添加一個元素10
console.log(stack.size()); // 這時棧的長度就變成了3
console.log(stack.isEmpty()); // false
stack.pop(); // 從棧頂中移除了一項
stack.pop(); // 從棧頂中又移除了一項
console.log(stack.size()); // 從棧中移除了兩個元素,最後獲取棧的長度就是1
複製代碼
同樣的,使用類來建立基於對象的棧。
class Stack{
constructor {
this.count = 0;
this.items = {};
}
}
複製代碼
對象版本的Stack
類中,用count
屬性來記錄棧的大小(也能夠從棧中添加和刪除元素)。
在數組的版本中,能夠向Stack
類中添加多個元素,而在對象這個版本的push
方法只容許一次插入一個元素。
push(ele) {
this.items[this.count] = ele;
this.count++;
}
複製代碼
在JavaScript中,對象是一系列鍵值對的集合。要向棧中添加元素,使用count
變量做爲items
對象的鍵名,插入的元素就是它的值。向棧插入元素以後,就遞增count
變量。
const stack = new Stack();
stack.push(5);
stack.push(10);
console.log(stack);
// {count: 2, items: {0: 5, 1: 10}}
複製代碼
能夠看到Stack
類內部items
裏的值和count
屬性的值在最後的log
中輸出。
count
屬性也表示棧的大小。這樣就能夠用count
屬性的值來實現size
方法。
size() {
return this.count;
}
複製代碼
判斷棧是否爲空的話,驗證count
的值是否爲0
就能夠了。
isEmpty() {
return this.count === 0;
}
複製代碼
因爲沒有使用數組來存儲元素,就要手動實現移除元素的邏輯。pop
方法同樣返回從棧移除的元素。
pop() {
// 首先判斷棧是否爲空,若是爲空,就返回undefined
if (this.isEmpty()) {
return undefined;
}
// 若是棧不爲空的話,就將`count`屬性減1
this.count--;
// result保存了棧頂的元素
const result = this.items[this.count];
// 刪除棧頂的元素
delete this.items[this.count];
// 以後返回剛纔保存的棧頂元素
return result;
}
複製代碼
訪問棧頂的元素,只須要把count
屬性減1
便可。
peek() {
if (this.isEmpty()) {
return undefined;
}
this.items[this.count - 1];
}
複製代碼
清空棧只須要把它的值設置成初始化時的值就好了。
clear() {
this.items = {};
this.count = 0;
}
複製代碼
固然還能夠用下面這種方法移除棧裏的全部元素。
anotherClear() {
while(!this.isEmpty()) {
this.pop();
}
}
複製代碼
toString
方法在數組的版本中,並不須要建立toString
方法,由於可使用數組的toString
方法。但對象的版本,就要建立一個toString
方法來像數組那樣輸出棧的內容。
toString() {
// 棧爲空,將返回一個空字符串。
if (this.isEmpty()) {
return "";
}
// 棧不爲空,就須要用它底部的第一個元素做爲字符串的初始值
let objString = `${this.items[0]}`;
// 棧只包含一個元素,就不會執行`for`循環。
for (let i = 1; i < this.count; i++) {
// 迭代整個棧的鍵,一直到棧頂,添加一個逗號(,)以及下一個元素。
objString = `${objString},${this.items[i]}`;
}
return objString;
}
複製代碼
這樣就完成了兩個不一樣版本的Stack
類。這也是一個不一樣方法寫代碼的例子。對於使用Stack
類,選擇使用數組仍是對象並不重要,兩種方法都提供同樣的方法,只是內部實現不同而已。
class Stack {
constructor() {
this.count = 0;
this.items = {};
}
push(ele) {
this.items[this.count] = ele;
this.count++;
}
size() {
return this.count;
}
isEmpty() {
return this.count === 0;
}
pop() {
// 首先判斷棧是否爲空,若是爲空,就返回undefined
if (this.isEmpty()) {
return undefined;
}
// 若是棧不爲空的話,就將`count`屬性減1
this.count--;
// result保存了棧頂的元素
const result = this.items[this.count];
// 這裏是刪除棧頂的元素,因爲使用的是對象,因此可使用delete運算符從對象中刪除一個特定的值
delete this.items[this.count];
// 以後返回棧頂的元素
return result;
}
peek() {
if (this.isEmpty()) {
return undefined;
}
this.items[this.count - 1];
}
clear() {
this.items = {};
this.count = 0;
}
anotherClear() {
while (!this.isEmpty()) {
this.pop();
}
}
toString() {
// 棧爲空,將返回一個空字符串。
if (this.isEmpty()) {
return "";
}
// 棧不爲空,就須要用它底部的第一個元素做爲字符串的初始值
let objString = `${this.items[0]}`;
// 棧只包含一個元素,就不會執行`for`循環。
for (let i = 1; i < this.count; i++) {
// 迭代整個棧的鍵,一直到棧頂,添加一個逗號(,)以及下一個元素。
objString = `${objString},${this.items[i]}`;
}
return objString;
}
}
複製代碼
棧的實際應用很是普遍。它能夠存儲訪問過的任何或路徑、撤銷的操做。
在生活中,咱們主要使用十進制。但在計算機中,二進制很是重要,由於計算機的全部內容都是用二進制數字表示的(0
和1
)。
要把十進制轉成二進制,能夠將該十進制數除以2
(二進制是滿二進一)並對商取整,直到結果是0
爲止。舉個例子,把十進制的數10
轉成二進制的數字,下面是對應的算法。
function decimal(num) {
const remStack = []; // 存儲二進制的棧
let number = num; // 須要轉成二進制的數
let rem = ""; // 餘數
let binaryString = ""; // 存儲推出棧的元素
// 當參數不爲0時,進入while語句
while (number > 0) {
rem = Math.floor(number % 2);
remStack.push(rem); // 把餘數添加到remStack數組中
number = Math.floor(number / 2); // number除以2,獲得下次要取餘數的值,此時的number的值已經不是傳入的參數了。
}
while (remStack.length !== 0) {
// 用pop方法把棧中的元素移除,將移除棧的元素連成字符串
binaryString += remStack.pop().toString();
}
return binaryString;
}
console.log(decimal(10)); // 1010
console.log(decimal(100)); // 1100100
複製代碼
在上面這段代碼裏,當參數不是0
時,進入while
語句。就獲得一個餘數賦值給rem
,並放入棧裏。而後讓number
除以2
,就獲得了下次進入while語句
取餘數的值。要注意的是,此時的number
的值已經不是傳入參數的值了。最後,用pop
方法把棧中的元素移除,將移除的元素連成字符串。
修改以前的算法,能夠將十進制轉成計數爲2~36
的任何進制。除了把十進制除以2
轉成二進制數外,還能夠傳入其餘任何禁止的基數爲參數。
function baseConverter(num, base) {
const remStack = [];
const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let number = num;
let rem = "";
let baseString = "";
if (!(base >= 2 && base <= 36)) {
return "";
}
while (number > 0) {
rem = Math.floor(number % base);
remStack.push(rem);
number = Math.floor(number / base);
}
while (remStack.length !== 0) {
baseString += digits[remStack.pop()];
}
return baseString;
}
console.log(baseConverter(10000, 2)); // 10011100010000
console.log(baseConverter(10000, 8)); // 23420
console.log(baseConverter(10000, 16)); // 64
console.log(baseConverter(10000, 36)); // 7PS
複製代碼
上面的代碼只須要改一個地方,把十進制轉成二進制的時候,餘數是0
和1
,再把十進制轉八進制的時候,餘數是0~7
;可是把十進制轉十六進制時,餘數是0~9
再加上A、B、C、D、E、F
(對應 十、十一、十二、1三、1四、15)。因此須要對棧中的數組作一個轉換才行(baseString += digits[remStack.pop()]
這段代碼)。從十一進制開始,字母表中的每一個字母都對應一個基數,A
就表明基數11
,B
就表明基數12
,以此類推。
我的感受操做棧數據結構相對於其餘的數據結構來講,仍是比較簡單的。無論用對象仍是數組均可以實現棧數據結構,只要聽從LIFO(後進先出)
原則便可。喜歡的掘友能夠點擊關注+點贊哦!後面會持續更新其餘數據結構,也把本身學的知識分享給你們。固然寫做也能夠當成覆盤。2021年加油!實現本身的目標。