1 Star 0 Fork 0

苏落凡 / 拉勾私教作业

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
滑动窗口.md 8.51 KB
一键复制 编辑 原始数据 按行查看 历史
帅到你自杀 提交于 2021-08-29 15:14 . 滑动窗口

滑动窗口

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/sliding-window-maximum

示例 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

第一种方案--双指针遍历法(会超时)

对于每个滑动窗口,我们可以使用 O(k) 的时间遍历其中的每一个元素,找出其中的最大值。对于长度为 n 的数组 nums 而言,窗口的数量为 n-k+1,因此该算法的时间复杂度为 O((n-k+1)k)=O(nk),会超出时间限制

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
		//定义结果集
        List<Integer> list = new ArrayList();
        //定义窗口左右边界的指针
        int left = 0,right = k-1;
        
        while(right < nums.length){
        	//将窗口最大值添加到结果集
            list.add(getMax(nums,left,right));
            //通过指针移动窗口
            right++;
            left++;
        }

		//list转数组
        return list.stream().mapToInt(Integer::intValue).toArray();
   }
	/**
	 * 获取数组指定区间的最大值
	 */
    public int getMax(int[] nums,int start,int end){
        int max = nums[start];
        for(int i = start+1;i<=end;i++){
            max = Math.max(max,nums[i]);
        }
        return max;
    }
}

第二种方案--优先队列(大顶堆)

对于「最大值」,我们可以想到一种非常合适的数据结构,那就是优先队列(堆),其中的大根堆可以帮助我们实时维护一系列元素中的最大值。

对于本题而言,初始时,我们将数组 nums 的前 k -1个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums 中的位置出现在滑动窗口左边界的左侧。因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其永久地从优先队列中移除。

我们不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组 (num,index),表示元素 num 在数组中的下标为 index。

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
    	//定义大顶堆,堆内元素存储数组,数组第一位存储nums的值,第二位存储nums的下标
		PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>(){
            public int compare(int[] o1,int[] o2){
            	//根据nums值排序
                return o2[0] - o1[0];
            }
        });
		//初始化,先将前k-1个元素放到大顶堆中
        for(int i=0;i<k-1;i++){
            queue.offer(new int[]{nums[i],i});
        }
		//定义结果集
        int[] ans = new int[nums.length - k + 1];
        
        for(int i = k - 1;i<nums.length;i++){
            queue.offer(new int[]{nums[i],i});
            while(queue.peek()[1] < i - k + 1){
                queue.poll();
            }
            ans[i-k +1] = queue.peek()[0];
        }

        return ans;
   }
}

复杂度分析

时间复杂度:O(nlogn),其中 n 是数组 nums 的长度。在最坏情况下,数组 nums 中的元素单调递增,那么最终优先队列中包含了所有元素,没有元素被移除。由于将一个元素放入优先队列的时间复杂度为 O(logn),因此总时间复杂度为 O(nlogn)。

空间复杂度:O(n),即为优先队列需要使用的空间。这里所有的空间复杂度分析都不考虑返回的答案需要的 O(n) 空间,只计算额外的空间使用。

第三种方案--单调队列

当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果前者大于等于后者,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。

由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。但与方法一中相同的是,此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。

为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
    	//定义单调队列
        MonotoneQueue window = new MonotoneQueue();
        //定义结果集
        int[] ans = new int[nums.length - k + 1];
        for(int i = 0;i<nums.length;i++){
            if(i<k-1){
            	//初始化,将前k-1个元素放到单调队列中
                window.push(nums[i]);
            }else{
                window.push(nums[i]);
            	//存储结果集
                ans[i - k + 1] = window.max();
                //保证队列少于k个
                window.pop(nums[i-k+1]);
            }
        }
        return ans;
    }

    public class MonotoneQueue{
        Deque<Integer> deque = new LinkedList<>();
        
        //尾部添加元素
        public void push(int n){
            while (!deque.isEmpty() && deque.peekLast() < n){
                deque.pollLast();
            }
            deque.addLast(n);
        }

        //弹出队列顶部元素
        public void pop(int n){
            if(deque.peekFirst() == n){
                deque.pollFirst();
            }
        }

        //获取队列最大值
        public int max(){
            return deque.peekFirst();
        }
    }
}

复杂度分析

**时间复杂度:**O(n),其中 n 是数组nums 的长度。每一个下标恰好被放入队列一次,并且最多被弹出队列一次,因此时间复杂度为 O(n)。

**空间复杂度:**O(k)。与方法二不同的是,在方法三中我们使用的数据结构是双向的,因此「不断从队首弹出元素」保证了队列中最多不会有超过 k 个元素,因此队列使用的空间为 O(k)。

扩展:ArrayList和LinkedList的区别

1. ArrayList是实现了基于动态数组的数据结构,而LinkedList是基于双向链表的数据结构;

2. 对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针;

3. 对于添加和删除操作add和remove,一般大家都会说LinkedList要比ArrayList快,因为ArrayList要移动数据


一般首选用ArrayList,由于LinkedList可以实现栈、队列以及双端队列等数据结构,所以当特定需要时候,使用LinkedList

滑动窗口/单调队列练习题:

3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 **最长子串 **的长度。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/

提示:双指针

1638.绝对差不超过限制的最长连续子数组

给你一个整数数组 nums ,和一个表示限制的整数 limit,请你返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。

如果不存在满足条件的子数组,则返回 0 。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit 提示:使用两个单调队列

马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/suluofan/homework.git
git@gitee.com:suluofan/homework.git
suluofan
homework
拉勾私教作业
master

搜索帮助

344bd9b3 5694891 D2dac590 5694891