目录
栈与队列
1 滑动窗口的最大值
剑指 Offer 59 - I. 滑动窗口的最大值https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/
1. 暴力
模拟一下,按照每一次滑动的初始位置来求最大值即可。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0) return new int[0];
if (1 == k) return nums;
int i = 0;
int max = 0;
int t = 0;
int[] res = new int[nums.length - k + 1];
int w = 0;
while ((i+k)<= nums.length){
// if(t > max) max = t;
max = nums[i];
for (int l = i; l < i+k; l++) {
max = Math.max(max, nums[l]);
}
i++;
res[w++] = max;
}
return res;
}
}
2. 模拟-保存窗口最大值
如果上一个窗口里,最大值不是第一个值,则可以直接进行max(前一个窗口最大值,新滑进来元素)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0) return new int[0];
if (1 == k) return nums;
int i = 0;
int max = 0;
int[] res = new int[nums.length - k + 1];
int w = 0;
for (int l = i; l < i+k; l++) {
max = Math.max(max, nums[l]);
}
res[w++] = max;
i++;
while ((i+k)<= nums.length){
if(res[w-1] == nums[i-1]){
max = nums[i];
for (int l = i; l < i+k; l++) {
max = Math.max(max, nums[l]);
}
}else{
max = Math.max(res[w-1], nums[i+k-1]);
}
i++;
res[w++] = max;
}
return res;
}
}
3. 单调队列
- 设数组 nums 的长度为 n ,则共有 (n-k+1) 个窗口;
- 获取每个窗口最大值需线性遍历,时间复杂度为 O(k) 。
本题难点: 如何在每次窗口滑动后,将 “获取窗口内最大值” 的时间复杂度从 O(k) 降低至 O(1) 。
回忆 剑指Offer 30. 包含 min 函数的栈 ,其使用 单调栈 实现了随意入栈、出栈情况下的 O(1) 时间获取 “栈内最小值” 。本题同理,不同点在于 “出栈操作” 删除的是 “列表尾部元素” ,而 “窗口滑动” 删除的是 “列表首部元素” 。
窗口对应的数据结构为 双端队列 ,本题使用 单调队列 即可解决以上问题。遍历数组时,每轮保证单调队列 deque :
- deque 内 仅包含窗口内的元素 ⇒ 每轮窗口滑动移除了元素 nums[i−1] ,需将 deque 内的对应元素一起删除。
- deque 内的元素 非严格递减 ⇒ 每轮窗口滑动添加了元素nums[j+1] ,需将 deque内所有 < nums[j + 1]的元素删除。
算法流程:
个人感觉不存deque,只需要max进行比较存储 ,再根据不同情况,比较或遍历k个数字即可。
class Solution {
public int[] maxSlidingWindow(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 j = 0, i = 1 - k; j < nums.length; i++, j++) {
// 删除 deque 中对应的 nums[i-1]
if(i > 0 && deque.peekFirst() == nums[i - 1])
deque.removeFirst();
// 保持 deque 递减
while(!deque.isEmpty() && deque.peekLast() < nums[j])
deque.removeLast();
deque.addLast(nums[j]);
// 记录窗口最大值
if(i >= 0)
res[i] = deque.peekFirst();
}
return res;
}
}
4. 单调队列 优化
将 “未形成窗口” 和 “形成窗口后” 两个阶段拆分到两个循环里实现。代码虽变长,但减少了冗余的判断操作。
class Solution {
public int[] maxSlidingWindow(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;
}
}
5. 官方题解
对于每个滑动窗口,可以使用 O(k) 的时间遍历其中的每一个元素,找出其中的最大值。对于长度为 n 的数组 nums 而言,窗口的数量为 n-k+1 ,因此该算法的时间复杂度为 O((n−k+1)k)=O(nk),会超出时间限制,因此我们需要进行一些优化。
可以想到,对于两个相邻(只差了一个位置)的滑动窗口,它们共用着 k-1 个元素,而只有 1 个元素是变化的。可以根据这个特点进行优化。(方法1、2我就是根据这个来做的~)
5.1 优先队列
对于「最大值」,可以想到一种非常合适的数据结构,那就是优先队列(堆),其中的大根堆可以帮助我们实时维护一系列元素中的最大值。
对于本题而言,初始时,将数组 nums 的前 k 个元素放入优先队列中。每当向右移动窗口时,就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums 中的位置出现在滑动窗口左边界的左侧。因此,当后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,可以将其永久地从优先队列中移除。
不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组(num,index),表示元素 num 在数组中的下标为 index。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
if(n == 0 || k == 0) return new int[0];
PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] pair1, int[] pair2) {
return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1];
}
});
for (int i = 0; i < k; ++i) {
pq.offer(new int[]{nums[i], i});
}
int[] ans = new int[n - k + 1];
ans[0] = pq.peek()[0];
for (int i = k; i < n; ++i) {
pq.offer(new int[]{nums[i], i});
while (pq.peek()[1] <= i - k) {
pq.poll();
}
ans[i - k + 1] = pq.peek()[0];
}
return ans;
}
}
延伸一题「大根堆的应用」
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
1. 建立大顶堆
大顶堆执行
poll()
去掉的是大数,留下的是小数。构造方法传入(w1, w2) -> w2 - w1 // 第二个参数减第一个是大顶堆
执行
heap.poll()这样
会将大的数出队,剩下的是小的数据。
import java.util.*;
public class Solution {
public static void main(String[] args) {
int[] arr = new int[]{3,2,1,6,9};
int[] res = getLeastNumbers(arr, 4);
System.out.println(Arrays.toString(res));
}
public static int[] getLeastNumbers(int[] arr, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>(
(w1, w2) -> w2 - w1 // 第二个参数减第一个是大顶堆
);
for (int value : arr) {
heap.offer(value);
if (heap.size() > k) heap.poll();
}
int[] res = new int[k];
int j = 0;
while (! heap.isEmpty()) {
res[j++] = heap.poll();
}
return res;
}
}
2. 建立小顶堆
PriorityQueue
默认建立小顶堆
执行poll()
出队的是值小的数据,保留的是值大的数据
import java.util.*;
public class Solution {
public static void main(String[] args) {
int[] arr = new int[]{3,2,1,6,9};
int[] res = getLeastNumbers(arr, 3);
System.out.println(Arrays.toString(res));
}
public static int[] getLeastNumbers(int[] arr, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int value : arr) {
heap.offer(value);
if (heap.size() > k) heap.poll();
}
int[] res = new int[k];
int j = 0;
while (! heap.isEmpty()) {
res[j++] = heap.poll();
}
return res;
}
}
5.2 单调队列
顺着方法一的思路继续进行优化。
也就是方法 4 。以上是官方题解的说法。
5.3 分块 + 预处理
可以将数组 nums从左到右按照 k 个一组进行分组,最后一组中元素的数量可能会不足 k 个。如果我们希望求出 nums[i] 到 nums[i+k−1] 的最大值,就会有两种情况:
这一手预处理 太秀了
// int[] nums = new int[]{1,3,-1,-3,5,3,6,7};
public int[] maxSlidingWindow(int[] nums, int k) {
System.out.println(Arrays.toString(nums));
int n = nums.length;
int[] prefixMax = new int[n];//表示下标 i 对应的分组中,以 i 结尾的前缀最大值
int[] suffixMax = new int[n];//表示下标 i 对应的分组中,以 i 开始的后缀最大值
for (int i = 0; i < n; ++i) {
if (i % k == 0) {
prefixMax[i] = nums[i];
}
else {
prefixMax[i] = Math.max(prefixMax[i - 1], nums[i]);
}
}
System.out.println(Arrays.toString(prefixMax));
for (int i = n - 1; i >= 0; --i) {
if (i == n - 1 || (i + 1) % k == 0) {
suffixMax[i] = nums[i];
} else {
suffixMax[i] = Math.max(suffixMax[i + 1], nums[i]);
}
}
System.out.println(Arrays.toString(suffixMax));
int[] ans = new int[n - k + 1];
for (int i = 0; i <= n - k; ++i) {
ans[i] = Math.max(suffixMax[i], prefixMax[i + k - 1]);
}
return ans;
}
[1, 3, -1, -3, 5, 3, 6, 7]
[1, 3, 3, -3, 5, 5, 6, 7]
[3, 3, -1, 5, 5, 3, 7, 7]
[3, 3, 5, 5, 6, 7]
复杂度分析
- 时间复杂度:O(n),其中 n 是数组 nums 的长度。我们需要 O(n) 的时间预处理出数组 prefixMax,suffixMax 以及计算答案。
- 空间复杂度:O(n),即为存储 prefixMax 和 suffixMax 需要的空间。
2 滑动窗口的最大值
剑指 Offer 59 - II. 队列的最大值https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/
最直观的想法是 维护一个最大值变量 ,在元素入队时更新此变量即可;但当最大值出队后,并无法确定下一个 次最大值 ,因此不可行。
这里的想法是在pop_front()的时候,判断出的是不是当前最大值,如果是,需要进行最大值的一个更新。
1. 暴力更新
class MaxQueue {
int max = Integer.MIN_VALUE;
Deque<Integer> deque = null;
public MaxQueue() {
deque = new LinkedList<>();
}
public int max_value() {
if(deque.isEmpty()) return -1;
return max;
}
public void push_back(int value) {
if(max < value){
max = value;
}
deque.push(value);
}
public int pop_front() {
if(deque.isEmpty()) return -1;
int res = deque.pollLast();
if(res == max){
max = Integer.MIN_VALUE;
Deque<Integer> deque2 = new LinkedList<>();
while(!deque.isEmpty()){
int t = deque.pop();
if(max < t) max = t;
deque2.push(t);
}
while (!deque2.isEmpty()) {
deque.push(deque2.pop());
}
}
return res;
}
}
/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue obj = new MaxQueue();
* int param_1 = obj.max_value();
* obj.push_back(value);
* int param_3 = obj.pop_front();
*/
2. 递减队列
构建一个递减列表来保存队列 所有递减的元素 ,递减链表随着入队和出队操作实时更新,这样队列最大元素就始终对应递减列表的首元素,实现了获取最大值 O(1) 时间复杂度。
使用双向队列原因:维护递减列表需要元素队首弹出、队尾插入、队尾弹出操作皆为 O(1) 时间复杂度。
- 入队时,递减队列和队列queue都push,但是递减队列要移除之前入队了的所有小于新入队的元素,保持递减。
- 出队时,如果递减队列和队列队首相等,则一同出队,否则,只有队列出队。
- 求max的时候,返回递减队列队首元素。
class MaxQueue {
Queue<Integer> queue;
Deque<Integer> deque;
public MaxQueue() {
queue = new LinkedList<>();
deque = new LinkedList<>();
}
public int max_value() {
return deque.isEmpty() ? -1 : deque.peekFirst();
}
public void push_back(int value) {
queue.offer(value);
while(!deque.isEmpty() && deque.peekLast() < value)
deque.pollLast();
deque.offerLast(value);
}
public int pop_front() {
if(queue.isEmpty()) return -1;
if(queue.peek().equals(deque.peekFirst()))
deque.pollFirst();
return queue.poll();
}
}