谈JS中的栈与队列

241 阅读7分钟

在js这么语言中,并没有直接定义栈与队列,它只有数组的定义以及数组的一些操作函数。那么我们怎样在js中定义出栈与队列呢?今天我们来探讨这个问题。

首先,我们要明确数组有哪些增删元素的函数。如下所示:

// 添加元素
numbers.push(60); // 在数组末尾添加 60,此时数组为[10, 20, 30, 40, 50, 60]
numbers.unshift(5); // 在数组开头添加 5,此时数组为[5, 10, 20, 30, 40, 50, 60]
//删除元素
numbers.pop(); // 删除最后一个元素,此时的数组为[5, 10, 20, 30, 40, 50]
numbers.shift(); // 删除第一个元素,此时的数组为[10, 20, 30, 40, 50]
// 使用 splice() 插入和删除
numbers.splice(2, 0, 25); // 参数'2'表示的是在下标为2的位置插入第三个参数'25','0'是从下标'2'开始要删除的元素个数,此时数组为[10, 20, 25, 30, 40, 50]

1.栈

定义:栈是一种遵循先进后出原则的数据结构,可以想象成一个下端闭口上端开口的容器,放入与取出都只能从上端开口进行,这意味着最后被压入栈中的元素是第一个被弹出的元素。

而要想在js中实现这样的定义,我们要让数组严格的遵循先进后出的原则,那这样我们就可以把这个弱化的数组称为栈。那这样的话,我们在增删数据时,只可以使用 push() + pop() 或者 unshift() + shift() 进行操作

const stack = []

//入栈
stack.push(1)
stack.push(2)
stack.push(3)
stack.push(4)
stack.push(5)

// 出栈
while(stack.length){   
    for(i = 1;i <= stack.length; i++){        
        console.log(`第${i}个出栈的是${stack.pop()}`);    
    }
}

结果为:

微信图片_20241129145356.png

遵循了栈先进后出的原则

2.队列

定义:队列是一种遵循先进先出原则的数据结构,可以想象成一个左进右出的管道,这意味着首先被插入队列中的元素是第一个被删除的元素。

而同样,要想在js中实现这样的定义,只可以使用 push() + shift() 和 unshift() + pop() 进行操作

const queue = []

//入队
queue.push(1)
queue.push(2)
queue.push(3)
queue.push(4)
queue.push(5)

// 出队
while(queue.length){
    for(i = 1;i <= queue.length; i++){
        console.log(`第${i}个出队列的是${queue.shift()}`);
    }
}

结果为:

微信图片_20241129145942.png

遵循了队列先进先出的原则

运用:

了解完这些后,我们可以通过一些算法题来巩固一下

1.LeetCode 232 —— 用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 你 只能 使用标准的栈操作 —— 也就是只有 push to toppeek/pop from topsize, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

解题思路:

微信图片_20241129174949.jpg

要用两个栈实现队列大体框架其实很容易就能想到,如上图所示。先往一个栈中push()进数据,再pop()出数据push()进另外一个栈再pop()出就能输出与输入一样顺序的数据,从而就可以满足队列先进先出的原则。

做到这里,可能有些小伙伴就想着哎 原来这个题目如此的简单,但是运行代码又会发现,测试不通过。而这又是为什么呢?原来,是还漏掉了其他一些情况,如果stack2里的数据还没完全pop()出去,而此时又往stack1中push()进了新的数据,然后又被pop()到了stack2中,那此时先输出的就是这个刚被push()进stack2中的那个新数据了,这样就不满足先入先出的原则了

如图所示:

微信图片_20241129174959.jpg

微信图片_20241129175000.jpg

那要如何解决这个问题呢?在往 stack2pop()数据前需要判断一下stack1是否还有数据,如果还有数据,那就需要先将stack1中的数据先存入stack2中,当数据全部存入stack2中时才可以 pop() 出数据,同样int peek() 返回队列开头的元素也需要进行这个判断。

完整代码如下:

var MyQueue = function() {
    this.stack1 = []
    this.stack2 = []
};


MyQueue.prototype.push = function(x) {
    this.stack1.push(x)
};


MyQueue.prototype.pop = function() {
    if(this.stack2.length <= 0){
        while(this.stack1.length){
            this.stack2.push(this.stack1.pop())
        }
    }
    return this.stack2.pop()
};


MyQueue.prototype.peek = function() {
    if(this.stack2.length <= 0){
        while(this.stack1.length){
            this.stack2.push(this.stack1.pop())
        }
    }
    const len = this.stack2.length
    return this.stack2[len - 1]
};


MyQueue.prototype.empty = function() {
    return !(this.stack1.length + this.stack2.length) //!是将int转换为bool类型
};

2.LeetCode 239 ——滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k **的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

 

示例 1:

输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7      5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

示例 2:

输入: nums = [1], k = 1
输出: [1]

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
  • 1 <= k <= nums.length

当看到这个题目的时候,第一想法肯定是用双指针去求解,这样当然可以求解,但是有一个很大的弊端,就是每次在两个指针之间都要求这三个数的最大值,那么如果这个数组很大呢?那就需要进行很多次的比较了,但是有时候一直移动它的最大值其实一直都没有变,例如例子中前两次的框选最大值都是三,那这样一直比较只会增加开销。

这时,我们就可以引入一个新的概念——“双端队列”

双端对列,顾名思义,这个队列的两端都可以进出

那我们要如何减少每次双指针之间的数都要比较这一问题呢,引入一个双端队列,让最大值一直处于队列的对头,有比它大的值出现时才会将其替代,就这样一直便遍历到后面,最大值一直在对头,并且这个队列里的元素呈现的是一个单调递减的样子。此时,还要注意一个问题,那就是这个k值小大,那在这个遍历过程中,可能这个最大值已经不处于这个窗口之间了,那么也要将其移除。

这两个问题对应的也就是下面这两块代码,需要着重理解

    for(let i = 0; i < len; i++){
        while(deque.length && nums[deque[deque.length - 1]] < nums[i]){
            deque.pop(); // 移除小于当前元素的索引
        }
        deque.push(i); // 将当前索引压入队列
  • 这部分循环遍历数组 numsi 是当前元素的索引。
  • 内部的 while 循环确保 deque 中的元素索引对应的值是递减的:每当遇到一个比队列尾部对应元素更大的元素时,将尾部元素移除,保持 deque 中索引对应的值从大到小。
  • 然后将当前元素的索引 i 添加到 deque 中。
        // 当队列头部存放的值和 i 形成的区间大于窗口宽度时
        while(deque.length && deque[0] <= i - k){
            deque.shift(); // 移除过期的索引
        }
  • 这个 while 循环检查 deque 的头部元素(索引)是否已经超出了当前窗口的范围 i - k。如果 deque[0] 这个索引已经在窗口范围之外,就将其移除,保证 deque 中的索引始终是当前窗口内的。

完整代码展示:

var maxSlidingWindow = function(nums, k) {
    const len = nums.length
    const res = []
    const deque = [] // 用于存放索引的双端队列
    for(let i = 0;i < len; i++){
        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
};

看到这里,相信大家对JS中栈与队列的使用更加的熟悉了,如果对大家有所帮助的话,那么请大家:

R-C.jpg