栈和队列算法实战

699 阅读4分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

栈是一种特殊的线性表,仅能够在栈顶进行操作,有着先进后出、后进先出的特性

实现

class Stack {
    #stackArr = [] // 私有属性
    constructor() {
        // 不建议直接在此定义stackArr, 因为stackArr只能通过暴露的方法进行操作, 不能让实例可以直接操作stackArr
        // this.stackArr = []
    }
    
    // push 添加一个元素到栈顶
    push(item) {
        this.stackArr.push(item)
    }
    
    // pop 弹出栈顶元素
    pop() {
        return this.stackArr.pop()
    }
    
    // top 返回栈顶元素, 取值操作
    top() {
        return this.stackArr[this.stackArr.length - 1]
    }
    
    // isEmpty 判断栈是否为空
    isEmpty() {
        return this.stackArr.length === 0
    }
    
    // size 返回栈里面元素个数
    size() {
        return this.stackArr.length
    }
    
    // clear 清空栈 重置
    clear() {
         this.stackArr = []
    }
}

栈的实现其实就是基于数组的封装,既然已经有了数组,为何还需要再封装一层实现栈呢?

  1. 数组提供了很多的操作方式,可以很方便的对于数据进行任意操作,其实是不可控的,基于数据封装一层栈结构,可以很好限制对数据的任意操作,只能遵从栈的操作特性(先进后出,后进先出),为实际业务开发中提供一种思考问题的方式
  2. 基于数组封装栈,是为了隐藏是实现细节,当我们遇到业务和开发问题时,站在栈的肩膀思考问题会来的更方便一些,而不是使用数组一把梭

应用

判断括号合法

let str1 = 'a(daj(fl(ks)aj)fl)kas' // 合法
let str2 = 'as(dj(flkajs)d(fsaj)fs)' // 合法
let str3 = 'as(dfl)ka(sf)j(skd)f)' //  不合法, 缺少
let str4 = 'asd(kl)alk)s(df' // 不合法 顺序不对

思路

循环字符串,针对不同类型的字符串做不同的操作

遇到左括号 - 压栈

遇到右括号 - 判断栈是否为空,若是为空则说明没有对应的左括号,不合法,栈不为空则弹栈,抵消掉一对括号

其他类型 - 跳过

当遍历完之后,若是栈为空, 则合法,否则就是不合法的

const STR1 = 'a(daj(fl(ks)aj)fl)kas' // 合法
const STR2 = 'as(dj(flkajs)d(fsaj)fs)' // 合法
const STR3 = 'as(dfl)ka(sf)j(skd)f)' //  不合法, 缺少
const STR4 = 'asd(kl)alk)s(df' // 不合法 顺序不对

/** 
 * 判断字符串中的括号是否合法
*/
const isLegalBracket = str => {
  if (Object.prototype.toString.call(str) !== "[object String]") {
    // throw new Error('str is must string');
    return false;
  }
    
  const STACK = new Stack()

  for (let i = 0; i < str.length; i++) {
    const ELE = str[i]
    
    if (ELE === '(') { // 压栈
      STACK.push(ELE)
    }else if(ELE === ')') { 
      if (STACK.isEmpty()) { // 栈为空则说明没有对应的左括号,不合法
        return false
      } else { // 弹栈
        STACK.pop()
      }
    }
  }

  return STACK.isEmpty()
}

console.log(isLegalBracket(STR1)) // true
console.log(isLegalBracket(STR2)) // true
console.log(isLegalBracket(STR3)) // false
console.log(isLegalBracket(STR4)) // false

计算逆波兰表达式

用途

逆波兰表达式是一种十分有用的表达式,它将复杂表达式转换为可以依靠简单的操作得到计算结果的表达式。例如(a+b)(c+d)转换为ab+cd+

优势

它的优势在于只用两种简单操作,入栈和出栈就可以搞定任何普通表达式的运算。其运算方式如下:

如果当前字符为变量或者为数字,则压栈,如果是运算符,则将栈顶两个元素弹出作相应运算,结果再入栈,最后当表达式扫描完后,栈里的就是结果。

参照表

正常的表达式(中缀表达式)逆波兰表达式(后缀表达式)
a + b[a, b, +]
a + (b - c)[a, b, c, -, +]
a + (b - c) * d[a, b, c, -, *, +]
a * (b + c) + d[a, b, c, +, *, d, +]

计算 ['1', '2', '3', '+', '*', '4', '+']等价于计算1 * (2 + 3) + 4

解题思路:

将数组依次遍历,当遇到非运算符时,将数据进行压栈处理,将当遇到预算符时,从栈中连续弹栈两次进行然后与运算符结合计算,并且将结果压栈,当循环结束之后,栈中只剩下一个数据,便是结果

const ARR = ['1', '2', '3', '+', '*', '4', '+']

const reversePolishNotation = arr => {
  // 容错处理
  if(!Array.isArray(arr)) {
    throw new Error('arguments is must array');
  }

  const STACK = new Stack()
  const OPERATORS = ['+', '-', '*', '/', '']

  for (let i = 0; i < arr.length; i++) {
    const ELE = arr[i]

    if(OPERATORS.includes(ELE)){ // 操作符
      // 1. 连续弹栈两次
      const A = STACK.pop()
      const B = STACK.pop()

      // 2. 将数据与运算符拼接
      const STR =  B + ELE + A

      // 3. 执行运算并将结果压栈
      const RES = parseInt(eval(STR)).toString()
      STACK.push(RES)
    }else { // 非操作符 直接压栈
      STACK.push(ELE)
    }
  }

  return STACK.top()
}

console.log(reversePolishNotation(ARR)); // 9

队列

队列是一种特殊的线性表,它只允许再队列的头部删除元素,在队列的尾部添加新的元素,特性先进先出,后进后出

实现

class Queue {
  constructor() {
    this.queue = []
  }

  // 队列尾部添加元素
  enqueue(queue) {
    this.queue.push(queue)
  }

  // 队列头部删除元素
  dequeue() {
    return this.queue.shift()
  }

  // 查看队列头部元素 不操作队列 仅仅查看
  header() {
    return this.queue[0]
  }

  // 查看队列尾部元素 不操作队列 仅仅查看
  tail() {
    return this.queue[this.queue.length - 1]
  }

  // 返回队列大小
  size() {
    return this.queue.length
  }

  // 清楚队列
  clear() {
    this.queue.length = []
  }

  // 是否为空队列
  isEmpty() {
    return this.queue.length === 0
  }
}

应用

约瑟夫环

有一个数组存放0-99的数字,要求每隔两个数删掉一个数,到末尾时循环至开头继续进行,求最后一个删除的数

若是 0-4

第一轮删除之后剩余 [0,1,3,4], 结束本轮,此时到末尾时数到2,下一轮开始为3

第二轮删除之后剩余 [1,3], 结束本轮,此时到末尾时数到3,刚好删除最后一位4, 下一轮开始为0

第三轮无删除操作,结束本轮,此时到末尾时数到2,下一轮开始为3, 但是数据本身不变

第四轮删除之后剩余[3], 此时只剩下最后一位数,便是最后一个需要删除的数

递归思想:


/**
 * [约瑟夫环问题]
 * @param  {Number} count [累加器]
 * @param  {[type]} arr   [数组]
 * @return {[type]}       [返回最后一个删除的数值]
 */
const josephus2 = (count = 0, arr) => {

  if(arr.length === 1) { // 递归的结束条件, 当只剩余1个时,即为最后一个删除的数
    return arr[0]
  }

  // 利用递归
  for (var i = 0; i < arr.length; i++) {
    count += 1
    if(count % 3 === 0) {
      arr.splice(i, 1)
      i -= 1 // 每一次删除之后,必须要重置i的值,不然arr本身是被修改过的,会导致arr.length变化
    }
  }

  return josephus2(count, arr)
}

console.log(josephus2(0, ARR))

队列思路:

/**
 * 生成一个序列数组
 * @param  {[type]} start [开始, 包含开始]
 * @param  {[type]} end   [结束, 不包含结束]
 * @return {[type]}       [生成的升序数组]
 */
const GENERATOR_NUMS = (start, end) => {
  let RES = []
  for (let i = start; i < end; i++) {
    RES.push(i)
  }

  return RES
}

const ARR = GENERATOR_NUMS(0, 100)

/**
 * 约瑟夫环问题
 * @param  {[type]} arr 数组
 * @return {[type]} 返回最后一个删除的数值
 */
const josephus = arr => {
  const QUEUE = new Queue()

  // 1. 将数组arr的数据依次添加至队列中
  arr.forEach(num => QUEUE.enqueue(num))

  // 2. 定义一个变量,用于控制数据的删除
  let index = 0

  // 3. 使用while循环, 当 i % 3 === 0的时候时需要删除,否则就从新添加队列尾部
  while(QUEUE.size() !== 1) {
    const ITEM = QUEUE.dequeue()
    index += 1

    if(index % 3 != 0){ // 不满足 添加至队列尾部
      QUEUE.enqueue(ITEM)
    }
  }

  return QUEUE.header()
}

console.log(josephus(ARR))