leecode更新

This commit is contained in:
dingjiawen 2022-09-15 15:15:46 +08:00
parent 06d21ded9b
commit 26376573fb
1 changed files with 331 additions and 0 deletions

View File

@ -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每+1ans都会赋值一个元素,因此进来的数永远在窗口里,不存在把队列里的数全挪完了窗口数也超过的情况
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;
}
}