此题对应力扣的239.滑动窗口的最大值
题目:
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
这个题刚开始做的时候,目的很明确使用链表模拟队列,然后随着窗口的移动进行相应的进队和出队,然后再利用Collections的方法max()求最大值。不过放在力扣上一运行超时了。
代码如下:
public int[] maxSlidingWindow(int[] nums, int k) {
//创建结果集
LinkedList result = new LinkedList<>();
//创建单指针
int end = k-1;
// 创建队列,并将窗口内的数据装入
LinkedList window = new LinkedList<>();
for(int i = 0;i < k;i++){
window.add(nums[i]);
}
int max ;
while(end < nums.length){
max = Collections.max(window);
result.add(max);
window.remove();
if(end == nums.length - 1){
break;
}
end++;
window.add(nums[end]);
}
return result.stream().mapToInt(Integer::valueOf).toArray();
}
原因是时间复杂度为O(n2),因为这个Collections的max()方法的时间复杂度就已经是O(n)了。后来我想怎么去简单的求解一个区域里的最大值呢,试了很多方法都不怎么行。
其实这个题的核心思想就是:不保证滑动窗口里的每一个值,但要保证滑动窗口里的可能最大值。
那么怎么去实现呢?我们的理想状态就是说,让这个队列呈现单调的趋势(也就是单调队列),最大值在出口处,滑动窗口每移动一次就把出口处的最大值给弹出去,然后再用一个结果集给收集起来,就大功告成了。想要到达这个理想状态,我们只需要将添加方法,和弹出方法自定义一下即可。
添加方法:每向队列尾端添加元素的时候,都与队尾的元素进行比较,如果队尾的元素比要添加进来的元素小,就把队尾的元素给弹出来,如此一直直到队尾的元素大于等于要添加进来的元素,再将元素从尾部添加即可(前提是队列不为空)。如此即可保持队列的单调型。
弹出方法:当滑动窗口进行移动的时候,要弹出元素,每次要进行弹出的时候都要判断,如果要弹出的这个元素与队列的头部元素(也就是此时队列的最大值)相同,就把队列的首部元素弹出;如果不等于,就不用管。(为什么这么做?这就体现了只对最大值的保护性,也就是如果弹出的跟你这个当前最大值没关系,那就不动他。如果随着滑动窗口的移动弹出来的就是这个最大值,那就要把他弹出来,此时的最大值自然要发生变化。)
窗口每移动一次就将队列头的最大值放入结果集即可。
public class LeetCode239Pro {
Deque<Integer> window = new LinkedList<>();
// 此法用双端队列模拟单调队列
public int[] maxSlidingWindow(int[] nums, int k) {
//创建结果集
LinkedList<Integer> result = new LinkedList<>();
// 将前k个元素先放入双端队列中
for(int i = 0;i < k;i++){
tianJia(nums[i]);
}
result.add(window.getFirst());
//窗口开始移动
for(int end = k;end < nums.length;end++){
tanChu(nums[end - k]);
tianJia(nums[end]);
result.add(window.getFirst());
}
return result.stream().mapToInt(Integer::valueOf).toArray();
}
// 添加方法
// 从尾端添加,添加的时候与尾部的元素进行大小比较,如果比尾部元素大,
// 就把尾部的元素弹出来,直到小于等于尾部元素即可。
public void tianJia(int num){
while(!window.isEmpty() && num > window.getLast() ){
window.pollLast();
}
window.addLast(num);
}
// 弹出方法
//弹出的时候与队列的头部元素进行的大小比较,如果相同则弹出,不相同就不用管。
// 中心思想:只维护最大值,但不维护窗口里的所有值
public void tanChu(int num){
if(window.getFirst() == num && !window.isEmpty()){
window.remove();
}
}
}
在写添加方法的时候,程序一直给我报java.util.NoSuchElementException,这就让我非常疑惑,琢磨了半天,最后发现问题就出在一个不容易发现的点上:
代码一:
while (!deque.isEmpty() && val > deque.getLast()) {
deque.removeLast();
}
代码二:
while (val > deque.getLast() && !deque.isEmpty() ) {
deque.removeLast();
}
这两个代码的区别就在我把&&前后两个布尔表达式换了一下位置,这就造成了代码一是可以运行的,而代码二是不能运行的。
这其中涉及到了两个知识点:
首先&&相对于&有个特点:短路,也就是说&&左边的布尔表达式的值一旦等于false,那么右边的是什么东西压根不会看,更不会去执行。如果基础不够扎实,心中总会存在这两个差不多无所谓用哪个的错误想法,因为平时编程序的时候也没报错,但如果一旦涉及这种错误根本找不到。
双端队列中的getLast()方法是会抛异常的,在队列为空的时候。
如此就可以知道为什么代码二会报错了。因为如果getLast()方法一旦报错,这个程序是一定不能继续执行的。而代码一能够执行,是因为他优先判断了队列是否为空,如果不为空它才会执行后面的,而如果为空,他压根就不会再执行后面的代码,getLast()也就不存在报错的可能性。
这种错误以后写代码的时候时一定要注意的。
如果用for循环一个个去转换再放入一个新的数组会让代码看起来冗余,这里可以用流的转换。
通用格式:
return 列表名.stream().mapToInt(Integer::valueOf).toArray();
用的少所以爱忘:
基本结构:
变量 = 布尔表达式 ? 如果布尔表达式为true的值:如果布尔表达式为false的值
高级用法:三元表达式也可以嵌套,例如两个值处均可以用三元表达式进行嵌套。