你们好,我是前端图图。今天就来聊聊栈的数据结构,由于最近在学数据结构和算法,我也把写文章看成一次复盘。下面废话很少说,开始吧!前端
栈是一种后进先出
原则的有序集合(就是后面进去的先出来的意思)。添加或待删除的元素都保存在栈的同一端,叫做栈顶(栈的末尾),另外一端叫做栈底。在栈里面,新元素都在栈顶,旧的元素都在栈底。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年加油!实现本身的目标。