大家好,我是前端图图。今天就来聊聊栈的数据结构,因为最近在学数据结构和算法,我也把写文章当作一次复盘。下面废话不多说,开始吧!
栈
栈的数据结构
栈是一种后进先出
原则的有序集合(就是后面进去的先出来的意思)。添加或待删除的元素都保存在栈的同一端,叫作栈顶(栈的末尾),另一端叫作栈底。在栈里面,新元素都在栈顶,旧的元素都在栈底。
在生活中有许多用来描述栈的例子。例如:叠放的书籍、盘子。
在计算机中,栈被用在编译器和内存中保存变量、方法调用等等,也被用在浏览器的历史记录(浏览器的返回按钮)。
基于数组的栈
下面用ES6
的类来创建一个栈。
class Stack {
constructor() {
this.items = [];
}
}
对于栈来说,可以使用数组,也可以使用对象。只要遵从LIFO(后进先出)
原则就行。
下面是栈的一些方法。
push(ele)
:添加一个或多个元素到栈顶。pop()
:移除栈顶的元素并返回移除的元素。peek()
:获取栈顶的元素。isEmpty()
:校验是否为空栈,如果为空就返回true
,否则返回false
。clear()
:清空栈。size()
:返回栈中的元素数量。
向栈添加元素
push
方法负责向栈内添加元素,这个方法只添加元素到栈顶(也就是栈的末尾)。
push(ele) {
this.items.push(ele);
}
这里是数组来保存栈的元素,所以直接使用数组的push
方法。
从栈里移除元素
pop
方法负责移除栈里的元素。栈遵从LIFO
原则,所以移除最后添加的元素。
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 类
首先初始化一个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
(对应 10、11、12、13、14、15)。所以需要对栈中的数组做一个转换才行(baseString += digits[remStack.pop()]
这段代码)。从十一进制开始,字母表中的每个字母都对应一个基数,A
就代表基数11
,B
就代表基数12
,以此类推。
总结
个人感觉操作栈数据结构相对于其他的数据结构来说,还是比较简单的。不管用对象还是数组都可以实现栈数据结构,只要遵从LIFO(后进先出)
原则即可。喜欢的掘友可以点击关注+点赞哦!后面会持续更新其他数据结构,也把自己学的知识分享给大家。当然写作也可以当成复盘。2021年加油!实现自己的目标。