描述
给定一个长度为 n 的数组 num 和滑动窗口的大小 size ,找出所有滑动窗口里数值的最大值。
例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
窗口大于数组长度或窗口长度为0的时候,返回空。
数据范围: 1≤n≤100001≤n≤10000,0≤size≤100000≤size≤10000,数组中每个元素的值满足 ∣val∣≤10000∣val∣≤10000
要求:空间复杂度 O(n)O(n),时间复杂度 O(n)O(n)
题目解析
我们需要找到数组 num
中所有滑动窗口的最大值。那么我们不妨先尝试用暴力解法,直接遍历数组,通过在每个窗口位置遍历窗口内的所有元素来找到最大值。
object Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* @param num int整型一维数组
* @param size int整型
* @return int整型一维数组
*/
fun maxInWindows(num: IntArray, size: Int): IntArray {
// 如果窗口大小大于数组长度,返回空数组
if (num.size < size || size == 0) return intArrayOf()
// 初始化结果数组
val result = IntArray(num.size - size + 1)
// 遍历数组,计算每个窗口的最大值
for (i in 0..num.size - size) {
var max = num[i]
for (j in i until i + size) {
if (num[j] > max) {
max = num[j]
}
}
result[i] = max
}
return result
}
}
在初始化结果数组时,使用条件'if (num.size < size || size == 0)
'来避免size
为0的情况。
外层循环的条件使用'0..num.size - size
',以确保不会访问越界索引。
方法简单直观,但对于每个窗口起始位置,都需要遍历size
个元素。
时间复杂度:O(n * size) 空间复杂度:O(n),存储结果数组。
如果想在时间效率上优化,则可以考虑单调队列。
单调队列
滑动窗口问题,我们可以通过维护一个单调队列来解决。单调队列是一种特殊的数据结构,它能够保证队列内的元素按照某种顺序排列(比如递增或递减),并且可以高效地支持添加元素和删除元素的操作。
首先我们创建一个双端队列deque来维护窗口内的最大值的索引并创建一个结果列表result来存储每个滑动窗口的最大值随后遍历数组nums:
对于每个索引i,维护deque,确保队列的末尾始终是当前窗口内的最大值的索引。
如果deque中存在比当前元素小的元素,移除它们,因为它们不可能是未来窗口的最大值。
将当前索引i添加到deque。
如果deque的长度大于窗口大小,移除队列头部的索引,因为它已经不在当前窗口内。
当遍历的索引i大于窗口大小减一时,将deque的第一个元素对应的值添加到结果列表中,因为这是当前窗口的最大值。
最后将结果列表转换为IntArray并返回。
object Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* @param num int整型一维数组
* @param size int整型
* @return int整型一维数组
*/
fun maxInWindows(num: IntArray, size: Int): IntArray {
// 如果窗口大于数组长度或窗口大小为0,返回空数组
if (num.size < size || size == 0) return intArrayOf()
val deque = ArrayDeque<Int>() // 用于存储索引的双端队列
val maxVals = mutableListOf<Int>() // 存储每个窗口的最大值
for (i in num.indices) {
// 移除队列尾部所有不大于当前元素的索引
while (deque.isNotEmpty() && num[deque.last()] <= num[i]) {
deque.removeLast()
}
deque.addLast(i) // 当前索引入队
// 如果队列头部索引不在窗口内,则移除
if (deque.first() < i - size + 1) {
deque.removeFirst()
}
// 当窗口已经滑动了size次,记录最大值
if (i >= size - 1) {
maxVals.add(num[deque.first()])
}
}
return maxVals.toIntArray()
}
}
时间复杂度:由于每个元素最多只会被插入和删除 deque
一次,因此该算法的时间复杂度是 O(n),其中 n 是数组的长度。
如果要进一步降低时间复杂度和空间复杂度,可以使用双指针方法
双指针方法
双指针的思路是维护一个当前滑动窗口的最大值和它的位置,并在窗口滑动时进行更新。在保证正确性的同时,利用双指针和局部最大值的特点,实现了高效的滑动窗口最大值的计算。
object Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* @param num int整型一维数组
* @param size int整型
* @return int整型一维数组
*/
fun maxInWindows(num: IntArray, size: Int): IntArray {
if (num.size < size || size == 0) return intArrayOf() // 如果窗口大于数组长度或窗口大小为0,返回空数组
val result = mutableListOf<Int>()
var start = 0
var end = 0
var maxIdx = -1 // 记录当前窗口最大值的索引
while (end < num.size) {
// 如果最大值的索引小于start,重新计算当前窗口最大值
if (maxIdx < start) {
maxIdx = start
for (i in start..end) {
if (num[i] > num[maxIdx]) {
maxIdx = i
}
}
} else if (num[end] > num[maxIdx]) {
maxIdx = end
}
// 当窗口大小达到 size 时,记录最大值
if (end - start + 1 == size) {
result.add(num[maxIdx])
start++ // 窗口滑动
}
end++
}
return result.toIntArray()
}
}
-
时间复杂度 O(n)
- 双指针和双端队列的组合使得每个元素最多进入和离开队列一次,因此总的时间复杂度为 O(n)。
- 相比于暴力方法每次计算每个窗口最大值的 O(n * size) 时间复杂度,显著降低了计算时间。
-
空间复杂度 O(size)
- 双端队列最多存储
size
个元素的索引,因此空间复杂度为 O(size)。 - 使用双端队列存储索引而非值,进一步减少了空间消耗。
- 双端队列最多存储
总结
解决滑动窗口问题通常需要用到双指针技术,通过维护一个固定大小的窗口来遍历数组。在每次窗口滑动过程中,计算并记录下窗口内所需的统计量,如最大值、最小值或总和。为了提高效率,可以使用额外的数据结构,例如单调队列或平衡树,来快速找到窗口内的最大或最小元素。通过这种方式,可以在线性时间内完成窗口的更新和统计量的计算。