day 9:队列

49 阅读3分钟
  1. 栈向队列的转化
  2. 双端队列
  3. 优先队列

△:优先队列属于高级数据结构,其本质是二叉堆结构,所以放到二叉树和堆那里一起学!!

如何用栈实现一个队列?

题目描述:使用栈实现队列的下列操作:
push(x) -- 将一个元素放入队列的尾部。
pop() -- 从队列首部移除元素。
peek() -- 返回队列首部的元素。
empty() -- 返回队列是否为空。

示例: MyQueue queue = new MyQueue();
queue.push(1);
queue.push(2);
queue.peek(); // 返回 1
queue.pop(); // 返回 1
queue.empty(); // 返回 false

说明:

  • 你只能使用标准的栈操作 -- 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
  • 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。

思路分析:
栈:先进后出
队列:先进先出
用栈实现队列——把栈逆序(栈底元素先被取出)——借助新的一个栈

image.png 首先把栈1元素依次取出放到栈2

image.png 然后直接对 stack2 执行出栈操作(就是队列的出队序列),欸,如果有新元素加入怎么办?

image.png 没关系,先把栈2为空,然后重复前面步骤(把栈1移到栈2再出栈)

/**
 * 初始化构造函数
 */
const MyQueue = function () {
    // 初始化两个栈
    this.stack1 = [];
    this.stack2 = [];
};

MyQueue.prototype.push = function (x) {
    // 直接调度数组的 push 方法
    this.stack1.push(x);
};


MyQueue.prototype.pop = function () {
    // 假如 stack2 为空,需要将 stack1 的元素转移进来
    if (this.stack2.length <= 0) {
        // 当 stack1 不为空时,出栈
        while (this.stack1.length !== 0) {
            // 将 stack1 出栈的元素推入 stack2
            this.stack2.push(this.stack1.pop());
        }
    }
    // 为了达到逆序的目的,我们只从 stack2 里出栈元素
    return this.stack2.pop();
};

/**
 * Get the front element.
 * @return {number}
 * 这个方法和 pop 唯一的区别就是没有将定位到的值出栈
 */
MyQueue.prototype.peek = function () {
    if (this.stack2.length <= 0) {
        // 当 stack1 不为空时,出栈
        while (this.stack1.length != 0) {
            // 将 stack1 出栈的元素推入 stack2
            this.stack2.push(this.stack1.pop());
        }
    }
    // 缓存 stack2 的长度
    const stack2Len = this.stack2.length;
    return stack2Len && this.stack2[stack2Len - 1];
};

/**
 * Returns whether the queue is empty.
 * @return {boolean}
 */
MyQueue.prototype.empty = function () {
    // 若 stack1 和 stack2 均为空,那么队列空
    return !this.stack1.length && !this.stack2.length;
};

双端队列

双端队列就是允许在队列的两端进行插入和删除的队列,既允许使用 pop、push 同时又允许使用 shift、unshift 的数组

滑动窗口

题目描述:给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例: 输入: 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
1 [3 -1 -3] 5 3 6 7
1 3 [-1 -3 5] 3 6 7
1 3 -1 [-3 5 3] 6 7
1 3 -1 -3 [5 3 6] 7
1 3 -1 -3 5 [3 6 7]

最大值分别对应:
3 3 5 5 6 7

提示:你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。

思路分析:
约束范围-双指针:定义left、right指针

image.png

然后在这范围遍历取出最大值,存进结果数组里;
重复步骤,知道right指针指到一个数

// 双指针 + 遍历
const maxSlidingWindow = function (nums, k) {
    // 缓存数组的长度
    const len = nums.length;
    // 定义结果数组
    const res = [];
    // 初始化左指针
    let left = 0;
    // 初始化右指针
    let right = k - 1;
    // 当数组没有被遍历完时,执行循环体内的逻辑
    while (right < len) {
        // 计算当前窗口内的最大值
        const max = calMax(nums, left, right);
        // 将最大值推入结果数组
        res.push(max);
        // 左指针前进一步
        left++;
        // 右指针前进一步
        right++;
    }
    // 返回结果数组
    return res;
};

// 这个函数用来计算最大值
function calMax(arr, left, right) {
    // 处理数组为空的边界情况
    if (!arr || !arr.length) {
        return;
    }
    // 初始化 maxNum 的值为窗口内第一个元素
    let maxNum = arr[left];
    // 遍历窗口内所有元素,更新 maxNum 的值
    for (let i = left; i <= right; i++) {
        if (arr[i] > maxNum) {
            maxNum = arr[i];
        }
    }
    // 返回最大值
    return maxNum;
}

可是问题来了:这样里外都在遍历,时间复杂度:O(kn)

假设数组的规模是 n,那么从起始位置开始,滑动窗口每次走一步,一共可以走 n - k 次。注意别忘了初始位置也算作一步的,因此一共走了 n - k + 1次。然后每个窗口内部我们又会固定执行 k 次遍历。注意 k 可不是个常数,它和 n 一样是个变量。

怎么优化???
把O(kn)----> O(n);把k丢掉
双端队列法

image.png

image.png 注意:
① 推入的元素(当前元素)大于队尾元素,那么就把它干掉并替换,直到队尾元素>=当前元素为止,此时再将当前元素入队;
② 那么我们取最大值永远是队头那个元素(因为我们维持队列是在递减性基础上更新)

(找张白纸按照代码逻辑写一遍,思路会清晰很多;或者看看这个视频:单调队列模拟过程讲解

// 双端队列 ---单调队列
const 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);
        // 当队头元素的索引已经被排除在滑动窗口之外时
        while (deque.length && deque[0] <= i - k) {
            // 将队头元素索引出队
            deque.shift();
        }
        // 判断滑动窗口的状态,只有在被遍历的元素个数大于 k 的时候,才更新结果数组
        if (i >= k - 1) {
            res.push(nums[deque[0]]);
        }
    }
    // 返回结果数组
    return res;
};