栈数据结构

栈是一种遵从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。一个非常贴切的例子就是一摞叠在一起的盘子。放盘子的时候,都是从下往上一个一个放;取的时候,也是从上往下一个一个地依次取,不能从中间任意抽出。
image.png
栈的基本操作:入栈和出栈,只允许在一端插入和删除数据,栈是一种“操作受限”的线性表数据结构
从功能上来说,数组或链表确实可以替代栈,但是栈这种特定的数据结构针对特定的场景:当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,就应该首选“栈”这种数据结构

JS实现一个“栈”

栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈叫作顺序栈,用链表实现的栈叫作链式栈

数组实现顺序栈

通过push和pop方法,就能用数组来模拟栈;
注意:如果入栈是放在数组最后,那么出栈也是先取数组最后的;如果入栈是放在数组最前,那么出栈也要取数组最前的;必须要在栈的一端进行操作

  1. // 定义栈类(纯粹是用数组的属性和方法来模拟栈)
  2. class StackArray {
  3. constructor() {
  4. this.items = []
  5. }
  6. // 入栈
  7. push(item) {
  8. this.items.push(item) // 放在最后
  9. }
  10. // 出栈(移除栈顶元素,同时返回被删除的元素)
  11. pop() {
  12. return this.items.pop();
  13. }
  14. // 返回栈顶元素值,不对栈做任何修改
  15. peek() {
  16. return this.items[this.items.length - 1];
  17. }
  18. // 判断栈是否为空
  19. isEmpty() {
  20. return this.items.length === 0
  21. }
  22. // 返回给定对象的位置
  23. search(item) {
  24. let index = this.items.lastIndexOf(item)
  25. if (index >= 0) {
  26. return this.size() - index
  27. } else {
  28. return -1
  29. }
  30. }
  31. // 获取栈元素个数
  32. size() {
  33. return this.items.length
  34. }
  35. // 清空栈
  36. clear() {
  37. this.items = []
  38. }
  39. toString() {
  40. return this.items.toString();
  41. }
  42. }
  43. // 测试
  44. const stack = new StackArray()
  45. console.log(stack.isEmpty()) // true
  46. stack.push('nodeA')
  47. stack.push('nodeB')
  48. stack.push('nodeC')
  49. while (!stack.isEmpty()) {
  50. console.log(stack)
  51. stack.pop()
  52. }
  53. // Stack { items: [ 'nodeA', 'nodeB', 'nodeC' ] }
  54. // Stack { items: [ 'nodeA', 'nodeB' ] }
  55. // Stack { items: [ 'nodeA' ] }

利用数组实现的栈,内部的数组items并不是私有属性,不受保护,可以利用WeakMap来实现私有属性:

  1. const items = new WeakMap() // 声明一个WeakMap类型的变量items
  2. class Stack {
  3. constructor() {
  4. items.set(this, []) // 以this(Stack类自己的引用)为键,把代表栈的数组存入items
  5. // ⭐items在Stack类里是真正的私有属性。采用这种方法,代码的可读性不强,而且在扩展该类时无法继承私有属性
  6. }
  7. push(element) {
  8. const s = items.get(this) // 从WeakMap中取出值,即以this为键从items中取值。
  9. s.push(element)
  10. }
  11. pop() {
  12. const s = items.get(this)
  13. const r = s.pop()
  14. return r
  15. }
  16. // 其他方法
  17. }

对象实现栈

  1. class Stack {
  2. constructor() {
  3. this.count = 0 // 记录栈的大小
  4. this.items = {}
  5. }
  6. // 数组实现的栈可以一次添加多个元素。但是对象,只允许一次插入一个元素
  7. push(element) {
  8. this.items[this.count] = element // 放在最后
  9. this.count++ // 插入元素后,递增count变量
  10. }
  11. // 对象,通过delete来删除
  12. pop() {
  13. if (this.isEmpty()) {
  14. return undefined
  15. }
  16. this.count--
  17. const result = this.items[this.count]
  18. delete this.items[this.count] // 删除最后
  19. return result
  20. }
  21. peek() {
  22. if (this.isEmpty()) {
  23. return undefined
  24. }
  25. return this.items[this.count - 1] // 获取栈顶元素
  26. }
  27. isEmpty() {
  28. return this.count === 0
  29. }
  30. size() {
  31. return this.count
  32. }
  33. clear() {
  34. /* while (!this.isEmpty()) {
  35. this.pop();
  36. } */
  37. this.items = {}
  38. this.count = 0
  39. }
  40. toString() {
  41. if (this.isEmpty()) {
  42. return ''
  43. }
  44. let objString = `${this.items[0]}`
  45. for (let i = 1; i < this.count; i++) {
  46. objString = `${objString},${this.items[i]}`
  47. }
  48. return objString
  49. }
  50. }
  51. // 测试
  52. const stack = new Stack()
  53. stack.push(5)
  54. stack.push(8)
  55. console.log(stack) // Stack { count: 2, items: { '0': 5, '1': 8 } }

栈操作的复杂度

空间复杂度:不管是顺序栈还是链式栈,存储数据只需要一个大小为 n 的数组就够了。在入栈和出栈过程中,只需要一两个临时变量存储空间,所以空间复杂度是 O(1)。注意:存储数据需要一个大小为 n 的数组,并不是说空间复杂度就是 O(n)。因为,这 n 个空间是必须的,无法省掉。所以说空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要额外的存储空间。
时间复杂度:不管是顺序栈还是链式栈,入栈、出栈只涉及栈顶数据的操作,所以时间复杂度都是 O(1)。

栈的应用

浏览器前进后退

使用两个栈X 和 Y,把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当点击前进按钮时,依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。比如顺序查看了 a,b,c 三个页面,就依次把 a,b,c 压入栈,此时两个栈的数据如下图:
image.png
当点击后退按钮,从页面 c 后退到页面 a 之后,就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y:
image.png
这时再点击前进按钮回到 b 页面,就把 b 再从栈 Y 中出栈,放入栈 X 中:
image.png
这时如果从页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:
image.png

函数调用栈

操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧压入栈,当被调用函数执行完成之后,将这个函数对应的栈帧出栈

int main() {
   int a = 1; 
   int ret = 0;
   int res = 0;
   ret = add(3, 5);
   res = a + ret;
   printf("%d", res);
   reuturn 0;
}

int add(int x, int y) {
   int sum = 0;
   sum = x + y;
   return sum;
}

image.png

表达式求值

实现一个四则运算表达式求值的功能:3+5*8-6
编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。从左向右遍历表达式,当遇到数字,直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。
如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。
image.png

括号匹配

场景:假设表达式中只包含三种括号,圆括号 ()、方括号 [ ] 和花括号{ },并且它们可以任意嵌套。比如,{ [ { } ] }或 [ { ( ) } ( [ ] ) ] 等都为合法格式,而{ [ } ( ) ] 或 [ ( { ) ] 为不合法的格式。那如何检查它是否合法?
解决:可以用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。

十进制转换其它进制

要把十进制转化成二进制,可以将该十进制数除以2(二进制是满二进一)并对商取整,直到结果是0为止
image.png

function decimalToBinary(decNumber) {
  const remStack = new Stack()
  let number = decNumber
  let rem
  let binaryString = ''
  while (number > 0) { // 先循环将余数推入栈中
    rem = Math.floor(number % 2)
    remStack.push(rem)
    number = Math.floor(number / 2)
  }
  while (!remStack.isEmpty()) {
    binaryString += remStack.pop().toString()
  }
  return binaryString
}

// 测试
console.log(decimalToBinary(233)) // 11101001
function baseConverter(decNumber, base) {
  const remStack = new Stack()
  const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' // 三十六进制以内转换表,从十一进制开始,字母表中的每个字母将表示相应的基数
  let number = decNumber
  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.isEmpty()) {
    baseString += digits[remStack.pop()] // 弹出表中对应的基数,主要是针对十一进制以上要用字母表示
  }
  return baseString
}

// 测试
console.log(baseConverter(100345, 8)) // 303771
console.log(baseConverter(100345, 16)) // 187F9