题目描述(困难难度)

239. Sliding Window Maximum - 图1

输出每个窗口内的最大值。

解法一 暴力

两层 for 循环,每次都从窗口中找最大值即可。

  1. public int[] maxSlidingWindow(int[] nums, int k) {
  2. int n = nums.length;
  3. if (n == 0) {
  4. return nums;
  5. }
  6. int result[] = new int[n - k + 1];
  7. for (int i = 0; i < result.length; i++) {
  8. int max = Integer.MIN_VALUE;
  9. for (int j = 0; j < k; j++) {
  10. max = Math.max(max, nums[i + j]);
  11. }
  12. result[i] = max;
  13. }
  14. return result;
  15. }

时间复杂度的话是 O(nk)

解法二优先队列

注意到我们每次循环都寻找最大的值,自然的可以想到优先队列,这样得到最大值就是 O(1) 了。

当优先队列的数字等于窗口大小的时候,我们只需要将第一个元素删除,然后将新的数字加入。

  1. public int[] maxSlidingWindow(int[] nums, int k) {
  2. // 建立最大堆
  3. Queue<Integer> max = new PriorityQueue<Integer>(new Comparator<Integer>() {
  4. @Override
  5. public int compare(Integer i1, Integer i2) {
  6. // TODO Auto-generated method stub
  7. return i2 - i1;
  8. }
  9. });
  10. int n = nums.length;
  11. if (n == 0) {
  12. return nums;
  13. }
  14. int result[] = new int[n - k + 1];
  15. int index = 0;
  16. for (int i = 0; i < n; i++) {
  17. //移除第一个元素
  18. if (max.size() >= k) {
  19. max.remove(nums[i - k]);
  20. }
  21. max.offer(nums[i]);
  22. //更新 result
  23. if (i >= k - 1) {
  24. result[index++] = max.peek();
  25. }
  26. }
  27. return result;
  28. }

时间复杂度的话,循环中主要是调用了 remove 函数和 offer 函数,虽然 offer 函数的时间复杂度是 log 级的,但是 removeO(k) ,所以最终的时间复杂度依旧是 O(nk)

218 题 一样,我们可以用 TreeMap 代替优先队列,这样删除的时间复杂度也就是 log 级了。

  1. public int[] maxSlidingWindow(int[] nums, int k) {
  2. TreeMap<Integer, Integer> treeMap = new TreeMap<>(new Comparator<Integer>() {
  3. @Override
  4. public int compare(Integer i1, Integer i2) {
  5. return i2 - i1;
  6. }
  7. });
  8. int n = nums.length;
  9. if (n == 0) {
  10. return nums;
  11. }
  12. int result[] = new int[n - k + 1];
  13. int index = 0;
  14. for (int i = 0; i < n; i++) {
  15. //此时不能用 treeMap 的大小和 k 比较, 因为 nums 中有相等的元素
  16. //当 i >= k 的时候每次都需要删除一个元素
  17. if (i >= k) {
  18. Integer v = treeMap.get(nums[i - k]);
  19. if (v == 1) {
  20. treeMap.remove(nums[i - k]);
  21. } else {
  22. treeMap.put(nums[i - k], v - 1);
  23. }
  24. }
  25. //添加元素
  26. Integer v = treeMap.get(nums[i]);
  27. if (v == null) {
  28. treeMap.put(nums[i], 1);
  29. } else {
  30. treeMap.put(nums[i], v + 1);
  31. }
  32. //更新 result
  33. if (i >= k - 1) {
  34. result[index++] = treeMap.firstKey();
  35. }
  36. }
  37. return result;
  38. }

此时时间复杂度就是 O(nlog(k)) 了。

解法三 单调队列

参考 这里-solution-using-deque-with-explanation)。

我们可以用一个单调递减队列。单调递减队列添加元素的算法如下。

如果当前元素比队列的最后一个元素大,那么就将最后一个元素出队,重复这步直到当前元素小于队列的最后一个元素或者队列为空。进行下一步。

如果当前元素小于等于队列的最后一个元素或者队列为空,那么就直接将当前元素入队。

按照上边的方法添加元素,队列中的元素就刚好是一个单调递减的序列,而最大值就刚好是队头的元素。

当队列的元素等于窗口的大小的时候,由于添加元素的时候我们进行了出队操作,所以我们不能像解法二那样每次都删除第一个元素,需要先判断一下队头元素是否是我们要删除的元素。

  1. public int[] maxSlidingWindow(int[] nums, int k) {
  2. Deque<Integer> max = new ArrayDeque<>();
  3. int n = nums.length;
  4. if (n == 0) {
  5. return nums;
  6. }
  7. int result[] = new int[n - k + 1];
  8. int index = 0;
  9. for (int i = 0; i < n; i++) {
  10. if (i >= k) {
  11. if (max.peekFirst() == nums[i - k]) {
  12. max.removeFirst();
  13. }
  14. }
  15. while (!max.isEmpty() && nums[i] > max.peekLast()) {
  16. max.removeLast();
  17. }
  18. max.addLast(nums[i]);
  19. if (i >= k - 1) {
  20. result[index++] = max.peekFirst();
  21. }
  22. }
  23. return result;
  24. }

时间复杂度就是 O(n)了,因为每个元素最多只会添加到队列和从队列删除两次操作。

解法四

参考 这里-solution-in-Java-with-two-simple-pass-in-the-array) ,一种神奇的解法,有点 238 题 解法二的影子。

我们把数组 k 个一组就行分组。

  1. nums = [1,3,-1,-3,5,3,6,7], and k = 3
  2. | 1 3 -1 | -5 4 3 | 5 7 |
  3. 如果我们要求 result[1],也就是下边 i j 范围内的数字的最大值
  4. | 1 3 -1 | -5 4 3 | 5 7 |
  5. ^ ^
  6. i j
  7. i j 范围内的数字被分割线分成了两部分

如果我们知道了左半部的最大值和右半部分的最大值,那么两个选最大的即可。

左半部分的最大值,也就是当前数到它右边界范围内的最大值。

rightMax[i] 保存 i 到它的右边界范围内的最大值,只需要从右到左遍历一遍就可以求出来了。

每次到右边界的时候就开始重新计算 max 值。

  1. int rightMax[] = new int[n];
  2. max = Integer.MIN_VALUE;
  3. for (int i = n - 1; i >= 0; i--) {
  4. if (i % k == 0) {
  5. max = Integer.MIN_VALUE;
  6. }
  7. if (max < nums[i]) {
  8. max = nums[i];
  9. }
  10. rightMax[i] = Math.max(nums[i], max);
  11. }

同理,右半部分的最大值,也就是当前数到它左边界范围内的最大值。

leftMax[i] 保存 i 到它的左边界范围内的最大值,只需要从左到右遍历一遍就可以求出来。

每次到左边界的时候就开始重新计算 max 值。

  1. int leftMax[] = new int[n];
  2. int max = Integer.MIN_VALUE;
  3. for (int i = 0; i < n; i++) {
  4. if (i % k == 0) {
  5. max = Integer.MIN_VALUE;
  6. }
  7. if (max < nums[i]) {
  8. max = nums[i];
  9. }
  10. leftMax[i] = Math.max(nums[i], max);
  11. }

有了上边的两个数组,当前范围的两个边界 ijrightMax[i] 存储的就是左半部分(i 到右边界)的最大值,leftMax[j] 存储的就是右半部分(j 到左边界)的最大值。result[i] 的结果也就出来了。

  1. result[i] = Math.max(rightMax[i], leftMax[j]);

代码的话,把上边的部分结合起来即可。

  1. public int[] maxSlidingWindow(int[] nums, int k) {
  2. int n = nums.length;
  3. if (n == 0) {
  4. return nums;
  5. }
  6. //当前数到自己的左边界的最大值
  7. int leftMax[] = new int[n];
  8. int max = Integer.MIN_VALUE;
  9. for (int i = 0; i < n; i++) {
  10. if (i % k == 0) {
  11. max = Integer.MIN_VALUE;
  12. }
  13. if (max < nums[i]) {
  14. max = nums[i];
  15. }
  16. leftMax[i] = Math.max(nums[i], max);
  17. }
  18. //当前数到自己的右边界的最大值
  19. int rightMax[] = new int[n];
  20. max = Integer.MIN_VALUE;
  21. for (int i = n - 1; i >= 0; i--) {
  22. if (i % k == 0) {
  23. max = Integer.MIN_VALUE;
  24. }
  25. if (max < nums[i]) {
  26. max = nums[i];
  27. }
  28. rightMax[i] = Math.max(nums[i], max);
  29. }
  30. int result[] = new int[n - k + 1];
  31. for (int i = 0; i < result.length; i++) {
  32. int j = i + k - 1;
  33. result[i] = Math.max(rightMax[i], leftMax[j]);
  34. }
  35. return result;
  36. }

时间复杂度和解法三一样是 O(n)

解法一和解法二的话是正常的思路,一步一步水到渠成。

解法三的话直觉上其实也能意识到,我开始想到了单调栈,但一开始代码写错了,然后就换思路了,有点可惜,如果单调栈写对的话,应该可以写出解法三。

解法四的话就很厉害了,一般情况下很少往那方面想,不过这种将解分割的思想也是常用的。

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情