今日任务
- 239 滑动窗口最大值 (题目:. - 力扣(LeetCode) )
- 347 前 K 个高频元素 (题目: . - 力扣(LeetCode) )
- 栈与队列总结
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操作要保持如下规则:
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
- 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)//所有元素入堆,堆的长度为kfor 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]intfunc (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 := *hn := 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]
}
栈与队列总结
嘿嘿,偷个懒,参考卡哥的: 代码随想录 ( 这也不是打广告啥的吧,就单纯觉得人家的东西好, 省时省力吧)