【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
}
相关推荐
计算机安禾2 小时前
【数据结构与算法】第13篇:栈(三):中缀表达式转后缀表达式及计算
c语言·开发语言·数据结构·c++·算法·链表
another heaven2 小时前
【软考 IDEF系列方法:从概念到核心差异】
数据结构
章鱼丸-2 小时前
DAY40 训练与测试规范写法
人工智能·算法·机器学习
不会写DN2 小时前
Go 中最主流 JWT 库 jwt -go
开发语言·后端·golang
代码飞天2 小时前
算法与数据结构之又臭又长的表
数据结构·算法
A923A2 小时前
【洛谷刷题 | 第七天】
算法·模拟·洛谷
故事和你912 小时前
洛谷-入门4-数组3
开发语言·数据结构·c++·算法·动态规划·图论
玉树临风ives2 小时前
atcoder ABC 451 题解
c++·算法·atcoder
_日拱一卒2 小时前
LeetCode:和为K的子数组
算法·leetcode·职场和发展