下面我们来看一道滑动窗口的题目
给你一个整数数组
nums,有一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的k个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7示例 2:
输入:nums = [1], k = 1 输出:[1]提示:
1 <= nums.length <= 105-104 <= nums[i] <= 1041 <= k <= nums.length
分析一下这道题目:
首先我们来想一个简单的思路,就是使用一个切片去维护一个窗口,每次窗口移动了,就重新遍历一遍这个窗口去求最大值,那么对于一个大小为n的数组维护一个长度为k的窗口,他的时间复杂度就是O(n × k),对于这一道题目时间复杂度还是很高的,我尝试了用这种方法,果然是超时了!
思路优化:
1.取出比新元素还小的元素:
首先我们可以想到,对于一个新添加进入的元素来说,如果他比原来的元素还大,那么原来的元素不可能成为最大的值,所以可以让新添加的元素去和已经存在的元素比较,已存在较小的元素出队!
2.新的队列会形成一个有序队列:
按照刚才说的,那么如果前面有小的元素,那么他一定被取出了,如果前面有大的元素一定不会取出,那么这就形成了一个从大到小的有序队列。
3.使用双端队列:
对于队尾的元素,需要比新元素小的需要出队,那么对于队头来说,超过当前滑动窗口的范围也需要出队!并且新元素需要在队尾入队,前后都可以出队和入队!这就相当于是一个双端队列!
4.使用切片模拟双端队列:
go语言原生是没有双端队列的,所以需要我们使用切片模拟,我们可以使用arr[:n]和arr[1:]来进行入队和出队的模拟,那么切片本身就相当于一个队列窗口了!
答案以及解析
package main
func maxSlidingWindow(nums []int, k int) []int {
// 先判断是否为空的情况
if len(nums) == 0 || k == 0 {
return []int{}
}
// 用来存答案
ans := make([]int, 0)
// 用来存元素的下标,模拟双端队列
win := make([]int, 0)
for i, x := range nums {
// 1. 移除滑出当前窗口的队头下标
if len(win) > 0 && win[0] < i-k+1 {
win = win[1:]
}
// 2. 维护单调性:弹出所有小于当前值 x 的队尾下标
for len(win) > 0 && nums[win[len(win)-1]] <= x {
win = win[:len(win)-1]
}
// 3. 当前下标入队
win = append(win, i)
// 4. 窗口形成后,队头永远是当前窗口最大值的下标
if i >= k-1 {
ans = append(ans, nums[win[0]])
}
}
return ans
}
总结:
这里需要注意几点:
1.因为这个队列只有进出操作,没有排序操作,所以队列最左边的一定是我们之前最早进入的元素,而队列右边一定是我们最后进入的元素,判断元素过期的时候我们就需要比较最左边元素是否过期。
2.我们还需要知道一些特殊的操作,除去队列队头元素(win=win[1:]),除去队列队尾的元素(win=win[:len(win)]),向队尾添加元素(win=append(win,i))
