11.滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 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
示例 2:
输入:nums = [1], k = 1 输出:[1]
方法1: 优先队列
自定义Comparator实现大顶堆,offer 入堆,poll 出堆,peek堆顶元素
Comparator.compare(a, b)的返回值规则:
- return > 0认为 a “大于” b交换 a 和 b 的位置(把 b 放到 a 前面)
- return = 0认为 a “等于” b位置不变
- return < 0认为 a “小于” b位置不变(a 保持在 b 前面)
代码设计comparator可以实现比较元素和下标。
new Comparator<int[]>() { public int compare(int[] pair1, int[] pair2) { // 第一步:先比较数值(pair[0]是数值,pair[1]是下标) return pair1[0] != pair2[0] // 若新入堆的元素和堆顶元素不同,则比较大小,否则比较下标【下标大的放堆顶————新的放堆顶】 ? pair2[0] - pair1[0] // 数值不同时,按数值降序 : pair2[1] - pair1[1]; // 数值相同时,按下标降序 } };
先把前k 个入堆,在遍历k+1~n,逐个入堆更新堆顶,若堆顶元素下标在窗口外则出堆。
这段代码的核心是利用优先队列(大顶堆) 来维护滑动窗口内的元素,堆顶始终是当前堆中的最大值。滑动窗口右移时,新元素入堆;同时检查堆顶元素的下标是否在当前窗口外,若在则弹出,直到堆顶元素属于当前窗口,此时堆顶就是窗口最大值。
1 class Solution { 2 public int[] maxSlidingWindow(int[] nums, int k) { 3 // 1. 获取数组长度 4 int n = nums.length; 5 6 // 2. 定义优先队列(大顶堆),存储int[] {数值, 下标} 7 // 自定义Comparator:先按数值降序,数值相同时按下标降序(避免下标小的先出) 8 PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() { 9 public int compare(int[] pair1, int[] pair2) { 10 // 核心:pair2[0]-pair1[0] 实现降序(大顶堆) 11 return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1]; 12 } 13 }); 14 15 // 3. 初始化:将第一个窗口的k个元素入堆 16 for (int i = 0; i < k; ++i) { 17 pq.offer(new int[]{nums[i], i}); // Java中用offer()添加元素(比add更安全) 18 } 19 20 // 4. 初始化结果数组:长度 = 总元素数 - 窗口大小 + 1 21 int[] ans = new int[n - k + 1]; 22 ans[0] = pq.peek()[0]; // peek()获取堆顶元素(不弹出),取数值部分 23 24 // 5. 滑动窗口:处理从第k个元素开始的后续元素 25 for (int i = k; i < n; ++i) { 26 // 5.1 新元素入堆 27 pq.offer(new int[]{nums[i], i}); 28 29 // 5.2 循环检查堆顶元素是否在窗口外(窗口范围:[i-k+1, i]) 30 // 若堆顶下标 <= i-k,说明不在窗口内,弹出 31 while (pq.peek()[1] <= i - k) { 32 pq.poll(); // poll()弹出堆顶元素 33 } 34 35 // 5.3 堆顶即为当前窗口最大值,存入结果数组对应位置 36 ans[i - k + 1] = pq.peek()[0]; 37 } 38 39 // 6. 返回结果数组 40 return ans; 41 } 42 }
方法2: 单调队列(双端链表)
思路(俗话版):
队尾只要有更年轻(i越靠后)同时还能力更强(数值越大)的,留着其他比它更早入职同时能力却更差的没有什么意义,统统开了;队首的虽然能力最强,但是年龄最大,一旦发现它超过年龄范围(不在滑动窗口的范围内),不用看能力就可以直接开了。
nums[i]入队前,判断是不是队列里最小的,
若nums[i]比原队尾元素大,则移除队尾元素。【为什么这么做?这又属于更新队列里面的“最大”元素,新来的比原来的大,原来的肯定没机会成为最大的(队首)】。若nums[i]比原队尾元素小,则直接插入队尾。最后检查队首元素是否还在窗口内,不在则移除。
【向dque加入元素下标的核心代码】
1 // 因为当前元素更大,窗口内这些小元素不可能成为最大值,直接移除 2 while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) { 3 deque.pollLast(); // 弹出队列尾部元素 4 } 5 // 3.2 将当前元素下标加入队列尾部 6 deque.offerLast(i); 7 }
注意:
peekLast()获取队列尾部元素(不弹出)
pollLast()弹出队列尾部元素
offerLast()向队列尾部添加元素
peekFirst()获取队列头部元素(不弹出)
pollFirst()弹出队列头部元素
【deque中加入的是当前窗口中“有潜力”成为队首元素的下标】
1 class Solution { 2 public int[] maxSlidingWindow(int[] nums, int k) { 3 // 1. 获取数组长度 4 int n = nums.length; 5 6 // 2. 定义双端队列Deque,存储数组元素的下标(核心:队列内下标对应数值单调递减) 7 Deque<Integer> deque = new LinkedList<Integer>(); 8 9 // 3. 初始化:处理第一个窗口的k个元素,构建单调队列 10 for (int i = 0; i < k; ++i) { 11 // 3.1 关键:移除队列尾部所有数值 <= 当前元素的下标(保证队列单调递减) 12 // 因为当前元素更大,窗口内这些小元素不可能成为最大值,直接移除 13 while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) { 14 deque.pollLast(); // 弹出队列尾部元素 15 } 16 // 3.2 将当前元素下标加入队列尾部 17 deque.offerLast(i); 18 } 19 20 // 4. 初始化结果数组:第一个窗口的最大值是队列头部下标对应的数值 21 int[] ans = new int[n - k + 1]; 22 ans[0] = nums[deque.peekFirst()]; 23 24 // 5. 滑动窗口:处理从第k个元素开始的后续元素 25 for (int i = k; i < n; ++i) { 26 // 5.1 重复步骤3.1:移除尾部所有比当前元素小的下标,保证队列单调递减 27 while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) { 28 deque.pollLast(); 29 } 30 // 5.2 当前元素下标入队 31 deque.offerLast(i); 32 33 // 5.3 检查队列头部下标是否在窗口外(窗口范围:[i-k+1, i]) 34 // 若头部下标 <= i-k,说明不在窗口内,弹出 35 while (deque.peekFirst() <= i - k) { 36 deque.pollFirst(); 37 } 38 39 // 5.4 队列头部即为当前窗口最大值的下标,存入结果 40 ans[i - k + 1] = nums[deque.peekFirst()]; 41 } 42 43 // 6. 返回结果数组 44 return ans; 45 } 46 }

浙公网安备 33010602011771号