[路飞]前端算法——数据结构篇(一、栈): 初识栈

381 阅读5分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

前言

前端算法系列是我对算法学习的一个记录, 主要从常见算法数据结构算法思维常用技巧几个方面剖析学习算法知识, 通过LeetCode平台实现刻意练习, 通过掘金和B站的输出来实践费曼学习法, 我会在后续不断更新优质内容并同步更新到掘金、B站和Github, 以记录学习算法的完整过程, 欢迎大家多多交流点赞收藏, 让我们共同进步, daydayup👊

目录地址:目录篇

相关代码地址: Github

相关视频地址: 哔哩哔哩-百日算法系列

总结

栈是一种线性逻辑结构,只支持入栈和出栈操作,遵循后进先出的原则(FILO)。栈既可以通过数组实现,也可以通过链表来实现,不管基于数组还是链表,入栈、出栈的时间复杂度都为 O(1)。

一、什么是栈

栈、就好比是一个弹夹

每次向弹夹中添加一枚子弹的过程叫 入栈

每次将弹夹中最上面的子弹发射的过程叫 出栈

当我们的弹夹已经填满时、我们说当前栈 溢出

43d931c71a7d9b7e1cdf32be82a52dec.jpeg

二、栈的特性

  • 只允许一端操作
  • 后进先出的原则

三、栈的优缺点

顺序栈:

优点: 
    实现简单
    查询的速度快, 密度高

缺点: 
    一般需要固定栈的空间大小, 容量固定, 容易溢出

链表栈:

优点: 
    存取的速度快
    可以利用碎片空间, 栈空间容量可变
    
缺点: 
    // 对与数据的大小和生命周期有一定的要求

四、栈的应用场景

当你看到这里时, 请停顿下来问自己, 为什么要用栈?

  • 括号匹配类问题
  • 数学表达式计算
  • 函数调用(参考JS函数执行)
  • 前进后退类型问题

五、栈的前端实现

实现前

实现前我们需要知道一个栈的结构应该具备哪些功能, 一般来说一个栈应该具备以下功能:

  1. 入栈
  2. 出栈
  3. 获取栈顶元素
  4. 获取栈的大小
  5. 判断是否为空
  6. 将栈内清空

当我们面对特殊的业务场景时我们还可以再添加额外的功能, 例如

  1. 设定 栈的空间大小
  2. 判断 栈是否溢出

数组版

module.exports.Stack = function Stack(size) {
  let items = new Array(size)

  // 入栈
  this.push = function (item) {
    if(this.size === size) return
    items.push(item)
  }

  // 出栈
  this.pop = function () {
    return items.pop()
  }

  // 栈顶元素
  this.top = function () {
    if (!items.length) {
      return null
    }
    return items[items.length - 1]
  }

  // 栈的大小
  this.size = function () {
    return items.length
  }

  // 是否空栈
  this.isEmpty = function () {
    return items.length === 0
  }

  // 清空栈
  this.clear = function () {
    items = []
  }
}

链表版

友链: 前端进阶指南——数据结构篇(一): 链表(TODO)

用链表实现的栈称为链式栈,相较于顺序栈,链式栈不需要占用连续的内存空间,也更加方便动态扩展,比较适合入栈和出栈操作,但是顺序栈更加方便查找。

// 链表版的栈实际就是一个单向链表, head表示栈顶
function List(val = 0, next = null) {
    this.val = val
    this.next = next
}
module.exports.Stack = function Stack() {
  let size = 0
  let head = null

  // 入栈
  this.push = function (item) {
    head = new List(item, head)
    size = size + 1
  }

  // 出栈
  this.pop = function () {
    if (this.isEmpty()) return
    let val = head.val
    head = head.next
    size = size - 1
    return val
  }

  // 栈顶元素
  this.top = function () {
    if (this.isEmpty()) return -1
    return head.val
  }

  // 栈的大小
  this.size = function () {
    return size
  }

  // 是否空栈
  this.isEmpty = function () {
    return size === 0
  }

  // 清空栈
  this.clear = function () {
    head = null
  }
}

实现后

我们来回答一下上面的问题: 我们为什么要用栈?

基本上使用栈能解决的问题用数组和链表都可以解决、那么我们为什么要使用栈来解决

一方面, 数组和链表暴露了许多其他的功能、在操作的安全性上有不足

另一方面, 利用系统栈配合递归的思想能帮我们以一种简单的方式解决复杂问题

还有就是, 利用栈的后入先出特性帮我们解决特定的问题, 如浏览器的前进后退问题

六、栈相关的经典问题

1、表达式计算类

/**
 * [2, 4, 15, 5, '/', '+', '+']
 * 题目: 计算后缀表达式 (逆波兰表达式) 的结果:
 *
 * 解题思路:
 *  1、循环遍历、数字入栈、操作符不入栈
 *  2、遇到符号取出栈顶的两个数字、先出的在(运算符)右侧、后弹出的在(运算符)左侧、
 *  3、组合成字符串、使用eval运算、并将运算结果压入栈中
 *  4、继续向后遍历、直到结束、返回栈顶内容
 */

function calc_exp(exp) {
  let stack = new Stack()
  for (let index = 0; index < exp.length; index++) {
    const item = exp[index]
    if (['+', '-', '*', '/'].includes(item)) {
      let b = stack.pop()
      let a = stack.pop()
      let res = parseInt(eval(`${a} ${item} ${b}`))
      stack.push(res)
    } else {
      stack.push(item)
    }
  }
  return stack.top()
}

// 9
console.log(calc_exp([2, 4, 15, 5, '/', '+', '+']))

2、括号匹配

/**
 * ()()(()dak(222)dao)(
 * 题目: 检测下面字符串中的括号是否一一对应
 *
 * 解题思路:
 *  1、遇到 ( 入栈, 遇到 ) 出栈
 *  2、循环结束判断栈的长度是否为 0
 */
function calc_str(str) {
  let stack = new Stack()
  for (let index = 0; index < str.length; index++) {
    const element = str[index]
    if (element == '(') {
      stack.push(element)
    }
    if (element == ')') {
      stack.pop()
    }
  }
  return stack.size() === 0
}

// false
console.log(calc_str('()()(()dak(222)dao)('))

七、刻意练习

// leetcode: https://leetcode-cn.com/

20. 有效的括号

145. 二叉树的后序遍历

227. 基本计算器 II

331. 验证二叉树的前序序列化

844. 比较含退格的字符串

946. 验证栈序列

1021. 删除最外层的括号

1124. 表现良好的最长时间段

1249. 移除无效的括号

八、深度(TODO)

// 系统栈、JS函数执行栈
// 递归、二叉树遍历
// ...