- 栈向队列的转化
- 双端队列
- 优先队列
△:优先队列属于高级数据结构,其本质是二叉堆结构,所以放到二叉树和堆那里一起学!!
如何用栈实现一个队列?
题目描述:使用栈实现队列的下列操作:
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 操作)。
思路分析:
栈:先进后出
队列:先进先出
用栈实现队列——把栈逆序(栈底元素先被取出)——借助新的一个栈
首先把栈1元素依次取出放到栈2
然后直接对 stack2 执行出栈操作(就是队列的出队序列),欸,如果有新元素加入怎么办?
没关系,先把栈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指针
然后在这范围遍历取出最大值,存进结果数组里;
重复步骤,知道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丢掉
双端队列法
注意:
① 推入的元素(当前元素)大于队尾元素,那么就把它干掉并替换,直到队尾元素>=当前元素为止,此时再将当前元素入队;
② 那么我们取最大值永远是队头那个元素(因为我们维持队列是在递减性基础上更新)
(找张白纸按照代码逻辑写一遍,思路会清晰很多;或者看看这个视频:单调队列模拟过程讲解)
// 双端队列 ---单调队列
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;
};