剑指offer(第二版)——滑动窗口的最大值

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神,以及其他的博客,在之后的每个例题详解后都会给出参考的思路或者代码链接,同学们都可以点进去看看!

本例题参考:

  1. K神题解

本文如有什么不足或不对的地方,欢迎大家批评指正,最后希望能和大家一起交流进步、拿到心仪的 offer !!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值