栈和队列(队列的应用)[三]

文章介绍了如何使用数据结构解决LeetCode中的两道难题:滑动窗口最大值和求前K个高频元素。对于滑动窗口最大值,文章提出了使用单调队列(基于C++的deque实现)来实时维护最大值。对于求前K个高频元素,文章推荐使用优先级队列(小顶堆),通过统计元素频率并维护一个大小为K的堆来找到前K个高频元素。这两种方法的时间复杂度分别为O(n)和O(nlogk),空间复杂度分别为O(k)和O(n)。
摘要由CSDN通过智能技术生成


一、滑动窗口最大值

思想:这道题属于困难题,不容易想到解决办法。对于“最大值”,我们可以想到一种非常合适的数据结构,那就是优先队列(堆),其中的大根堆可以帮助我们实时维护一系列元素中的最大值。我们将数组nums的前k个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,但堆顶的元素就是堆中所有元素的最大值,这个最大值可能并不在滑动窗口出口,返回不了我们想要的“最大值”。
顺着思路继续优化,考虑使用单调队列,可以实现题目要求!保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。

滑动窗口最大值(leetcode 239. )

这个队列的C++代码

class MyQueue{
public:
	void pop(int value){
	}
	void push(int value){
	}
	int front(){
		return que.front();
	}
};

放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。
C++中没有直接支持单调队列,需要我们自己来实现一个单调队列
伪代码

#C++中deque是stack和queue默认的底层实现容器
deque<int> que;#queue在没有指定容器的情况下,deque就是默认底层容器。
#每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
#同时pop前判断队列当前是否为空
pop(int val){
	if(!que.empty() && val==que.front()){
		que.pop_front(); }
}
#如果push数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
#这样就保持了队列里的数值是单调从大到小的了。
push(int val){
	if (!que.empty())&& val>que.back()){#push进来的元素大于队列
		que.pop_back();}
	}
	que.push_back(value);
}
#查询当前队列里的最大值 直接返回队列前端也就是front就可以了
int front(){
	return que.front();
}

这样就用deque实现了一个单调队列!具体滑动窗口实现看下面的代码:

from collections import deque

class MyQueue: #单调队列(从大到小)
    def __init__(self):
        self.queue = deque() #这里需要使用deque实现单调队列,直接使用list会超时
    #每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
    #同时pop之前判断队列当前是否为空。
    def pop(self, value):
        if self.queue and value == self.queue[0]:
            self.queue.popleft()#list.pop()时间复杂度为O(n),这里需要使用collections.deque()  
    #如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
    #这样就保持了队列里的数值是单调从大到小的了。
    def push(self, value):
        while self.queue and value > self.queue[-1]:
            self.queue.pop()
        self.queue.append(value)
        
    #查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
    def front(self):
        return self.queue[0]
    
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        que = MyQueue()
        result = []
        for i in range(k): #先将前k的元素放进队列
            que.push(nums[i])
        result.append(que.front()) #result 记录前k的元素的最大值
        for i in range(k, len(nums)):
            que.pop(nums[i - k]) #滑动窗口移除最前面元素
            que.push(nums[i]) #滑动窗口前加入最后面的元素
            result.append(que.front()) #记录对应的最大值
        return result
时间复杂度: O(n)
空间复杂度: O(k)

题目中使用单调队列的时间复杂度是 O(n)。空间复杂度因为我们定义一个辅助队列,所以是O(k)。注意: 单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 另C++中deque是栈stack和队列queue默认的底层实现容器,deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。

二、求前 K 个高频元素

前 K 个高频元素(leetcode 347.)

思路:
本题主要涉及问题有三个:
1.统计元素出现频率(可以用map来实现)
2.对频率排序(使用容器适配器就是优先级队列)
3.找出前k个高频元素
注意: 这里解释一下优先级队列,它其实是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。而且优先级队列内部元素是自动依照元素的权值排列。默认情况下(也称缺省情况下),优先级队列priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。
为了帮助大家理解,再解释一下堆,堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。如果父亲结点是大于等于左右孩子就是大顶堆(堆头是最大元素),小于等于左右孩子就是小顶堆(堆头是最小元素)。最小堆适用于找到最小值或前 k 个最小值的场景,而最大堆适用于找到最大值或前 k 个最大值的场景。
实现堆的常用方法是使用数组,并根据插入和删除操作来维护堆的性质。
使用数组实现最小堆的基本步骤:
1.创建一个空数组,用于存储堆的元素。
2.实现插入操作(Insertion):
将新元素添加到数组的末尾。
通过比较该元素与其父节点的值,并交换它们的位置,以确保堆的性质。重复此步骤直到满足堆的性质为止。
3实现删除操作(Deletion):
移除堆的根节点(最小值)。
将数组的最后一个元素放置在根节点的位置。
通过与其子节点比较,并交换位置,将该元素下沉到合适的位置,以满足堆的性质。重复此步骤直到满足堆的性质为止。
大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

import heapq
class Solution:
	def topKFrequent(self,nums,k):
		#用字典统计nums中每个元素出现次数
		map_={}#nums[i]对应的元素次数
		for i in range(len(nums)):
	#使用字典的get()方法获取字典map_中键为nums[i]的的出现次数,如果键不存在,则返回默认值0。
			map_[nums[i]]=map_.get(nums[i],0)+1#将获取到的出现次数加1,并将更新后的值存储回字典中。
		#对频率排序
		#定义一个小顶堆,大小为k
		pri_que=[]
		#用固定大小为k的小顶堆,扫描所有频率的数值
		for key,freq in map_.items():
			heapq.heappush(pri_que,(freq,key))#将(freq,key)插入堆pri_que中并保持堆的性质。这意味着插入后,堆中的最小元素仍然位于堆顶。
			if len(pri_que)>k:#如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
				heapq.heappop(pri_que)#弹出并返回堆pri_que中的最小元素。该操作会同时调整堆,以保持堆的性质。
		#找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        result = [0] * k#创建一个长度为k的列表result,并将其所有元素初始化为0
        for i in range(k-1, -1, -1):#使用一个循环从k-1到0,以递减的顺序迭代。
            result[i] = heapq.heappop(pri_que)[1]#在循环中,使用 heapq.heappop(pri_que)从优先队列pri_que中弹出并返回最小(或最大)的元素。
        return result
#时间复杂度:O(nlogk)
#空间复杂度:O(n)	

这时候有同学会问为什么不用快排呢,因为使用快排的话,要将字典map转换为向量vector的结构,然后对整个数组进行排序,而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的解法。
那么为什么题目要求是求前 K 个高频元素,我么没有使用大顶堆而是使用小顶堆? 因为,使用大顶堆就要把所有元素都进行排序,而且每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
(ps:近期都是关于数据结构基础知识分享,感兴趣的同学可以关注本人公众号:HEllO算法笔记)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值