栈的定义

栈是一种遵从 后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。如图:
image.png

基于数组的栈

我们使用 ES6 的类来创建一个基于数组的栈:

  1. class Stack {
  2. constructor() {
  3. this.items = []
  4. }
  5. }

接下来,为栈声明一些方法:

push(element):添加一个或多个新元素到栈顶
pop():移除栈顶的元素,同时返回被移除的元素
peek():查看栈顶的元素,不对栈做任何修改
isEmpty():判断栈是否为空,如果栈里没有任何元素就返回 true,否则返回 false
clear():清空栈,即移除栈里的所有元素
size(): 返回栈里的元素个数,该方法和数组的 length 属性类似
print(): 打印栈元素,将栈中的元素以字符串的形式打印出来

下面,我们逐一实现栈的方法:

push 向栈添加元素

push 方法负责往栈里添加新元素,该方法只能添加元素到栈顶

  1. // 往栈里压入一个元素,因为使用数组实现栈,往栈里压入元素就是往数组的末尾添加元素
  2. push(element) {
  3. this.items.push(element);
  4. }

pop 从栈顶移除元素

pop 方法用来移除栈顶的元素,也就是移除的是最后添加进去的元素

  1. // 移除栈顶元素,因为使用数组实现栈,移除栈顶元素就是移除数组的最后一个元素
  2. pop() {
  3. return this.items.pop();
  4. }

peek 查看栈顶元素

  1. peek() {
  2. return this.items[this.items.length - 1]
  3. }

isEmpty 检查栈是否为空

isEmpty 方法检查栈是否为空,如果栈为空就返回 true,否则就放回 false

  1. isEmpty() {
  2. return this.items.length === 0;
  3. }

clear 清空栈

clear 方法用来移除栈里所有的元素,把栈清空

  1. clear() {
  2. this.items = []
  3. }

size 返回栈里的元素个数

  1. size() {
  2. return this.items.length
  3. }

print 打印栈元素

  1. print() {
  2. return this.items.toString();
  3. }

完整代码

  1. class Stack {
  2. constructor() {
  3. this.items = [];
  4. }
  5. push(element) {
  6. this.items.push(element);
  7. }
  8. pop() {
  9. return this.items.pop();
  10. }
  11. peek() {
  12. return this.items[this.items.length - 1];
  13. }
  14. isEmpty() {
  15. return this.items.length === 0;
  16. }
  17. clear() {
  18. this.items = [];
  19. }
  20. size() {
  21. return this.items.length;
  22. }
  23. print() {
  24. return this.items.toString()
  25. }
  26. }

基于对象的栈

除了可以使用数组来实现栈,也可以使用JavaScript对象来存储所有的栈元素,保证它们的顺序并且遵循 LIFO 原则。在使用对象实现是栈中,使用 count 变量来记录栈元素的个数,并且使用 count 变量作为键将栈元素存储在 items 对象中。

  1. class Stack {
  2. constructor() {
  3. // 栈的个数
  4. this.count = 0;
  5. // 存储栈元素
  6. this.items = {};
  7. }
  8. }

push 向栈插入元素

我们将 count 作为键,插入的元素作为值存储在对象中

  1. push(element) {
  2. this.items[this.count] = element;
  3. this.count++;
  4. }

pop 从栈顶移除元素

  1. pop() {
  2. // 栈是否为空,为空则返回 undefined
  3. if (this.isEmpty()) {
  4. return undefined;
  5. }
  6. // count 属性减 1,此时的值即为栈顶元素在对象中的位置
  7. this.count--;
  8. // 根据 count 取出栈顶元素
  9. const result = this.items[this.count];
  10. // 删除栈顶元素
  11. delete this.items[this.count];
  12. // 将栈顶元素返回
  13. return result;
  14. }

peek 查看栈顶元素

  1. peek() {
  2. if (this.isEmpty()) {
  3. return undefined;
  4. }
  5. // count 属性 减 1 后的值即为 栈顶元素的健值
  6. return this.items[this.count - 1];
  7. }

isEmpty 检查栈是否为空

  1. isEmpty() {
  2. // count 变量表示栈的元素个数
  3. return this.count === 0;
  4. }

clear 清空栈

清空栈,只需将 items变量复原为空对象,count 变量复原为 0 即可

  1. clear() {
  2. // 将对象置为空对象
  3. this.items = {};
  4. // 将 count 变量置为 0
  5. this.count = 0;
  6. }

size 返回栈里的元素个数

  1. size() {
  2. return this.count;
  3. }

print 打印栈元素

  1. print() {
  2. // 栈为空,返回空字符串
  3. if (this.isEmpty) {
  4. return '';
  5. }
  6. let objString = `${this.items[0]}`;
  7. // 迭代 栈的键,一直到栈顶,将栈元素拼接成字符串
  8. for (let i = 1; i <this.count; i++) {
  9. objString = `${objString},${this.items[i]}`;
  10. }
  11. return objString;
  12. }

完整代码

  1. class Stack {
  2. constructor() {
  3. // 栈的个数
  4. this.count = 0;
  5. // 存储栈元素
  6. this.items = {};
  7. }
  8. push(element) {
  9. this.items[this.count] = element;
  10. this.count++;
  11. }
  12. pop() {
  13. // 栈是否为空,为空则返回 undefined
  14. if (this.isEmpty()) {
  15. return undefined;
  16. }
  17. // count 属性减 1,此时的值即为栈顶元素在对象中的位置
  18. this.count--;
  19. // 根据 count 取出栈顶元素
  20. const result = this.items[this.count];
  21. // 删除栈顶元素
  22. delete this.items[this.count];
  23. // 将栈顶元素返回
  24. return result;
  25. }
  26. peek() {
  27. if (this.isEmpty()) {
  28. return undefined;
  29. }
  30. // count 属性 减 1 后的值即为栈顶元素的健值
  31. return this.items[this.count - 1];
  32. }
  33. isEmpty() {
  34. // count 变量表示栈的元素个数
  35. return this.count === 0;
  36. }
  37. clear() {
  38. // 将对象置为空对象
  39. this.items = {};
  40. // 将 count 变量置为 0
  41. this.count = 0;
  42. }
  43. size() {
  44. return this.count;
  45. }
  46. print() {
  47. // 栈为空,返回空字符串
  48. if (this.isEmpty()) {
  49. return '';
  50. }
  51. let objString = `${this.items[0]}`;
  52. // 迭代 栈的键,一直到栈顶,将栈元素拼接成字符串
  53. for (let i = 0; i < this.count; i++) {
  54. objString = `${objString}, ${this.items[i]}`;
  55. }
  56. return objString;
  57. }
  58. }

栈的应用

进制转换

在现实生活中,我们主要使用十进制,但在计算科学中,计算机里的所有内容都是用二进制数字表示的 (0 和 1)。要与计算机节流,就必须把十进制转换成二进制。

要把十进制转换成二进制,我们可以将十进制数除以 2,并对商取整,直到商为 0 为止。举个例子,把十进制的数 20 转换成二进制的数字,其过程如下:
image.png

思路分析:
将十进制数除以 2,当除法的结果 (商) 不为 0 时,我们会获得一个余数,并将余数放到栈里
然后让结果 (商) 继续除以 2,并将余数入栈,直到结果 (商) 为 0
将栈中的元素出栈,连接成字符串,就是转换后的二进制数,如上图,转换后的二进制位 10100

代码实现

  1. const decimalToBinary = (decNumber) => {
  2. const stack = new Stack();
  3. let number = decNumber;
  4. let remainder;
  5. let binaryNumber = '';
  6. while (number > 0) {
  7. // 获取余数
  8. remainder = Math.floor(number % 2);
  9. // 将余数入栈
  10. stack.push(remainder);
  11. // 将结果 (商) 除以 2
  12. number = Math.floor(number / 2);
  13. }
  14. while (!stack.isEmpty()) {
  15. // 栈元素出栈,拼接成字符串
  16. binaryNumber += stack.pop().toString();
  17. }
  18. return binaryNumber
  19. }
  20. console.log(decimalToBinary(20)) // 10100
  21. console.log(decimalToBinary(10)) // 1010
  22. console.log(decimalToBinary(1000)) // 1111101000
  23. console.log(decimalToBinary(255)) // 11111111

下面,我们将十进制转二进制的算法进行改造,使之能把十进制转换成基数为 2~36 的任意进制。除了把十进制数除以 2 转成 二进制数,还可以传入其他任意进制的基数为参数,然后转换成相应的进制数。

  1. const baseConverter = (decNumber, base) => {
  2. const stack = new Stack();
  3. const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  4. let number = decNumber;
  5. let remainder;
  6. let baseString = '';
  7. if (!(base >=2 && base <=36)) {
  8. return '';
  9. }
  10. while(number > 0) {
  11. remainder = Math.floor(number % base);
  12. stack.push(remainder);
  13. number = Math.floor(number / base);
  14. }
  15. while(!stack.isEmpty()) {
  16. baseString += digits[stack.pop()];
  17. }
  18. return baseString;
  19. }

合法括号

下面的字符串中包含小括号,编写一个函数判断字符串中的括号是否合法,所谓合法,就是括号成对出现

  1. sdf(ds(ew(we)rw)rwqq)qwewe 合法
  2. (sd(qwqw)sd(sd)) 合法
  3. ()()sd()(sd()fw))( 不合法


思路分析:**

括号存在嵌套关系,也存在并列关系,如果使用数组存储这些括号,然后再一对一对的将括号抵消掉,这似乎是一个可行的办法,可是如何判断一个左括号对应的是哪个右括号呢?在使用数组的角度上思考这个问题,就会陷入一种无从下手的绝望之中。

我们换个思路,在栈的角度上思考这个问题,解题的步骤就非常简单。我们可以使用 for 循环遍历字符串中的每个字符,对每个字符做如下的操作:

  1. 遇到左括号,就把左括号压入栈中
  2. 遇到右括号,判断栈是否为空,为空则说明没有左括号与之对应,缺少左括号,字符串括号不合法;如果栈不为空,则把栈顶元素移除,这对括号就抵消掉了

当 for 循环结束之后,如果栈是空的,就说明所有的左右括号都抵消掉了,如果栈里还有元素,则说明缺少右括号,字符串括号不合法

实现代码:
下面,我们使用 栈 解题

  1. const isLeaglBrackets = (str) => {
  2. const stack = new Stack();
  3. for (let i = 0; i < str.length; i++) {
  4. const item = str[i];
  5. if (item === '(') {
  6. // 将 左括号 压入 栈
  7. stack.push(item);
  8. } else if (item === ')') {
  9. // 如果为空,就说明没有左括号与右括号抵消
  10. if (stack.isEmpty()) {
  11. return false;
  12. } else {
  13. // 将左括号弹出栈
  14. stack.pop();
  15. }
  16. }
  17. }
  18. // 栈是否为空,为空则说明字符串中所有的左右括号都抵消掉了,那么字符串中的括号是合法的,否则是不合法的
  19. return stack.size() === 0;
  20. }
  21. console.log(isLeaglBrackets("()()))")); // false
  22. console.log(isLeaglBrackets("sdf(ds(ew(we)rw)rwqq)qwewe")); // true
  23. console.log(isLeaglBrackets("()()sd()(sd()fw))(")); // false

计算逆波兰表达式

逆波兰表达式,也叫后缀表达式,它将复杂表达式转换为可以依靠简单的操作得到计算结果的表达式,例如我们平时写 a + b,这是中缀表达式,写成后缀表达式就是: ab+。又比如中缀表达式 (a+b)*c 写成后缀表达式为:ab+c*

现在我们实现一个算法,来计算逆波兰表达式

  1. ["4", "13", "5", "/", "+"] 等价于(4 + (13 / 5)) = 6
  2. ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"] 等价于((10 *
  3. (6 / ((9 + 3) * -11))) + 17) + 5

思路分析:
**
[‘4’, ‘13’, ‘5’, ‘/‘, ‘+’] 是一个数组,我们在数组的角度来思考这个问题,遇到 ‘ / ‘ 时,把 13 和 5 从数组里取出来计算,然后把 13 和 5 删除并不计算结果放入到数组中 4 的后面。天哪,太复杂了,太笨拙了,我已经无法继续思考了😱

如果是使用栈来解决这个问题,一切就那么的简单而自然,使用 for 循环遍历数组,对每一个元素做如下操作:

  1. 如果元素不是 + - * / 中的某一个,就压入栈中
  2. 如果元素是 + - * / 中的某一个,则从栈里连续弹出两个元素,并对这两个元素进行计算,将计算结果压入栈中

for 循环结束之后,栈里只有一个元素,这个元素就是整个表达式的计算结果

实现代码

  1. const cacl_inverse_polish_expression = (exp) => {
  2. const stack = new Stack();
  3. for (let i = 0; i < exp.length; i++) {
  4. const item = exp[i]
  5. if (['+', '-', '*', '/'].indexOf(item) >= 0) {
  6. // 从栈顶弹出两个元素
  7. const value_1 = stack.pop();
  8. const value_2 = stack.pop();
  9. // 拼成表达式
  10. const expStr = value_2 + item + value_1;
  11. // 计算结果并取整
  12. const result = parseInt(eval(expStr));
  13. // 将计算结果压入栈
  14. stack.push(result.toString());
  15. } else {
  16. stack.push(item);
  17. }
  18. }
  19. // 表达式如果是正确的,最终栈里只有一个元素,且就是表达式计算的结果
  20. return stack.pop();
  21. }
  22. const exp_1 = ["4", "13", "5", "/", "+"];
  23. const exp_2 = ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"];
  24. console.log(cacl_inverse_polish_expression(exp_1)); // 输出结果:6
  25. console.log(cacl_inverse_polish_expression(exp_2)); // 输出结果:22

中缀表达式转后缀表达式

中缀表达式是人们常用的算术表示方法,例如 3 + 48 + 4 * 6 等都是中缀表达式,操作符以中缀的形式处于操作数中间。

后缀表达式也称作逆波兰表达式,通常是将运算符写在操作数之后,如将中缀表达式 3 + 4 写成后缀表达式就是 34+

下面,我们实现一个算法,将中缀表达式转换成后缀表达式

  1. 输⼊:["12","+", "3"]
  2. 输出:["12","3","+"]
  3. 输⼊:["(","1","+","(","4","+","5","+","3",")","-","3",")","+","(","9","+","8",")"]
  4. 输出:[ '1', '4', '5', '+', '3', '+', '+', '3', '-', '9', '8', '+', '+' ]
  5. 输⼊:['(', '1', '+', '(', '4', '+', '5', '+', '3', ')', '/', '4', '-', '3', ')', '+', '(', '6', '+', '8', ')', '*', '3']
  6. 输出:['1', '4', '5', '+', '3', '+', '4', '/', '+', '3', '-', '6', '8', '+', '3', '*', '+']

思路分析:
定义一个数组 postfixList ,用于存储后缀表达式,遍历中缀表达式,对每一个遍历到的元素做如下处理:
如果是数字,直接放入 postfixList 中

  1. 遇到左括号则入栈
  2. 遇到右括号,把栈顶元素弹出并放入到 postfixList 中,直到遇到左括号,最后弹出左括号
  3. 遇到运算符,把栈顶的运算符弹出,直到栈顶的运算符优先级低于当前运算符,把弹出的运算符加入到 postfixList 中,当前的运算符入栈
  4. for 循环结束后,栈里可能还有元素,都弹出放入到 postfixList 中

实现代码

  1. // 定义运算符的优先级
  2. const priorityMap = {
  3. "+": 1,
  4. "-": 1,
  5. "*": 2,
  6. "/": 2
  7. }
  8. const infixExpToPostfixExp = (exp) => {
  9. const stack = new Stack();
  10. const postfixList = [];
  11. for (let i = 0; i < exp.length; i++) {
  12. const item = exp[i];
  13. if (!isNaN(item)) {
  14. // 如果是数字,直接放入到 postfixList 中
  15. postfixList.push(item);
  16. } else if (item === "(") {
  17. // 遇到左括号入栈
  18. stack.push(item);
  19. } else if (item === ")") {
  20. // 遇到右括号,把栈顶元素弹出,直到遇到左括号
  21. while(stack.peek() !== "(") {
  22. postfixList.push(stack.pop());
  23. }
  24. // 左括号出栈
  25. stack.pop();
  26. } else {
  27. // 遇到运算符,把栈顶的运算符弹出,直到栈顶的运算符优先级低于当前运算符
  28. while(!stack.isEmpty()
  29. && ['+', '-', '*', '/'].indexOf(stack.peek()) >=0
  30. && priorityMap[stack.peek() >= priorityMap[item]]) {
  31. // 把弹出的运算符加入到 postfixList 中
  32. postfixList.push(stack.pop());
  33. }
  34. // 当前运算符入栈
  35. stack.push(item);
  36. }
  37. }
  38. // for 循环结束后,栈里可能还有元素,都弹出放入到 postfixList 中
  39. while(!stack.isEmpty()) {
  40. postfixList.push(stack.pop())
  41. }
  42. return postfixList
  43. }
  44. // 12+3
  45. console.log(infixExpToPostfixExp(["12","+", "3"]))
  46. // 2-3+2
  47. console.log(infixExpToPostfixExp(["2","-", "3", "+", "2"]))
  48. // (1+(4+5+3)-3)+(9+8)
  49. var exp = ["(","1","+","(","4","+","5","+","3",")","-","3",")","+","(","9","+","8",")"];
  50. console.log(infixExpToPostfixExp(exp))
  51. // (1+(4+5+3)/4-3)+(6+8)*3
  52. var exp = ['(', '1', '+', '(', '4', '+', '5', '+', '3', ')', '/', '4', '-', '3', ')', '+', '(', '6', '+', '8', ')', '*', '3']
  53. console.log(infixExpToPostfixExp(exp))
  54. console.log(infixExpToPostfixExp(["12","+", "3","*", "5"]))
  55. console.log(infixExpToPostfixExp(["12","*", "3","+", "5"]))

参考资料:
书籍:《JavaScript数据结构与算法》