day11 | 栈与队列 part-3 (Go) | 239 滑动窗口最大值、347 前 K 个高频元素 (好难)

今日任务 


239 滑动窗口最大值 

      题目:. - 力扣(LeetCode) 

      给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值 

想法:

        看到滑动窗口,窗口是一进一出的,能想到是需要一个队列,队列的长度应该是k.  返回的最终结果数组的长度应该是 len(nums)-(k-1).但是如何求每个窗口(队列)中的最大值是多少??  当时想的对每个窗口排序,也未实现. 

问题:

        认真的说, 我看了好多视频和文字题解都没理解他们说的: “队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。”   例:对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。

        其实上面真正困惑我的是一直把“滑动窗口”这个题目和单调队列的实现效果,混在一起思考了,因为我不明白的是我就算是把(窗口)队列的元素 从“{2, 3, 5, 1 ,4}”变成了“{5, 4} ”, 窗口的长度都变了,我窗口咋移除最前面的元素.........直到我仔细看了两遍代码 

        (窗口的大小并没有变,仍是 k , 只是窗口所对应的队列,是我们经过处理的,例如:我窗口是{2,3,5}和向后移动两步的窗口{5,1,4} 其中最大值都是 5. 那么对于这2 步移动过程中 单调队列的“出口处”一直都是{5}. 那移动过程中,判断窗口左侧的值 nums[L]和队列的出口处的值是否相等,不相等就不用有移除的操作了,因为在单调队列中 ,加入元素 5 的时候,前面 2 和 3 就不可能再成为窗口最大的元素了,已经移除过了......不知道这样解释能看明白嘛)   这段话是我在看了代码之后才产生的理解.

        其实理解过一次之后,这个单调队列就特别清晰了

解决思路:

        首先呢, 我们要明确我们这道题的关键是,如何求得每个窗口(队列)中的最大值是多少?  

        此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。实现一个单调递减队列: 调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。

设计单调队列的时候,pop,和push操作要保持如下规则:

  1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
  2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。队列的具体实现见下面代码:

题解和图片参考自: 代码随想录

// 定义一个结果切片, 还有就是 结果切片的长度 = len(nums) -(k-1)
// 用一个切片模拟队列, 队列里的长度保持为 k,  现在好奇的是如何判断队列里面的元素最大呢?
// (需要一个单调递减队列)
// ------------------------------------
// 自己理清思路后,写出的代码
// 定义一个队列,单调递减的队列.,下面是一些实现单调队列的方法
type MyQueue struct {
    queue []int
}

func NewMyQueue() *MyQueue {
    return &MyQueue{
        queue: make([]int,0),
    }
}

// 获取队列的出口值(获取窗口最大值),经过特殊处理的队列,出口值肯定是当前窗口中最大的
func (m *MyQueue) Front() int {
    return m.queue[0]
}

// 获取队列的入口值,即数组的最后
func (m *MyQueue) Back() int {
    return m.queue[len(m.queue)-1]
}

// 队列是否为空
func (m *MyQueue) Empty() bool {
    return len(m.queue) == 0
}


// 加入元素 push, 如果要加入的元素比队列入口的值大,则将队列入口的值弹出,直到碰到比自己大的,或者队列为空时,将元素加入队列.
func (m *MyQueue) Push(val int) {
    for !m.Empty() && m.Back() < val {
        m.queue = m.queue[:len(m.queue)-1]
    }
    m.queue = append(m.queue,val)
}

// 弹出元素 pop , 如果队列的出口处 和 val 相等,则将该元素从队列中弹出(val 是窗口向后移动过程中,要移除的元素)
func (m *MyQueue) Pop(val int) {
    if !m.Empty() && m.Front() == val{
        m.queue = m.queue[1:]
    }
}

// -----------------------------------
func maxSlidingWindow(nums []int,k int) []int {
    queue := NewMyQueue()
    length := len(nums)
    res := make([]int,0)

    // 先将 k 个元素放入队列
    for i:=0;i<k;i++{
        queue.Push(nums[i])
    }
    // 先将第一个窗口的最大值加入
    res = append(res,queue.Front())

    // 移动窗口
    for i := k; i<length;i++{
        // 将窗口新加入的元素 添加到 队列中
        queue.Push(nums[i])
        // 将窗口后移,前面溢出的元素从队列中移除(如果元素还在队列中的话)
        queue.Pop(nums[i-k])
        // 将当前窗口中最大的元素加入到 res 中.
        res = append(res,queue.Front())
    }

    return res
}

 347 前 K 个高频元素

      题目: . - 力扣(LeetCode) 

      给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

      进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

      示例-输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]

想法:

        第一反应: 循环统计每个频率高的元素,然后按照其出现的次数多少来排序,然后输出切片的前 k 位, 暂时没想到和 栈、队列 能产生什么关系啊

问题:

        这题目猛一看思路很简单: (1)要统计元素出现频率 (2) 对频率排序 (3) 找出前K个高频元素

然而 对频率如何排序 这个问题是有点棘手, 常规想到的就是 排序的一些包直接排一下, 然而看到的题解大部分都在说使用小顶堆,然而我还不知道什么是小顶堆.....😭

科普:

        堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

        我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

        寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描)

        题解和图片参考自: 代码随想录        

解决思路:

        (1) 首先使用 hash map 记录每个元素出现的次数

        (2) 定义一个小顶堆,用来排序map的 value. 其实要借助 go 中的 container/heap 的包. 我们要自己实现一些堆的接口(具体效果是小顶堆还是大顶堆,和自己实现的接口方法有关.) 我也尚未仔细研究这个引用的包,也不能很好的讲清楚这里.待我研究明白后,看能不能补齐一个完整的小顶堆的实现.... 

        (3) 从小顶堆中取出高频元素并返回, 对于小顶堆来说 pop 出来的都是从小到大的元素,这里注意 我们可以往结果数组中 倒着塞入数据即可.

注: 这题从难度上来说好像没有滑动窗口难, 但是这题让我理解起来确实很困难, 因为我不理解那些题解上来就讲 “优先队列、小顶堆、大顶堆、然后最后还得是借助的封装好的包来将元素入堆”, 搞得很懵, 希望二刷时,我能带着更丰富的知识来解这种题.

// 方法一: 使用小顶堆排序
func topKFrequent(nums []int, k int) []int {
	map_num := map[int]int{}
	//记录每个元素出现的次数
	for _, item := range nums {
		map_num[item]++
	}
	h := &IHeap{}
	heap.Init(h)
	//所有元素入堆,堆的长度为k
	for key, value := range map_num {
		heap.Push(h, [2]int{key, value})
		if h.Len() > k {
			heap.Pop(h)
		}
	}
	res := make([]int, k)
	//按顺序返回堆中的元素, 其实小顶堆取出来的元素都是从小到大的,那么在往 res 数组中塞数据时,应该倒着往数组里添加.
	for i := 0; i < k; i++ {
		res[k-i-1] = heap.Pop(h).([2]int)[0]
	}
	return res
}

// 构建小顶堆
type IHeap [][2]int

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

func (h IHeap) Less(i, j int) bool {
	return h[i][1] < h[j][1]
}

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

func (h *IHeap) Push(x interface{}) {
	*h = append(*h, x.([2]int))
}
func (h *IHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}




//方法二: 利用O(nlogn)排序
func topKFrequent(nums []int, k int) []int {
    ans:=[]int{}
    map_num:=map[int]int{}
    for _,item:=range nums {
        map_num[item]++
    }
    for key,_:=range map_num{
        ans=append(ans,key)
    }
    //核心思想:排序
    //可以不用包函数,自己实现快排
    sort.Slice(ans,func (a,b int)bool{
        return map_num[ans[a]]>map_num[ans[b]]
    })
    return ans[:k]
}

栈与队列总结

        嘿嘿,偷个懒,参考卡哥的: 代码随想录 ( 这也不是打广告啥的吧,就单纯觉得人家的东西好, 省时省力吧)

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值