【LeetCode】239.滑动窗口最大值

题目链接:239. 滑动窗口最大值

题目描述:

数据范围:

go 复制代码
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
1 <= k <= nums.length

思路1:采用双指针去维护这个窗口边界,同时每次滑动都遍历计算窗口内的最大值,这种方式时间复杂度O(n*k),显然时间复杂度比较高,数据范围也比较大,不太能接受。

思路2:仍然采用双指针来维护窗口边界,主要优化是这个O(K)复杂度的遍历,这里可以用一个K大小的大根堆来维护窗口内的最大值,这里由于不仅看值,还要看下标,因此堆中的每个节点可以存储是一个二元组<元素,下标>,但仔细一想其实不用,我们只需要存储下标就行了,堆比较的时候还是按照值来调整下标在堆中的顺序;每次滑动窗口将当前的元素下标加入堆中,并且检查堆顶元素的下标如果不在窗口内了(堆顶下标+k < =i(当前窗口右边界)),那么就需要不断移除堆顶元素,直到满足当前堆中的最大元素下标在窗口内,每次记录答案都直接从堆中取当前的最大值,也就是堆顶的值,这里调整堆复杂度是O(logK),因此总的复杂度为O(nlogK)

golang里面实现堆排主要是需要实现以下几个方法,注意push和pop实现是指针方法。

go 复制代码
Len()
Less(i,j int) bool
Swap(i,j int)
Push(v interface{}) 指针
Pop() interface{} 指针

实现1:用sort.IntSlice可以少实现两个方法,它本身有序

go 复制代码
// 实现一个大根堆,堆顶元素最大,只存储下标即可,这里有sort,所以我们实现less、push和pop方法即可
var a []int
type hp struct{sort.IntSlice}
func (h hp) Less(i, j int) bool {
    return a[h.IntSlice[i]] > a[h.IntSlice[j]]
}
func (h *hp) Push(x interface{}) {
    h.IntSlice = append(h.IntSlice, x.(int))
}

func (h *hp) Pop() interface{} {
    old := h.IntSlice
    n := len(old)
    res := old[n-1]
    h.IntSlice = old[:n-1]
    return res
}

func maxSlidingWindow(nums []int, k int) []int {
    if k == 1 {
        return nums
    }
    a = nums
    h := &hp{make([]int,k)}
    // 入堆
    for i := 0; i < k; i++ {
       h.IntSlice[i] = i
    }
    heap.Init(h)

    ans := make([]int, 0, len(nums)-1)
    ans = append(ans, nums[h.IntSlice[0]])
    for i := k; i < len(nums) ; i++{
        heap.Push(h, i)
        for h.IntSlice[0] <= i-k {
            heap.Pop(h)
        }
        ans = append(ans, nums[h.IntSlice[0]])
    }
    return ans
}

实现2:用数组实现堆

go 复制代码
type hp []int // 记录下标
var a []int
func (h hp) Less(i, j int) bool {
    return a[h[i]] > a[h[j]]
}
func (h *hp) Push(x interface{}) {
    *h = append(*h, x.(int))
}

func (h hp) Len() int {
    return len(h)
}

func (h hp) Swap(i, j int) {
   h[i], h[j] = h[j], h[i]
}

func (h *hp) Pop() interface{} {
    old := *h
    n := len(old)
    res := old[n-1]
    *h = old[:n-1]
    return res
}

func maxSlidingWindow(nums []int, k int) []int {
    if k == 1 {
        return nums
    }
    a = nums
    h := &hp{}
    // 入堆
    for i := 0; i < k; i++ {
      heap.Push(h,i)
    }
    heap.Init(h)

    ans := make([]int, 0, len(nums)-1)
    ans = append(ans, nums[(*h)[0]])
    for i := k; i < len(nums) ; i++{
        heap.Push(h, i)
        for (*h)[0] <= i-k { // 当前最大元素的下标不在区间内,则弹出
            heap.Pop(h)
        }
        ans = append(ans, nums[(*h)[0]])
    }
    return ans
}

思路3:我们用堆主要是为了维护这个窗口内最大值,那么我们换一种思路,用一个单调队列维护窗口的最大值,顺序我们按照从左往右,单调递减。对前k个元素过一遍单调队列的时候,队头(左边)始终保持最大值,因此,新进一个元素的时候,我们只需要和队尾(右边)的元素比较,如果大于队尾的元素,就移除掉队尾的元素,因此已经没用了。当窗口移动的时候,为了保证队列中的元素都在窗口内,我们需要检查一下队首元素的下标+k的值是否小于等于i,如果小于,就该去掉队头的元素了。因此队列中元素满足两个特性:

  • 从左往右值是单调递减的。
  • 从做往右,下标是单调递增的。

我们push的时候只需要按照这两个特定来维护队列即可,窗口滑动的时候,我们还需要检查队首元素的下标是否在当前窗口内,不在就删除队首元素,直到在窗口内。每个元素在遍历的时候有且仅会进出一次队列,因此实际上时间复杂度为O(n)。

go 复制代码
func maxSlidingWindow(nums []int, k int) []int {
    queue := []int{} // 单调队列维护下标
    push := func(i int) {
        for len(queue) > 0 && nums[i] >= nums[queue[len(queue)-1]] {
            queue = queue[:len(queue)-1]
        }
        queue = append(queue,i)
    }
    for i := 0; i < k; i++ {
        push(i)
    }
    n := len(nums)
    ans := make([]int, 0, n-k+1)
    ans = append(ans, nums[queue[0]])
    for i := k; i < len(nums); i++ {
        push(i)
        for queue[0] <= i-k { // 删除队头元素
            queue = queue[1:]
        }
        ans  = append(ans, nums[queue[0]])
    }
    return ans
}
相关推荐
不会就选b6 小时前
算法日常・每日刷题--<贪心>25
数据结构·算法
Cccp.1239 小时前
【leetcode】(七)前缀树和贪心算法
数据结构·leetcode·贪心算法
千里码aicood9 小时前
基于CART算法的图书分类系统设计与实现
数据库·算法·分类
做cv的小昊10 小时前
【World Model】π0.5:a Vision-Language-Action Model with Open-World Generalization
人工智能·算法·机器学习·大模型·多模态·vla·世界模型
لا معنى له10 小时前
World-In-World: World Models in a Closed-Loop World
算法
念何架构之路12 小时前
go-grpc使用
rpc·golang
Logic10112 小时前
C语言/数据结构位运算题解:异或XOR找出多任务下载器中的“独特下载速度“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
Discipline102912 小时前
尼泊尔河流流量:天气驱动的预测与高流量风险
人工智能·算法
XuCoder13 小时前
改一个数,右边全得重算,这题怎么扛住两万次查询
算法