栈的定义
栈是一种遵从 后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。如图:
基于数组的栈
我们使用 ES6 的类来创建一个基于数组的栈:
class Stack {
constructor() {
this.items = []
}
}
接下来,为栈声明一些方法:
push(element):添加一个或多个新元素到栈顶
pop():移除栈顶的元素,同时返回被移除的元素
peek():查看栈顶的元素,不对栈做任何修改
isEmpty():判断栈是否为空,如果栈里没有任何元素就返回 true,否则返回 false
clear():清空栈,即移除栈里的所有元素
size(): 返回栈里的元素个数,该方法和数组的 length 属性类似
print(): 打印栈元素,将栈中的元素以字符串的形式打印出来
下面,我们逐一实现栈的方法:
push 向栈添加元素
push 方法负责往栈里添加新元素,该方法只能添加元素到栈顶
// 往栈里压入一个元素,因为使用数组实现栈,往栈里压入元素就是往数组的末尾添加元素
push(element) {
this.items.push(element);
}
pop 从栈顶移除元素
pop 方法用来移除栈顶的元素,也就是移除的是最后添加进去的元素
// 移除栈顶元素,因为使用数组实现栈,移除栈顶元素就是移除数组的最后一个元素
pop() {
return this.items.pop();
}
peek 查看栈顶元素
peek() {
return this.items[this.items.length - 1]
}
isEmpty 检查栈是否为空
isEmpty 方法检查栈是否为空,如果栈为空就返回 true,否则就放回 false
isEmpty() {
return this.items.length === 0;
}
clear 清空栈
clear 方法用来移除栈里所有的元素,把栈清空
clear() {
this.items = []
}
size 返回栈里的元素个数
size() {
return this.items.length
}
print 打印栈元素
print() {
return this.items.toString();
}
完整代码
class Stack {
constructor() {
this.items = [];
}
push(element) {
this.items.push(element);
}
pop() {
return this.items.pop();
}
peek() {
return this.items[this.items.length - 1];
}
isEmpty() {
return this.items.length === 0;
}
clear() {
this.items = [];
}
size() {
return this.items.length;
}
print() {
return this.items.toString()
}
}
基于对象的栈
除了可以使用数组来实现栈,也可以使用JavaScript对象来存储所有的栈元素,保证它们的顺序并且遵循 LIFO 原则。在使用对象实现是栈中,使用 count 变量来记录栈元素的个数,并且使用 count 变量作为键将栈元素存储在 items 对象中。
class Stack {
constructor() {
// 栈的个数
this.count = 0;
// 存储栈元素
this.items = {};
}
}
push 向栈插入元素
我们将 count 作为键,插入的元素作为值存储在对象中
push(element) {
this.items[this.count] = element;
this.count++;
}
pop 从栈顶移除元素
pop() {
// 栈是否为空,为空则返回 undefined
if (this.isEmpty()) {
return undefined;
}
// count 属性减 1,此时的值即为栈顶元素在对象中的位置
this.count--;
// 根据 count 取出栈顶元素
const result = this.items[this.count];
// 删除栈顶元素
delete this.items[this.count];
// 将栈顶元素返回
return result;
}
peek 查看栈顶元素
peek() {
if (this.isEmpty()) {
return undefined;
}
// count 属性 减 1 后的值即为 栈顶元素的健值
return this.items[this.count - 1];
}
isEmpty 检查栈是否为空
isEmpty() {
// count 变量表示栈的元素个数
return this.count === 0;
}
clear 清空栈
清空栈,只需将 items变量复原为空对象,count 变量复原为 0 即可
clear() {
// 将对象置为空对象
this.items = {};
// 将 count 变量置为 0
this.count = 0;
}
size 返回栈里的元素个数
size() {
return this.count;
}
print 打印栈元素
print() {
// 栈为空,返回空字符串
if (this.isEmpty) {
return '';
}
let objString = `${this.items[0]}`;
// 迭代 栈的键,一直到栈顶,将栈元素拼接成字符串
for (let i = 1; i <this.count; i++) {
objString = `${objString},${this.items[i]}`;
}
return objString;
}
完整代码
class Stack {
constructor() {
// 栈的个数
this.count = 0;
// 存储栈元素
this.items = {};
}
push(element) {
this.items[this.count] = element;
this.count++;
}
pop() {
// 栈是否为空,为空则返回 undefined
if (this.isEmpty()) {
return undefined;
}
// count 属性减 1,此时的值即为栈顶元素在对象中的位置
this.count--;
// 根据 count 取出栈顶元素
const result = this.items[this.count];
// 删除栈顶元素
delete this.items[this.count];
// 将栈顶元素返回
return result;
}
peek() {
if (this.isEmpty()) {
return undefined;
}
// count 属性 减 1 后的值即为栈顶元素的健值
return this.items[this.count - 1];
}
isEmpty() {
// count 变量表示栈的元素个数
return this.count === 0;
}
clear() {
// 将对象置为空对象
this.items = {};
// 将 count 变量置为 0
this.count = 0;
}
size() {
return this.count;
}
print() {
// 栈为空,返回空字符串
if (this.isEmpty()) {
return '';
}
let objString = `${this.items[0]}`;
// 迭代 栈的键,一直到栈顶,将栈元素拼接成字符串
for (let i = 0; i < this.count; i++) {
objString = `${objString}, ${this.items[i]}`;
}
return objString;
}
}
栈的应用
进制转换
在现实生活中,我们主要使用十进制,但在计算科学中,计算机里的所有内容都是用二进制数字表示的 (0 和 1)。要与计算机节流,就必须把十进制转换成二进制。
要把十进制转换成二进制,我们可以将十进制数除以 2,并对商取整,直到商为 0 为止。举个例子,把十进制的数 20 转换成二进制的数字,其过程如下:
思路分析:
将十进制数除以 2,当除法的结果 (商) 不为 0 时,我们会获得一个余数,并将余数放到栈里
然后让结果 (商) 继续除以 2,并将余数入栈,直到结果 (商) 为 0
将栈中的元素出栈,连接成字符串,就是转换后的二进制数,如上图,转换后的二进制位 10100
代码实现
const decimalToBinary = (decNumber) => {
const stack = new Stack();
let number = decNumber;
let remainder;
let binaryNumber = '';
while (number > 0) {
// 获取余数
remainder = Math.floor(number % 2);
// 将余数入栈
stack.push(remainder);
// 将结果 (商) 除以 2
number = Math.floor(number / 2);
}
while (!stack.isEmpty()) {
// 栈元素出栈,拼接成字符串
binaryNumber += stack.pop().toString();
}
return binaryNumber
}
console.log(decimalToBinary(20)) // 10100
console.log(decimalToBinary(10)) // 1010
console.log(decimalToBinary(1000)) // 1111101000
console.log(decimalToBinary(255)) // 11111111
下面,我们将十进制转二进制的算法进行改造,使之能把十进制转换成基数为 2~36 的任意进制。除了把十进制数除以 2 转成 二进制数,还可以传入其他任意进制的基数为参数,然后转换成相应的进制数。
const baseConverter = (decNumber, base) => {
const stack = new Stack();
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let number = decNumber;
let remainder;
let baseString = '';
if (!(base >=2 && base <=36)) {
return '';
}
while(number > 0) {
remainder = Math.floor(number % base);
stack.push(remainder);
number = Math.floor(number / base);
}
while(!stack.isEmpty()) {
baseString += digits[stack.pop()];
}
return baseString;
}
合法括号
下面的字符串中包含小括号,编写一个函数判断字符串中的括号是否合法,所谓合法,就是括号成对出现
sdf(ds(ew(we)rw)rwqq)qwewe 合法
(sd(qwqw)sd(sd)) 合法
()()sd()(sd()fw))( 不合法
思路分析:**
括号存在嵌套关系,也存在并列关系,如果使用数组存储这些括号,然后再一对一对的将括号抵消掉,这似乎是一个可行的办法,可是如何判断一个左括号对应的是哪个右括号呢?在使用数组的角度上思考这个问题,就会陷入一种无从下手的绝望之中。
我们换个思路,在栈的角度上思考这个问题,解题的步骤就非常简单。我们可以使用 for 循环遍历字符串中的每个字符,对每个字符做如下的操作:
- 遇到左括号,就把左括号压入栈中
- 遇到右括号,判断栈是否为空,为空则说明没有左括号与之对应,缺少左括号,字符串括号不合法;如果栈不为空,则把栈顶元素移除,这对括号就抵消掉了
当 for 循环结束之后,如果栈是空的,就说明所有的左右括号都抵消掉了,如果栈里还有元素,则说明缺少右括号,字符串括号不合法
实现代码:
下面,我们使用 栈 解题
const isLeaglBrackets = (str) => {
const stack = new Stack();
for (let i = 0; i < str.length; i++) {
const item = str[i];
if (item === '(') {
// 将 左括号 压入 栈
stack.push(item);
} else if (item === ')') {
// 如果为空,就说明没有左括号与右括号抵消
if (stack.isEmpty()) {
return false;
} else {
// 将左括号弹出栈
stack.pop();
}
}
}
// 栈是否为空,为空则说明字符串中所有的左右括号都抵消掉了,那么字符串中的括号是合法的,否则是不合法的
return stack.size() === 0;
}
console.log(isLeaglBrackets("()()))")); // false
console.log(isLeaglBrackets("sdf(ds(ew(we)rw)rwqq)qwewe")); // true
console.log(isLeaglBrackets("()()sd()(sd()fw))(")); // false
计算逆波兰表达式
逆波兰表达式,也叫后缀表达式,它将复杂表达式转换为可以依靠简单的操作得到计算结果的表达式,例如我们平时写 a + b
,这是中缀表达式,写成后缀表达式就是: ab+
。又比如中缀表达式 (a+b)*c
写成后缀表达式为:ab+c*
现在我们实现一个算法,来计算逆波兰表达式
["4", "13", "5", "/", "+"] 等价于(4 + (13 / 5)) = 6
["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"] 等价于((10 *
(6 / ((9 + 3) * -11))) + 17) + 5
思路分析:
**
[‘4’, ‘13’, ‘5’, ‘/‘, ‘+’] 是一个数组,我们在数组的角度来思考这个问题,遇到 ‘ / ‘ 时,把 13 和 5 从数组里取出来计算,然后把 13 和 5 删除并不计算结果放入到数组中 4 的后面。天哪,太复杂了,太笨拙了,我已经无法继续思考了😱
如果是使用栈来解决这个问题,一切就那么的简单而自然,使用 for 循环遍历数组,对每一个元素做如下操作:
- 如果元素不是
+ - * /
中的某一个,就压入栈中 - 如果元素是
+ - * /
中的某一个,则从栈里连续弹出两个元素,并对这两个元素进行计算,将计算结果压入栈中
for 循环结束之后,栈里只有一个元素,这个元素就是整个表达式的计算结果
实现代码
const cacl_inverse_polish_expression = (exp) => {
const stack = new Stack();
for (let i = 0; i < exp.length; i++) {
const item = exp[i]
if (['+', '-', '*', '/'].indexOf(item) >= 0) {
// 从栈顶弹出两个元素
const value_1 = stack.pop();
const value_2 = stack.pop();
// 拼成表达式
const expStr = value_2 + item + value_1;
// 计算结果并取整
const result = parseInt(eval(expStr));
// 将计算结果压入栈
stack.push(result.toString());
} else {
stack.push(item);
}
}
// 表达式如果是正确的,最终栈里只有一个元素,且就是表达式计算的结果
return stack.pop();
}
const exp_1 = ["4", "13", "5", "/", "+"];
const exp_2 = ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"];
console.log(cacl_inverse_polish_expression(exp_1)); // 输出结果:6
console.log(cacl_inverse_polish_expression(exp_2)); // 输出结果:22
中缀表达式转后缀表达式
中缀表达式是人们常用的算术表示方法,例如 3 + 4
、8 + 4 * 6
等都是中缀表达式,操作符以中缀的形式处于操作数中间。
后缀表达式也称作逆波兰表达式,通常是将运算符写在操作数之后,如将中缀表达式 3 + 4
写成后缀表达式就是 34+
。
下面,我们实现一个算法,将中缀表达式转换成后缀表达式
输⼊:["12","+", "3"]
输出:["12","3","+"]
输⼊:["(","1","+","(","4","+","5","+","3",")","-","3",")","+","(","9","+","8",")"]
输出:[ '1', '4', '5', '+', '3', '+', '+', '3', '-', '9', '8', '+', '+' ]
输⼊:['(', '1', '+', '(', '4', '+', '5', '+', '3', ')', '/', '4', '-', '3', ')', '+', '(', '6', '+', '8', ')', '*', '3']
输出:['1', '4', '5', '+', '3', '+', '4', '/', '+', '3', '-', '6', '8', '+', '3', '*', '+']
思路分析:
定义一个数组 postfixList ,用于存储后缀表达式,遍历中缀表达式,对每一个遍历到的元素做如下处理:
如果是数字,直接放入 postfixList 中
- 遇到左括号则入栈
- 遇到右括号,把栈顶元素弹出并放入到 postfixList 中,直到遇到左括号,最后弹出左括号
- 遇到运算符,把栈顶的运算符弹出,直到栈顶的运算符优先级低于当前运算符,把弹出的运算符加入到 postfixList 中,当前的运算符入栈
- for 循环结束后,栈里可能还有元素,都弹出放入到 postfixList 中
实现代码
// 定义运算符的优先级
const priorityMap = {
"+": 1,
"-": 1,
"*": 2,
"/": 2
}
const infixExpToPostfixExp = (exp) => {
const stack = new Stack();
const postfixList = [];
for (let i = 0; i < exp.length; i++) {
const item = exp[i];
if (!isNaN(item)) {
// 如果是数字,直接放入到 postfixList 中
postfixList.push(item);
} else if (item === "(") {
// 遇到左括号入栈
stack.push(item);
} else if (item === ")") {
// 遇到右括号,把栈顶元素弹出,直到遇到左括号
while(stack.peek() !== "(") {
postfixList.push(stack.pop());
}
// 左括号出栈
stack.pop();
} else {
// 遇到运算符,把栈顶的运算符弹出,直到栈顶的运算符优先级低于当前运算符
while(!stack.isEmpty()
&& ['+', '-', '*', '/'].indexOf(stack.peek()) >=0
&& priorityMap[stack.peek() >= priorityMap[item]]) {
// 把弹出的运算符加入到 postfixList 中
postfixList.push(stack.pop());
}
// 当前运算符入栈
stack.push(item);
}
}
// for 循环结束后,栈里可能还有元素,都弹出放入到 postfixList 中
while(!stack.isEmpty()) {
postfixList.push(stack.pop())
}
return postfixList
}
// 12+3
console.log(infixExpToPostfixExp(["12","+", "3"]))
// 2-3+2
console.log(infixExpToPostfixExp(["2","-", "3", "+", "2"]))
// (1+(4+5+3)-3)+(9+8)
var exp = ["(","1","+","(","4","+","5","+","3",")","-","3",")","+","(","9","+","8",")"];
console.log(infixExpToPostfixExp(exp))
// (1+(4+5+3)/4-3)+(6+8)*3
var exp = ['(', '1', '+', '(', '4', '+', '5', '+', '3', ')', '/', '4', '-', '3', ')', '+', '(', '6', '+', '8', ')', '*', '3']
console.log(infixExpToPostfixExp(exp))
console.log(infixExpToPostfixExp(["12","+", "3","*", "5"]))
console.log(infixExpToPostfixExp(["12","*", "3","+", "5"]))
参考资料:
书籍:《JavaScript数据结构与算法》