PS:《剑指offer》是很多同学找工作都会参考的一本面试指南,同时也是一本算法指南(为什么它这么受欢迎,主要应该是其提供了一个循序渐进的优化解法,这点我觉得十分友好)。现在很多互联网的算法面试题基本上可以在这里找到影子,为了以后方便参考与回顾,现将书中例题用Java实现(第二版),欢迎各位同学一起交流进步。
GitHub: https://github.com/Uplpw/SwordOffer。
剑指offer完整题目链接: https://blog.csdn.net/qq_41866626/article/details/120415258
1 题目描述
给定一个数组和滑动窗口的大小,请找出所有滑动窗口的最大值。例如,输入数组{2,3,4,2,6,2,5,1}和数字3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}。
leetcode链接: 滑动窗口的最大值(以下代码已测试,提交通过)
2 测试用例
一般是考虑功能用例,特殊(边缘)用例或者是反例,无效测试用例这三种情况。甚至可以从测试用例寻找一些规律解决问题,同时也可以让我们的程序更加完整鲁棒。
(1)功能用例:数组包含多个无序的数字。
(2)边缘用例:单个元素的数字,或者k=1。
(3)无效用例:数组为空,或者k大于数组长度。
3 思路
分析:
下面介绍两种解决方式。
解法1:暴力求解
一直最直接直观的方式就是暴力求解,使用双重循环进行解决,外层循环数组有多少个滑动窗口,内层计算每个窗口的最大值。
另外也可以对其进行优化,首先计算出第一个滑动窗口的最大值,在计算下一个滑动窗口的最大值时,判断上一个最大值与当前滑动窗口最左边的上一个元素大小关系:
- 如果不等,判断上一个最大值与滑动窗口新增元素关系,如果大于等于,则当前窗口最大值等于上一个最大值,否则当前窗口最大值等于新增元素值。
- 如果相等,则需要重新遍历当前窗口的元素,计算最大值。
时间空间复杂度为 O ( n k ) , O ( 1 ) O(nk),O(1) O(nk),O(1)
解法2:利用有序队列
时间复杂度能不能再次优化,关键在于如何在 O ( 1 ) O(1) O(1)的时间内在滑动窗口找到最大值。
回忆之前的题目包含 min 函数的栈 ,其使用 单调栈 实现了随意入栈、出栈情况下的 O(1) 时间获取 “栈内最小值” 。本题同理,不同点在于 “出栈操作” 删除的是 “列表尾部元素” ,而 “窗口滑动” 删除的是 “列表首部元素” 。
窗口对应的数据结构为 双端队列 ,本题使用 单调队列 即可解决以上问题。遍历数组时,每轮保证单调队列 deque :
- deque 内 仅包含窗口内的元素 ⇒ 每轮窗口滑动移除了元素 nums[i - 1] ,需将 deque 内的对应元素一起删除。
- deque 内的元素 非严格递减 ⇒ 每轮窗口滑动添加了元素 nums[j + 1] ,需将 deque 内所有 < nums[j + 1] 的元素删除。
单调队列 注意:
(1)队列按从大到小放入
(2) 如果首位值(即最大值)不在窗口区间,删除首位
(3)如果新增的值小于队列尾部值,加到队列尾部
(4)如果新增值大于队列尾部值,删除队列中比新增值小的值,如果在把新增值加入到队列中
(5)如果新增值大于队列中所有值,删除所有,然后把新增值放到队列首位,保证队列一直是从大到小
时间空间复杂度为 O ( n ) , O ( k ) O(n),O(k) O(n),O(k)
4 代码
算法实现:
import java.util.Deque;
import java.util.LinkedList;
public class MaxInSlidingWindow {
// 解法1:暴力求解
public static int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0 || nums.length < k || k <= 0) {
return new int[0];
}
if (k == 1) {
return nums;
}
int length = nums.length;
int len = length - k + 1;
int[] array = new int[len];
array[0] = nums[0];
for (int j = 0; j < k; j++) {
array[0] = (array[0] < nums[j]) ? nums[j] : array[0];
}
for (int i = 1; i < len; i++) {
if (array[i - 1] != nums[i - 1]) {
if (array[i - 1] >= nums[i + k - 1]) {
array[i] = array[i - 1];
} else {
array[i] = nums[i + k - 1];
}
} else {
int tempLength = i + k;
array[i] = nums[i];
for (int j = i + 1; j < tempLength; j++) {
array[i] = (array[i] < nums[j]) ? nums[j] : array[i];
}
}
}
return array;
}
// 解法2:利用有序队列
public static int[] maxSlidingWindow1(int[] nums, int k) {
if(nums.length == 0 || k == 0) return new int[0];
Deque<Integer> deque = new LinkedList<>();
int[] res = new int[nums.length - k + 1];
// 未形成窗口
for(int i = 0; i < k; i++) {
// 队列不为空,新增值大于队列尾部值,删除队列中比新增值小的值,如果在把新增值加入到队列中
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);
}
res[0] = deque.peekFirst();
// 形成窗口后
for(int i = k; i < nums.length; i++) {
// 如果首位值(即最大值)不在窗口区间,删除首位
if(deque.peekFirst() == nums[i - k])
deque.removeFirst();
// 队列不为空,新增值大于队列尾部值,删除队列中比新增值小的值,如果在把新增值加入到队列中
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);
// 窗口最大值等于队列首部元素
res[i - k + 1] = deque.peekFirst();
}
return res;
}
public static void main(String[] args) {
int[] arr = maxSlidingWindow(new int[]{1, 3, -1, -3, 5, 3, 6, 7}, 3);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println();
}
}
参考
在解决本书例题时,参考了一些大佬的题解,比如leetcode上的官方、K神,以及其他的博客,在之后的每个例题详解后都会给出参考的思路或者代码链接,同学们都可以点进去看看!
本例题参考:
本文如有什么不足或不对的地方,欢迎大家批评指正,最后希望能和大家一起交流进步、拿到心仪的 offer !!!