【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
}
相关推荐
卷福同学11 小时前
不用服务器,不用配环境,我10分钟上线了一个AI Agent
人工智能·后端·算法
问君能有几多愁~13 小时前
C++ 数据结构复习笔记
数据结构·c++·笔记
至乐活着13 小时前
深入解析跳表SkipList:原理、实现与性能优化实战
数据结构·算法·跳表·skiplist·java实现
Jerry14 小时前
LeetCode 383. 赎金信
算法
ai产品老杨14 小时前
H264 H265视频分析常见问题和排查清单
人工智能·算法·音视频
xurime15 小时前
Excelize 开源十周年,发布 2.11.0 版本
golang·开源·github·excel·导出·导入·excelize·基础库
Jerry15 小时前
LeetCode 454. 四数相加 II
算法
可编程芯片开发15 小时前
基于CPS-SPWM链式STATCOM系统在电压不平衡环境下控制策略的simulink建模与仿真
算法
Jerry16 小时前
LeetCode 202. 快乐数
算法
hans汉斯16 小时前
基于改进交叉熵损失函数与Transformer的心电信号高风险分类研究
功能测试·深度学习·算法·yolo·目标检测·分类·transformer