JS--数据结构与算法--栈

162 阅读5分钟

为什么学习数据结构

每个编程语言都有自己擅长的领域和使用范围,但每个语言真正相通的点在于数据结构和算法

数据结构和算法是脱离编程语言而存在的,不同的语言有不同的版本,但内在的逻辑却不会变化,编程思想不会变化。

什么是栈

栈是一种线性表,仅能够在栈顶操作,有着后进先出(Last In First Out)的特性。

对于栈,必须牢记一点:

  • 栈的操作,只能在栈顶操作

栈示意图

生活中,有一个贴切的例子,羽毛球筒就是很形象的栈结构。每次去羽毛球,你只能从顶部取,最底下的羽毛球是娶不到的,用完羽毛球后,只能从顶部放回去。

还有一个栈的典型例子就是,我们在编辑文档时候,经常使用的control + z 撤回的操作,这就可以用栈来实现,把每一步操作都放到栈中,当你想回退的时候,就使用pop方法,把栈顶元素弹出,于是就得到了上一步的操作。

数据存储

从数据的存储角度来看,实现栈有两种方式,

  • 以数组作为基础
  • 以链表为基础

数组是最简单的实现方式,数组也是大家平时使用最频繁的,最熟悉的数据类型。

先定义一个简单的栈。

    function Stack() {
        let items = [] // 使用数组来存储数据
    }

栈方法:

  • push 添加元素到栈顶(放一个羽毛球)
  • pop 弹出栈顶元素(从羽毛球筒拿出一个羽毛球)
  • top 返回栈顶元素,不是弹出,这点和pop有点区别,(看一眼羽毛球筒中最顶端的羽毛球,但是不拿)
  • isEmpty 栈是否为空(羽毛球是否用完了)
  • size 栈里元素的个数(一共有多少羽毛球)
  • clear 清空栈(把羽毛球筒里的球都倒出来)

下面来实现一个栈

    function Stack() {
        let items = [] // 使用数组来存储数据
        
        //从栈顶添加元素,也叫压栈
        this.push = function(elements) {
            items.push(elements)
        }
        //弹出栈顶元素,
        this.pop = function() {
            return items.pop()
        }
        // 返回栈顶元素,
        this.top = function() {
            return items[items.length - 1]
        }
        // 判断栈是否为空
        this.isEmpty = function() {
            return items.length === 0
        }
        // 返回栈的个数
        this.size = function() {
            return items.length
        }
        // 清空栈
        this.clear = function() {
            items = []
        }
    }

上面的代码就实现了一个很简单的栈。

栈的应用

1.判断合法括号(成对出现)
  • (1)(2)(3(4)) 合法,成对出现
  • (1)(2)(3(4) 不合法,没有成对出现,少了一个 ')'

从栈的角度去考虑这个问题,就会很简单。

  • 我们遍历每个字符,
  • 遇到左括号,就压入栈中
  • 遇到右括号,
    • 判断栈是否为空,为空说明没有左括号对应,就是不合法,
    • 不为空就把栈顶元素弹出,就抵消了一对括号。
  • 当遍历结束后,如果栈是空的,就说明所有的左右括号都抵消了,如果栈里还有元素,就说明缺少右括号,不合法。
    function isLegalBrackets(str) {
        let stack = new Stack()
        
        for(let i = 0; i < str.length; i++) {
            let item = str[i]
            
            if(item === '(') {
                // 遇到左括号,压入栈中,
                stack.push(item)
            } else if(item === ')') {
                // 判断栈是否为空,如果为空,说明没有左括号与之抵消,不合法
                if(stack.isEmpty()) {
                    return false
                } else {
                    // 栈顶元素弹出 
                    stack.pop()
                }
            }
        }
        
        return stack.isEmpty
    }
2.计算后缀表达式

什么是后缀表达式?

['1', '2', '3', '+', '*']

1 + 1 中缀表达式

1 1 + 后缀表达式

数字在前,运算符在后。这样的就是后缀表达式。

现在要计算表达式 ['4', '13', '5', '/', '+']的运算结果

这个问题站在栈的角度上去考虑,就很好解决,

  • 遍历数组
  • 如果元素是数字,就压入栈中,
  • 如果元素是运算符中的其中一个,择从栈中连续弹出两个元素,并对这两个元素计算,将计算结果压入栈中,
  • 遍历结束后,栈中只有一个元素,这个元素就是表达式的结果。
    // 计算后缀表达式
    function calcExp(exp) {
      let stack = new Stack()
    
      for(let i = 0; i < exp.length; i++) {
        let item = exp[i]
        if(['+','*','-','/'].indexOf(item) >= 0) {
          let valueFirst = stack.pop()
          let valueSecond = stack.pop()
          // 第一个出栈的结果在表达式的左边, 第二个出栈的结果在表达式的右边
          let expStr = valueSecond + item + valueFirst // 表达式的字符串
          let res = parseInt(eval(expStr))
          stack.push(res)
        }else {
          stack.push(item)
        }
      }
    
    // 表达式如果是正确的,最终栈里只有一个元素,为表达式的最终结果
      return stack.pop() 
    }
3.实现一个具有min方法的栈,返回栈里的最小元素,并且时间复杂度为o(1).

分析过程,非常重要 !要多阅读几遍

  • 1.我们要实现的这个栈,除了有常规的方法外,还要有min方法,所以我们要有两个栈,一个栈是为常规方法存在的,一个栈是为min方法存在的。
  • 2.编程思想有一个很重要的思想叫分而治之,就是分开想,分开处理。我们现在考虑常规栈dataStack,它就是一个常规栈,就是常规方法pop ,push哪些,正常实现就好了。 这个时候我们再去考虑最小栈 minStack ,这个时候,你就不要再去考虑常规栈(dataStack)的情况了,只关心最小栈(minStack), 最小栈就是用来存储栈里的最小值,我们先考虑边界情况,如果minStack为空,这个时候如果push进来一个元素,那这个元素一定是最小的,所以此时,直接放入minStack即可。如果minStack不为空,那么minStack栈的栈顶不就是他的最小元素吗,如果push 进来的元素比minStack的栈顶元素还小,那就直接放入minStack就好了,这样minStack始终存放的都是最小值。
    function MinStack() {
      let dataStack = new Stack() // 用来存放数据
      let minStack = new Stack() // 用来存放最小值
    
      this.push = function(item) {
        dataStack.push(item) // 数据栈,是常规栈,常规操作即可
    
        // 如果minStack为空,或者item < minStack的栈顶元素,就放入minStack中
        if (minStack.isEmpty() || item < minStack.top()) {
          minStack.push(item)
        } else {
          // 如果item > 栈顶元素,把minStack的栈顶元素再放入一次
          // 是为了 minStack 和 dataStack 的元素个数一致
          minStack.push(minStack.top())
        }
      }
    
      // pop 的时候,两个栈都pop
      this.pop = () => {
        dataStack.pop()
        minStack.pop()
      }
      // min方法就是取minStack的栈顶元素
      this.min = function () {
        return minStack.top()
      }
    }
    
    let min_stack = new MinStack()
    
    min_stack.push(1)
    min_stack.push(2)
    min_stack.push(3)
    console.log(min_stack.min())