leecode更新
This commit is contained in:
parent
06d21ded9b
commit
26376573fb
|
|
@ -0,0 +1,331 @@
|
|||
package com.markilue.leecode.stackAndDeque;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @BelongsProject: Leecode
|
||||
* @BelongsPackage: com.markilue.leecode.stackAndDeque
|
||||
* @Author: dingjiawen
|
||||
* @CreateTime: 2022-09-14 09:31
|
||||
* @Description: TODO 力扣239题 滑动窗口最大值:
|
||||
* 给你一个整数数组 nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k个数字。滑动窗口每次只向右移动一位。
|
||||
* 返回 滑动窗口中的最大值 。
|
||||
* @Version: 1.0
|
||||
*/
|
||||
public class MaxSlidingWindow {
|
||||
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
int[] nums = {1, 3, -1, -3, -5, -7, -9, 5, 3, 6, 7};
|
||||
int k = 3;
|
||||
int[] ints = maxSlidingWindow(nums, k);
|
||||
System.out.println(Arrays.toString(ints));//[3, 3, -1, -3, -5, 5, 5, 6, 7]
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test1() {
|
||||
int[] nums = {1, 3, -1, -3, -5, -3, -9, 5, 3, 6, 7};//[3, 3, -1, -3, -3, 5, 5, 6, 7]
|
||||
int k = 3;
|
||||
int[] ints = maxSlidingWindow3(nums, k);
|
||||
System.out.println(Arrays.toString(ints));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test4() {
|
||||
int[] nums = {1, 3, -1, -3, -3, -3, -9, 5, 3, 6, 7};//[3, 3, -1, -3, -3, 5, 5, 6, 7]
|
||||
int k = 3;
|
||||
int[] ints = maxSlidingWindow(nums, k);
|
||||
System.out.println(Arrays.toString(ints));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test2() {
|
||||
int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
|
||||
int k = 3;
|
||||
int[] ints = maxSlidingWindow(nums, k);
|
||||
System.out.println(Arrays.toString(ints));//[3, 3, 5, 5, 6, 7]
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test3() {
|
||||
int[] nums = {1};
|
||||
int k = 1;
|
||||
int[] ints = maxSlidingWindow(nums, k);
|
||||
System.out.println(Arrays.toString(ints));//[1]
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test5() {
|
||||
int[] nums = {1, 3, 1, 2, 0, 5};
|
||||
int k = 3;
|
||||
int[] ints = maxSlidingWindow4(nums, k);
|
||||
System.out.println(Arrays.toString(ints));//[3, 3, 2, 5]
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test6() {
|
||||
int[] nums = {7,2,4};
|
||||
int k = 2;
|
||||
int[] ints = maxSlidingWindow3(nums, k);
|
||||
System.out.println(Arrays.toString(ints));//[3, 3, 2, 5]
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 本人思路:滑动窗口看似需要知道全部的值,实际上只需要知道连续增大的数及其他的索引即可:
|
||||
* 例如[1,3,-1,-3,5,3,6,7] ,事实上真正重要的信息是[1,3,5,6,7]以及他们的索引[0,1,4,6,7]
|
||||
* 可以发现的是这个数是否被使用可以看其索引和左右的差值 如(1-0)+(4-1-1)=3 (3+1)-3+1=2所以3使用2次;同理,(4-1-1)+(6-4-1)=3 (3+1)-3+1=2所以5使用2次
|
||||
* 其中添加了太多逻辑判断,导致逻辑混乱,目前还有问题
|
||||
*
|
||||
* @param nums
|
||||
* @param k
|
||||
* @return
|
||||
*/
|
||||
public int[] maxSlidingWindow(int[] nums, int k) {
|
||||
|
||||
//使用一个栈,用于存放进来的数据
|
||||
Stack<Integer> stack = new Stack<>();
|
||||
|
||||
boolean flag = false;
|
||||
int lastIndex = 0;
|
||||
//记录stack栈顶的数的索引
|
||||
int index = 0;
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
if (stack.empty()) {
|
||||
stack.push(nums[i]);
|
||||
//被迫放的,将flag改一下
|
||||
flag = true;
|
||||
index = i;
|
||||
continue;
|
||||
}
|
||||
//一个数与栈顶元素的索引之差大于3,那么就把他固化下来,并且把他的下一个元素放进去,并设置flag
|
||||
if (i - index >= k && flag) {
|
||||
//将flag设置回来
|
||||
// flag=true;
|
||||
// index=i-k;
|
||||
lastIndex = index;
|
||||
stack.push(nums[i - k + 1]);
|
||||
//再把flag设置回去
|
||||
index = i - k + 1;
|
||||
}
|
||||
|
||||
if (nums[i] >= stack.peek() && !flag) {
|
||||
//前面的数是想要的数
|
||||
lastIndex = index;
|
||||
stack.push(nums[i]);
|
||||
index = i;
|
||||
continue;
|
||||
} else if (nums[i] >= stack.peek() && flag) {
|
||||
//前面的数不是想要的数
|
||||
stack.pop();
|
||||
|
||||
if (stack.empty() || stack.peek() < nums[i]) {
|
||||
lastIndex = index;
|
||||
flag = false;
|
||||
}
|
||||
index = i;
|
||||
stack.push(nums[i]);
|
||||
// lastIndex=index;
|
||||
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nums[i] < stack.peek() && !flag) {
|
||||
//栈顶元素想要,再放一次
|
||||
stack.push(stack.peek());
|
||||
//这个元素可能要,所以放一下
|
||||
lastIndex = index;
|
||||
stack.push(nums[i]);
|
||||
index = i;
|
||||
flag = true;
|
||||
continue;
|
||||
} else if (nums[i] < stack.peek() && flag && index - lastIndex + (i - index) + 1 > k) {
|
||||
//中间的数是大数,但是没有上一次的大,但是已经达到范围了,就将这个数设置成想要的
|
||||
flag = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int[] result = new int[nums.length - k + 1];
|
||||
|
||||
for (int i = result.length - 1; i >= 0; i--) {
|
||||
result[i] = stack.pop();
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 官方使用优先队列完成的做法:时间复杂度O(nlogn)
|
||||
* 对于「最大值」,我们可以想到一种非常合适的数据结构,那就是优先队列(堆),其中的大根堆可以帮助我们实时维护一系列元素中的最大值。
|
||||
* 对于本题而言,初始时,我们将数组 nums 的前 kk 个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums 中的位置出现在滑动窗口左边界的左侧。因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其永久地从优先队列中移除。
|
||||
* 我们不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组(num,index),表示元素 num 在数组中的下标为 index。
|
||||
* <p>
|
||||
* TODO 本质上就是维护一个可以比较大小的优先队列,队列中存放(值,索引)
|
||||
* 当遍历到索引等于这个队列中第一个数的索引时,就把第一个数弹出去,每次遍历都peek第一个数,则就是最大的数
|
||||
* 遍历数组复杂度O(n),将元素放入优先队列O(logn),因此时间复杂度O(nlogn)
|
||||
* @param nums
|
||||
* @param k
|
||||
* @return
|
||||
*/
|
||||
public int[] maxSlidingWindow1(int[] nums, int k) {
|
||||
int n = nums.length;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 官方使用单调队列的做法:时间复杂度O(n)
|
||||
* 由于我们需要求出的是滑动窗口的最大值,如果当前的滑动窗口中有两个下标 i 和 j,其中 i 在 j 的左侧(i < j),并且 ii 对应的元素不大于 jj 对应的元素(nums[i]≤nums[j]),那么会发生什么呢?
|
||||
* 当滑动窗口向右移动时,只要 i 还在窗口中,那么 j 一定也还在窗口中,这是 i 在 j 的左侧所保证的。因此,由于 nums[j] 的存在,nums[i] 一定不会是滑动窗口中的最大值了,我们可以将 nums[i] 永久地移除。
|
||||
* 因此我们可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组 nums 中对应的值是严格单调递减的。因为如果队列中有两个相邻的下标,它们对应的值相等或者递增,那么令前者为 ii,后者为 jj,就对应了上面所说的情况,即 nums[i] 会被移除,这就产生了矛盾。
|
||||
* 当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果前者大于等于后者,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。
|
||||
* 由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。但与方法一中相同的是,此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。
|
||||
* 为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」。
|
||||
* <p>
|
||||
* TODO 实际理念就是:维护一个两端都可以操作的队列(这个队列是单调的)
|
||||
* 1)在一个窗口中,如果一个数组的左边元素比右边元素小,那么他就永远不会被用到,那么将它从队列的尾部移除
|
||||
* 2)如果在右边,但是比他小,那么他可能被用到,则加在队列的尾部
|
||||
* 3) 那么根据1)和2)可以得到一个单调递减的队列了,队列最前面一定是最大的元素
|
||||
* 4)通过每次i++之后都会result数组都会赋值一次,保证进来的数一定是窗口里的数,永远维护着窗口
|
||||
*
|
||||
* @param nums
|
||||
* @param k
|
||||
* @return
|
||||
*/
|
||||
public int[] maxSlidingWindow2(int[] nums, int k) {
|
||||
int n = nums.length;
|
||||
Deque<Integer> deque = new LinkedList<Integer>();
|
||||
//先构建第一个窗口
|
||||
for (int i = 0; i < k; ++i) {
|
||||
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
|
||||
deque.pollLast();
|
||||
}
|
||||
deque.offerLast(i);
|
||||
}
|
||||
|
||||
int[] ans = new int[n - k + 1];
|
||||
ans[0] = nums[deque.peekFirst()];
|
||||
//由于i每+1,ans都会赋值一个元素,因此进来的数永远在窗口里,不存在把队列里的数全挪完了,窗口数也超过的情况
|
||||
for (int i = k; i < n; ++i) {
|
||||
//移除比他小的元素
|
||||
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
|
||||
deque.pollLast();
|
||||
}
|
||||
deque.offerLast(i);
|
||||
//移除过期元素
|
||||
while (deque.peekFirst() <= i - k) {
|
||||
deque.pollFirst();
|
||||
}
|
||||
ans[i - k + 1] = nums[deque.peekFirst()];
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 自己尝试实现一次官方单调队列(双端队列)的做法
|
||||
* 速度超过66%,内存超过90%
|
||||
*/
|
||||
public int[] maxSlidingWindow3(int[] nums, int k) {
|
||||
|
||||
int n = nums.length;
|
||||
Deque<Integer> deque = new LinkedList<>();
|
||||
|
||||
//创建第一个窗口
|
||||
for (int i = 0; i < k; i++) {
|
||||
while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
|
||||
deque.removeLast();
|
||||
}
|
||||
deque.offerLast(i);
|
||||
}
|
||||
|
||||
//创建结果
|
||||
int[] result = new int[n - k + 1];
|
||||
//将第一个窗口的结果赋值给result
|
||||
result[0] = nums[deque.peekFirst()];
|
||||
for (int i = k; i < n; i++) {
|
||||
//如果新来的数比这个数大,就把他移除
|
||||
while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
|
||||
deque.removeLast();
|
||||
}
|
||||
deque.offerLast(i);
|
||||
//移除过期元素
|
||||
while (deque.peekFirst() <= i - k) {
|
||||
deque.removeFirst();
|
||||
}
|
||||
//将最前面的数赋值给结果
|
||||
result[i - k + 1] = nums[deque.peekFirst()];
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 官方分块+预处理法:时间复杂度O(n)
|
||||
*
|
||||
* TODO 实际上就是将数据分头处理;
|
||||
* 1)窗口两端的一定是可以被窗口长度整除的,这些数单独跟窗口两端的进行比较
|
||||
* 2)不在窗口两端的则需要在窗口前缀和窗口后缀中找到最大值,在取前缀和后缀中的最大值,就可以得到那个位置上的最大值
|
||||
* 3)因此根据2)可以提前从前往后遍历和从后往前遍历,分别获取到最大值
|
||||
* @param nums
|
||||
* @param k
|
||||
* @return
|
||||
*/
|
||||
public int[] maxSlidingWindow4(int[] nums, int k) {
|
||||
int n = nums.length;
|
||||
int[] prefixMax = new int[n];
|
||||
int[] suffixMax = new int[n];
|
||||
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]);
|
||||
}
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue