滑动窗口的最大值
题目
版本1 采用ArrayQueue作为双端队列 正确 有冗余操作
public int[] maxSlidingWindow(int[] nums, int k) {
// 滑动窗口的最大值
// nums中, 一个长度为k的滑动窗口, 从左向右移动, 输出每个时刻, 滑动窗口的最大值
// 这道题从目的上类似求有限制的子序列和那道题, 窗口k就是限制, 对于每个元素, 寻找距离k范围内的最大值
// 因此可以使用单调队列来实现, 队列中维护的是索引值i, 对应的顺序按照nums[i]从大到小排列
// 采用双端队列
ArrayDeque<Integer> queue = new ArrayDeque<>();
List<Integer> ans = new ArrayList<>();
for (int i = 0; i < nums.length; i ++) {
int preMax = Integer.MIN_VALUE;
// 弹出元素 弹出时, 需要校验索引是否在距离k之内
if (i >= k - 1 && !queue.isEmpty()) {
// 注意i - queue.peekFirst()是索引距离, 因此
while (!queue.isEmpty() && i - queue.peekFirst() + 1> k) {
queue.pollFirst();
}
if (!queue.isEmpty()) {
preMax = nums[queue.peek()];
}
}
if (i >= k - 1) {
// 需要记录最大值的结果
ans.add(Math.max(preMax, nums[i]));
}
// 添加元素到队列中
if (queue.isEmpty()) {
queue.offer(i);
} else {
if (nums[i] > nums[queue.peek()]) {
// 添加到队头
queue.addFirst(i);
} else {
// 从队尾开始比较
while (nums[queue.peekLast()] < nums[i]) {
queue.pollLast();
}
queue.offerLast(i);
}
}
}
int [] ansArray = new int[ans.size()];
for (int i = 0; i < ans.size(); i ++) {
ansArray[i] = ans.get(i);
}
return ansArray;
}
正确的原因
(1) 对于每个元素, 当窗口包括当前元素的时候, 之前窗口的最大值, 由双端队列头部元素得到
(2) 每个元素, 需要添加到双端队列中, 如果大于头部元素, 直接添加到队列头, 否则从队列尾部, 比较弹出所有比当前元素小的值, 然后添加到队列尾部
缺点
(1) 添加元素的时候无需从头部添加, 直接从队尾开始判断清空元素即可, 这样队列中就不用维护比较多的元素
(2) 用一个list来存储每次的结果, 最后再转成数组, 比较耗时
版本2 用的Deque 比较好的版本
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> deque = new LinkedList<Integer>();
// 结果的大小可以直接计算出来
int [] ans = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i ++) {
// 每次都从队列尾部开始比较, 将比当前值小的元素全部弹出, 队列中最少维护一个最近最大的元素即可
// 每次先添加当前元素, 这样直接获取队列的头部元素, 就是最大值了, 无需再比较了
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offerLast(i);
// 每次弹出前, 清理不在窗口中的索引
while (!deque.isEmpty() && i - deque.peekFirst() + 1 > k) {
deque.pollFirst();
}
if (i + 1 >= k) {
// 记录最大值
ans[i - k + 1] = nums[deque.peekFirst()];
}
}
return ans;
}
正确的原因
(1) 每次循环 先添加元素, 此时队列头部的元素就是结果, 无需比较一次
(2) 最终结果数组的大小就是nums.length - k + 1, 可以直接赋值
带限制的子序列和
题目
版本1 dp + 单调栈 正确
public int constrainedSubsetSum(int[] nums, int k) {
// 带限制的子序列的和, 构成结果的子序列, 任意两个数字之间的索引距离不能大于k
// 采用dp + 单调队列的解法
// dp[i] 表示以索引i对应的元素作为子序列结尾的, 子序列的和的最大值
int [] dp = new int[nums.length];
// base case
dp[0] = nums[0];
// 构造一个单调队列, 来维护一个索引
// 在队列中, 索引i的顺序是由dp[i]的大小决定的, 从大到小排列
Deque<Integer> queue = new LinkedList<>();
queue.offer(0);
// 遍历一遍nums, 记录以任何i结尾的子序列的最大值
// 注意这里最大值要设为第一个元素, 不然第一个元素就不会参与到最大值的比较了
int max = dp[0];
for (int i = 1; i < nums.length; i ++) {
// 弹出不符合索引条件的索引
// 这里题目要求 索引差值 <= k 即可
while (!queue.isEmpty() && i - queue.peekFirst() > k) {
queue.pollFirst();
}
// k最小是1, 那么队列中一定会有元素
// 队列头部的元素就是符合索引条件的之前的子序列和最大值
// 注意这里dp[queue.peekFirst()]可能是负值, 因此需要和0比较大小了再相加
// 也就是之前子序列的和, 可能不会使用
dp[i] = Math.max(0, dp[queue.peekFirst()]) + nums[i];
max = Math.max(max, dp[i]);
// 添加当前元素到队列中
while (!queue.isEmpty() && dp[queue.peekLast()] < dp[i]) {
queue.pollLast();
}
queue.offerLast(i);
}
return max;
}
正确的原因
(1) 注意int max = dp[0]; 如果单独把dp[0]挑出来作为base case, 然后循环的时候从1开始, 那么最大值应该设置为dp[0], 不然dp[0]就无法参与最大值的比较
(2) i - queue.peekFirst() > k, 题目要求的是索引差值 <= k, 因此要注意范围
(3) dp[i] = Math.max(0, dp[queue.peekFirst()]) + nums[i]; 需要注意, 根据dp数组的定义, 如果之前dp的最大值是小于0的, 是可以不实用之前的值的
最大宽度坡
题目
版本1 单调栈
public int maxWidthRamp(int[] A) {
if (A.length <= 1) {
return 0;
}
// 求数组中元素的宽度坡, 其实求得就是对于每个i, 找到距离它最远的j, 同时满足A[i] <= A[j]
// 那么对于每个i, 是不是都需要去找一次呢? 假设我当前A[i1] = 2, 那么A[i2] = 3, i2 > i1
// 此时i2的值需要去寻找j吗, 答案是不需要, 因为你找到的j 能满足A[j] > A[i2], 那么一定满足A[j] > A[i1]
// 并且i1得到的宽度, 显然比i2得到的宽度宽
// 那么我们可以采用一个单调栈, 从索引0开始存储数组中的降序的索引, 这样栈里的所有索引就是必须要计算一次宽度的
// 这样就大大减少了我们需要计算的i的数目
// 因此也就是如果在数组左边有比自己小的元素, 那么自己这个位置就不需要计算, 只需要计算左边最小的那个元素的宽度即可
// 栈中第一个元素一定是索引0
Stack<Integer> stack = new Stack<>();
stack.push(0);
for (int i = 1; i < A.length; i ++) {
// 单调栈中保存元素值降序的索引
if (A[i] < A[stack.peek()]) {
stack.push(i);
}
}
// 然后从尾部开始, 计算宽度, 即寻找对应的j
int max = Integer.MIN_VALUE;
for (int i = A.length - 1; i >= 0; i --) {
if (!stack.isEmpty() && A[i] >= A[stack.peek()] && i >= stack.peek()) {
max = Math.max(i - stack.pop(), max);
// 这里的i需要自增一下, 来抵消i -- 的效果, 让当前的元素重新进行一次判断
i ++;
}
}
return max;
}
正确的原因
(1) 通过单调栈 减少了可能的i
(2) 然后再从数组尾部遍历, 寻找可能的j, 注意在计算一次宽度后, 需要将当前元素再次和栈顶元素计算一次, 因为这种情况, 可能依旧满足A[j] >= A[栈顶元素]