栈哥与队宝的欢乐日常:挤挤更有趣的数据结构故事🤗

388 阅读8分钟

数组上的方法

在聊栈哥与队宝之前,我们先来回顾一下关于JS数组上的方法:

arr.push('f')   // 向尾部添加元素
arr.pop()   // 移除数组尾部元素
arr.unshift('hello')  // 头部添加
arr.shift()  // 头部移除
arr.slice(2, 5)   // 从下标2删除到下标5(不包括5) [2,5)
arr.splice(1, 0, 'hello')  // 删除0个元素添加一个'hello'
arr.splice(0, 2)  // 从下标为0的元素开始删除2个

其中slicesplice区别在于slice不会改变原数组,而splice会改变原数组。

const arr = [1, 2, 3, 4, 5, 6, 7]

console.log(arr.slice(2, 5));   // [ 3, 4, 5 ]
console.log(arr);   // [1, 2, 3, 4, 5, 6, 7]

const arr1 = [1, 2, 3, 4, 5, 6, 7]

console.log(arr1.splice(2, 3));  // [ 3, 4, 5 ]
console.log(arr1);    // [ 1, 2, 6, 7 ]

接下来我们来聊一聊栈。栈是一种常见的数据结构,它是什么样子的呢?就相当于一堆叠起来的盘子,你放一个盘子只能往上面添加(正常情况下),而你取盘子也是只能从上面取。栈就相当于这种操作,先进后出,最上面的为栈顶,最下面为栈底。

怎么使用栈呢?在 JS 中,我们可以通过let stack = []来创建一个栈,这不是一个数组吗?对,就是一个数组,但是我们可以只让它一边进一边出,也就是只使用数组上的push()+pop()unshift()+shift()方法,就是一个栈了,只能在尾部添加或删除,所以可以称栈结构是一个弱化版的数组。

const stack = []

stack.push('可爱多')
stack.push('巧乐兹')
stack.push('小布丁')
stack.push('老冰棍')

while (stack.length) {
  const top = stack.pop()
  console.log('现在吃:' + top);
}

上面的代码实现了一个简单的入栈出栈以及遍历栈的过程。

先将四种类型不同的冰棒使用数组上的push()方法顺序压入栈中,然后使用while (stack.length)遍历栈,每次循环使用stack.pop()出一个栈顶的冰棒,会返回栈顶元素的值,并且打印出来这个冰棒,这时候stack.length就会-1,当减到0时栈空,即遍历完毕。 image.png

栈的应用

既然知道了栈这种数据结构,那么我们可以用栈来干嘛呢?,来看一道leetcode第20题——有效的括号问题。 image.png

s = "()[]{}"  // 输出 true
s = "([])"   // 输出 true
s = "(["    //输出 false
s = "([)]"   //输出 false

这就是一个非常适合用栈来解决的问题。但是我们如何将左括号与右括号判断配对呢?这就可以用到对象中的键值对配对。可以创建一个对象,里面的属性为左边括号,属性值为右边括号。

const lToR = {
    '(': ')',
    '[': ']',
    '{': '}'
  }

然后遍历字符串,左边括号就入栈,遇到右边括号就判断当前是否有栈顶元素(栈空了直接返回false)且栈顶的元素是否有相应的左括号(lToR[stack.pop()] == s[i]),一旦有一个不等于或遍历完栈没空(左括号多了,比如([],此时(还在栈中,就返回false

具体代码如下;

let s = '([{}])'

var isValid = function (s) {
  const lToR = {
    '(': ')',
    '[': ']',
    '{': '}'
  }

  const stack = []

  for (let i = 0; i < s.length; i++) {
    //如果是左括号就入栈
    if (s[i] === '(' || s[i] === '[' || s[i] === '{') {
      stack.push(s[i])
    } else {  //当前获取到的是右边的括号  //考虑右边括号多,没有左边的括号配对
      if (!stack.length || lToR[stack.pop()] !== s[i]) {     // '{' === '}'
        return false
      }
    }
  }

  return !stack.length  //考虑左边括号多了
};

console.log(isValid(s));  // true

除了这个问题,许多关于对称的问题都可以先想着用栈来解决。

队列

相比栈,队列就是一种先进先出的结构了,就比如超市结账排队,先排队先进去的就先结账出来,也就是先进先出,也是通过let queue = []创建,也是弱化版的数组,只使用push()+shift()unshift()+pop()方法,只能在尾部添加,头部删除取出。

const queue = []

queue.push('辣椒炒肉')
queue.push('黄豆鸡脚')
queue.push('剁椒鱼头')

while (queue.length) {
  const food = queue.shift()
  console.log('现在开始做:' + food);
}

上面的代码展示了队列的入队出队和遍历队列的过程。 image.png 使用push()方法从尾部添加,shift()方法从头部弹出。

队列的应用

再来看一道leetcode的第225题算法题,用队列来实现栈。 image.png 我们可以先创建两个队列(queue1queue2),且只能在队尾添加,队头删除,只能获取队头的元素。

push()方法直接往queue1中直接添加即可完成(this.queue1.push(x);

image.png

再来看pop()top()方法的实现。以上图为例,先push()进去1234pop()方法就是获取出来4,并且把4出队列,根据上图,可以先将123压入queue2中,然后再弹出queue1队头的元素4,如下。 image.png

但是当接着pop()此时queue1为空队列,即输出错误,所以我们应该判断如果此时queue1为空队列,将俩个队列交换,这样下次pop()操作就可以同上操作一样弹出queue1队头元素3了。

if (!this.queue1.length) {
    [this.queue1, this.queue2] = [this.queue2, this.queue1];
  }

交换完如下; image.pngtop()获取栈顶的元素可以将前面的pop()push()方法结合。先使用pop()将栈顶元素出栈并且此时用变量x 接收返回值,然后再将push(x)入栈(弹出一个栈顶元素,用变量接收该值再加入进去),再return出 x 的值即可获取栈顶元素。

let x = this.pop();
this.queue1.push(x);
return x;

最后判断是否为空empty()可以直接用俩个队列长度是否为0,可简写成两个队列长度加起来为0即为空,返回true,反之false。

具体代码如下;

var MyStack = function () {
  this.queue1 = [];
  this.queue2 = [];
};

// 压入栈
MyStack.prototype.push = function (x) {
  this.queue1.push(x);
};

// 弹出栈顶元素
MyStack.prototype.pop = function () {
  if (!this.queue1.length) {
    [this.queue1, this.queue2] = [this.queue2, this.queue1];
  }
  while (this.queue1.length > 1) {
    this.queue2.push(this.queue1.shift());
  }
  return this.queue1.shift();
};

// 获取栈顶元素
MyStack.prototype.top = function () {
  let x = this.pop();
  this.queue1.push(x);
  return x;
};

// 判断是否为空
MyStack.prototype.empty = function () {
  return !(this.queue1.length + this.queue2.length)
};

会了这题相信你对队列有了更加深刻的认识。还有一道leecode232题用俩个栈实现队列可以做一下。

滑动窗口最大值

接下来我们再来看一道leetcode第239题,关于滑动窗口的题目;

image.png 给出一个数组,维护一个固定长度的窗口,每次向右移动一位,选取滑动窗口的最大值。比较容易想到的解法有双指针,不过其每次都要遍历固定长度选取最大值,事件复杂度会超过题目要求。我们可以用双端队列(俩端都能进出)来解决。

首先:

const res = []     // 遍历到长度大于等于k时,存储每次的队头
const deque = []   // 存的是当前维护队列(长度k)的每个下标

一直维护一个队头为最大值的双端队列,当加入一个值时,与队尾的元素比较,如果该值更大,则接着与下一个比较,直到比到有一个比它大的在前面为止,用下面代码可以实现。

while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      deque.pop()
    }

也就是维护一个单调递减的队列,但是维护队列时需判断队头元素的下标是否在窗口内

比如; image.png

当加入2时,队头的元素已经超过了滑动窗口的范围,所以要移除掉队头元素(deque存的是维护的队列元素的下标),所以判断是否deque[0]<=i-k然后deque.shift()移除头部的元素。

具体实现代码如下;

let nums = [1, 3, -1, -3, 2, 3, 6, 7], k = 3  //[3,3,5,5,6,7]

var maxSlidingWindow = function (nums, k) {
  const len = nums.length
  const res = []     // 遍历到长度大于等于k时,存储每次的队头
  const deque = []   // 存的是当前维护队列(长度k)的每个下标

  for (let i = 0; i < len; i++) {
    // 比较当前元素与队尾的元素,如果新的>队尾,pop移除
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      deque.pop()
    }

    deque.push(i)

    // 当队列头部存放的值 和 i 形成的区间大于窗口宽度时
    while (deque.length && deque[0] <= i - k) {
      deque.shift()
    }

    // 该取最大值
    if (i >= k - 1) {
      res.push(nums[deque[0]])
    }
  }

  return res
};

好了,关于栈与队列就聊到这里了,如果觉得文章对你有所帮助的话可以点点赞o(●'◡'●)