栈与队列之练习进阶篇
一、单调队列(由双端队列实现)
解决问题:连续区间最值问题
1.剑指 Offer 59 - II. 队列的最大值
请定义一个队列并实现函数 max_value
得到队列里的最大值,要求函数max_value
、push_back
和 pop_front
的均摊时间复杂度都是O(1)。
若队列为空,pop_front
和 max_value
需要返回 -1
示例 1:
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
分析:
1>.题目要求使用队列实现元素的队尾入队操作、队首出队操作以及返回队列元素最大值,且均摊时间复杂度均为O(1)
2>.我们都知道,在队列中,出队以及入队操作的时间复杂度均为O(1),但返回队列元素最大值的一般做法是使用循环遍历队列,找到最大值,其时间复杂度不言而喻是**O(n)**了,那么如何将max_value操作的时间复杂度变为O(1)呢?
3>.引入队列的另一种结构,单调队列,它是通过双端队列实现的
4>.单调队列的思想:单调队列的队首一定表示队列中最大的元素,当有元素入队时,若队列为空,以及队列不为空且队尾元素大于该元素时,该元素从队尾入队,否则(即队尾元素小于该元素)使用循环将队列中所有小于该元素的元素从队尾出队,再将该元素从队尾入队
5>.当给定区间的元素遍历结束后,若单调队列为空,表示该队列无最大值,否则单调队列的队首元素即为给定连续区间的最大值
class MaxQueue {
//普通队列
private Queue<Integer> queue;
//单调队列
private LinkedList<Integer> monotonicQueue;
public MaxQueue() {
//初始化队列
this.queue = new LinkedList<>();
this.monotonicQueue = new LinkedList<>();
}
//返回队列中的最大元素
public int max_value() {
//若单调队列为空,说明此区间无最大值
if (this.monotonicQueue.isEmpty()) {
return -1;
}
//否则,单调队列队首元素即为给定区间的最大值
return this.monotonicQueue.peekFirst();
}
//入队操作
public void push_back(int value) {
//将元素添加到普通队列
queue.offer(value);
//将元素添加到单调队列
while (!this.monotonicQueue.isEmpty() && this.monotonicQueue.peekLast() < value) { //判断入队元素是否大于队中元素,若大于,循环从队尾移出队列中所有小于此入队元素的元素
this.monotonicQueue.pollLast();
}
this.monotonicQueue.offerLast(value); //从队尾添加该元素
}
//出队操作
public int pop_front() {
//判断队列是否为空
if (this.queue.isEmpty()) {
return -1;
}
//记录出队元素
int temp = this.queue.poll();
//若出队元素与此时单调队列中最大值相等,也需要将单调队列中的队首(即最大元素)移出
if (temp == this.monotonicQueue.peekFirst()) {
this.monotonicQueue.pollFirst();
}
return temp;
}
}
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
class Solution {
//创建一个内部类(表示单调队列类,在类中定义一个单调队列以及入队、出队、返回队列最大值的方法)
private class MonotonicQueue {
//定义单调队列
private LinkedList<Integer> queue;
public MonotonicQueue() {
this.queue = new LinkedList<>();
}
//入队操作
public void offer(int value) {
//如果队列不为空,移出队列中比value值小的元素
while (!this.queue.isEmpty() && this.queue.peekLast() < value) {
this.queue.pollLast();
}
this.queue.offerLast(value);
}
//出队操作
public int poll() {
if (this.queue.isEmpty()) {
return -1;
}
return this.queue.pollFirst();
}
//求队列最大值
public int max() {
if (this.queue.isEmpty()) {
return -1;
}
return this.queue.peekFirst();
}
}
public int[] maxSlidingWindow(int[] nums, int k) {
//对入参进行判断
if(nums==null || nums.length==0){
return nums;
}
//创建单调队列
MonotonicQueue monotonicQueue = new MonotonicQueue();
//创建集合存放每个区间的最大元素
LinkedList<Integer> result = new LinkedList<>();
//遍历数组
for (int i = 0; i < nums.length; i++) {
if (i < k - 1) {
//入队
monotonicQueue.offer(nums[i]);
} else {
monotonicQueue.offer(nums[i]);
//求三个元素中的最大值
int max = monotonicQueue.max();
//将最大元素添加到集合中
result.add(max);
//记录移除的元素
int temp = nums[i - k + 1];
//如果移除的元素是最大元素,将双端队列中的元素也移除
if (temp == max) {
monotonicQueue.poll();
}
}
}
//将集合转为数组
return result.stream().mapToInt(Integer::intValue).toArray();
}
}
二、单调栈
解决问题:下一个更大元素问题
单调栈结构思想:
1>.for循环从区间的最后一个元素开始,直到区间的第一个元素结束
2>.创建一个单调栈,如果栈不为空且对应位置的区间元素大于栈顶元素,while循环将栈中小于该元素的所有元素弹出栈,这样在程序执行的过程中,栈顶元素永远为最大元素,当循环结束后,如果栈为空,返回-1,否则返回栈顶元素,将其存储在res数组中,再将元素元素压入栈中
3>.for循环结束后,res数组中元素即为给定区间所有元素的下一个更大元素
1.使用单调栈求解区间[2,1,5,3,4,9,6]每一个元素的下一个更大元元素
//暴力法
//int[] nums={2,1,5,3,4,9,6}; 求此区间每个元素的下一个更大元素
//对入参进行判断
if(nums==null || nums.length==0){
return nums;
}
//创建一个数组用于存放下一最大元素
int[] result=new int[nums.length];
//使用-1填充数组
Arrays.fill(result,-1);
//从区间第一个元素开始遍历
for(int i=0;i<nums.length;i++){
//内层循环从外层循环的元素之后一个元素开始循环
for(int j=i+1;j<nums.length;j++){
//判断第i个元素是否有下一个更大元素
if(nums[j]>nums[i]){
//将第i个元素的下一个更大元素存储到result数组中
result[i]=nums[j];
break;
}
}
}
return result;
//使用单调栈解决下一个最大元素
//对入参进行判断
if(nums==null || nums.length==0){
return nums;
}
//创建一个单调栈
Stack<Integer> stack=new Stack<>();
int[] result=new int[nums.length];
//从区间最后一个元素开始遍历,直到区间第一个元素
for(int i=nums.length-1;i>=0;i--){
//while循环将栈中比nums[i]小的元素全部弹出
while(!stack.isEmpty() && stack.peek() <=nums[i]){
stack.pop();
}
//如果栈为空,表示该元素后没有下一个大于它的元素,返回-1并存储到result数组中;否则返回栈顶元素
result[i]=stack.isEmpty()? -1:stack.peek();
//将该元素压入栈中,继续判断下一个元素
stack.push(nums[i]);
}
return result;
2.每日温度(leetcode 739)
给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
public int[] dailyTemperatures(int[] temperatures){
//对入参进行判断
if(temperatures==null || temperatures.length==0){
return temperatures;
}
int[] result=new int[temperatures.length];
//创建一个单调栈,注意此题中,栈中存储的是区间元素的索引
Stack<Integer> stack=new Stack();
//从区间的最后一个元素开始
for(int i=temperatures.length-1;i>=0;i--){
while(!stack.isEmpty() && temperatures[stack.peek()] <=temperatures[i]){
stack.pop();
}
result[i]=(stack.isEmpty()? 0:(stack.peek()-i));
stack.push(i);
}
return result;
3.下一个更大元素 I(leetcode 496)
nums1
中数字 x
的 下一个更大元素 是指 x
在 nums2
中对应位置 右侧 的 第一个 比 x
大的元素。
给你两个 没有重复元素 的数组 nums1
和 nums2
,下标从 0 开始计数,其中nums1
是 nums2
的子集。
对于每个 0 <= i < nums1.length
,找出满足 nums1[i] == nums2[j]
的下标 j
,并且在 nums2
确定 nums2[j]
的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1
。
返回一个长度为 nums1.length
的数组 ans
作为答案,满足 ans[i]
是如上所述的 下一个更大元素。
示例 1:
输入:nums1 = [4,1,2], nums2 = [1,3,4,2].
输出:[-1,3,-1]
解释:nums1 中每个值的下一个更大元素如下所述:
- 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
- 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。
- 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
解题思路:
方法1:暴力法
1>.创建一个数组,用于存储所求区间的下一个更大元素,首先用fill方法将数组元素全部填充为-1
2>.双层for循环,所求区间1在外,在区间2中找区间1中元素在内层
3>.如果在区间2中找到区间1元素,从区间2该元素的下一个元素开始找其后比它大的元素,此时需要while循环,将找到的结果存储在所创建的数组中,立刻终止while循环
4>.整个循环结束后,返回数组
方法2:利用单调栈
1>.创建一个单调栈和两个数组
2>.使用单调栈求出区间2中所有元素的下一个更大元素存储到res1数组中
3>.双层for循环,找到区间1中元素在区间2中的对应位置
4>.取出res1数组中对应的元素存储到res2数组中,返回数组res2
//暴力法:时间复杂度O(n^3)
int[] result=new int[nums1.length];
Arrays.fill(result,-1);
//遍历数组nums1
for(int i=0;i<nums1.length;i++){
//遍历数组nums2
for(int j=0;j<nums2.length;j++){
//找数组nums1中元素在数组nums2的对应位置
if(nums2[j]==nums1[i]){
//遍历数组nums1中元素在数组nums2对应位置之后的元素,找下一个大于它的元素
while(j+1<nums2.length){
if(nums1[i]<nums2[j]){
//找到下一个更大元素,将结果存储到result数组中
result[i]=nums2[j];
//立刻终止循环
break;
}
j++;
}
}
}
}
return result;
//使用单调栈:时间复杂度O(n^2)
//解决一个区间中的元素在另一区间对应位置之后的下一个更大元素
//对入参进行判断
if (nums1 == null || nums2 == null || nums1.length == 0 || nums2.length == 0) {
return null;
}
int[] res1 = new int[nums2.length];
int[] res2 = new int[nums1.length];
Stack<Integer> stack = new Stack<>();
//使用单调栈求出第二个区间每个元素的下一个更大元素,将结果存储到res1数组中
for (int i = nums2.length - 1; i >= 0; i--) {
while (!stack.isEmpty() && stack.peek() <= nums2[i]) {
stack.pop();
}
res1[i] = (stack.isEmpty()) ? -1 : stack.peek();
stack.push(nums2[i]);
}
//遍历两个区间,如果区间1中的元素等于区间2中的元素,将res1中对应位置的结果赋给res2数组中返回
for (int i = 0; i < nums1.length; i++) {
for (int j = 0; j < nums2.length; j++) {
if (nums1[i] == nums2[j]) {
res2[i] = res1[j];
break;
}
}
}
return res2;