From 26376573fb9adf9e67f9c85288092eb036f638e4 Mon Sep 17 00:00:00 2001 From: dingjiawen <745518019@qq.com> Date: Thu, 15 Sep 2022 15:15:46 +0800 Subject: [PATCH] =?UTF-8?q?leecode=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stackAndDeque/MaxSlidingWindow.java | 331 ++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 Leecode/src/main/java/com/markilue/leecode/stackAndDeque/MaxSlidingWindow.java diff --git a/Leecode/src/main/java/com/markilue/leecode/stackAndDeque/MaxSlidingWindow.java b/Leecode/src/main/java/com/markilue/leecode/stackAndDeque/MaxSlidingWindow.java new file mode 100644 index 0000000..0b2144e --- /dev/null +++ b/Leecode/src/main/java/com/markilue/leecode/stackAndDeque/MaxSlidingWindow.java @@ -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 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。 + *

+ * 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 pq = new PriorityQueue(new Comparator() { + 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] 会被移除,这就产生了矛盾。 + * 当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果前者大于等于后者,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。 + * 由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。但与方法一中相同的是,此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。 + * 为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」。 + *

+ * 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 deque = new LinkedList(); + //先构建第一个窗口 + 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 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; + } + + +}