1. 优先队列简介
优先队列(Priority Queue):一种特殊的队列。在优先队列中,元素被赋予优先级,当访问队列元素时,具有最高优先级的元素最先删除
与普通队列最大的不同:出队顺序,在优先队列中,具有最高优先级的元素最先出队
普通队列的出队顺序跟入队顺序相关,符合**先进先出(First in, First out)**的规则。
而优先队列的出队顺序跟入队顺序无关,优先队列是按照元素的优先级来决定出队顺序的。优先级高的元素优先出队,优先级低的元素后出队。优先队列符合 最高级先出(First in, Largest out) 的规则
2. 适用场景
- 数据压缩:赫夫曼编码算法
- 最短路径算法:Dijkstra 算法
- 最小生成树算法:Prim 算法
- 任务调度器:根据优先级执行系统任务
- 事件驱动仿真:顾客排队算法
- 选择问题:查找第 k 个最小元素
3. 优先队列的实现
由上面的定义可知,优先队列与一般队列的不同在于入队和出队操作
而优先队列的实现方式也有很多种,除了使用**数组(顺序存储)实现与链表(链式存储)**实现之外,我们最常用的是使用 二叉堆结构实现优先队列。
- 数组(顺序存储)实现优先队列:入队操作直接插入到数组队尾,时间复杂度为 O(1)。出队操作需要遍历整个数组,找到优先级最高的元素,返回并删除该元素,时间复杂度为O(n)
- 链表(链式存储)实现优先队列:链表中的元素按照优先级排序,入队操作需要为待插入元素创建节点,并在链表中找到合适的插入位置,时间复杂度为O(n)。出队操作直接返回链表队头元素,并删除队头元素,时间复杂度为O(1)。
- 二叉堆结构实现优先队列:构建一个二叉堆结构,二叉堆按照优先级进行排序。入队操作就是将元素插入到二叉堆中合适位置,时间复杂度为O(logn)。吹对操作则返回二叉堆中优先级最大节点并删除,时间复杂度也是O(logn)
综上所述,优先队列使用二叉堆实现效率较高
4. 二叉堆实现的优先队列
-
定义:
- 二叉堆:符合以下两个条件之一的完全二叉树
1. 大顶堆:根节点值 ≥ 子节点值
2. 小顶堆:根节点值 ≤ 子节点值
- 二叉堆:符合以下两个条件之一的完全二叉树
-
二叉堆的基本操作:
二叉堆主要涉及到两个操作:堆调整方法和将数组构建为二叉堆方法- 堆调整方法 heapAdjust:把移走了最大值元素以后的剩余元素组成的序列再构造为一个新的堆积
具体步骤如下:
1. 从根节点开始,自上而下地调整节点的位置,使其成为堆积。即把序号为 i 的节点与其左子树节点(序号为 2 * i)、右子树节点(序号为 2 * i + 1)中值最大的节点交换位置
2. 因为交换了位置,使得当前节点的左右子树原有的堆积特性被破坏。于是,从当前节点的左右子树节点开始,自上而下继续进行类似的调整
3. 如此下去直到整棵完全二叉树成为一个大顶堆 - 将数组构建为二叉堆方法(初始堆建立方法) heapify
步骤如下:
1. 如果原始序列对应的完全二叉树(不一定是堆)的深度为 d,则从 d - 1 层最右侧分支节点(序号为 ⌊n/2⌋)开始,初始时令 i = ⌊n/2⌋,调用堆调整算法
2. 每调用一次堆调整算法,执行一次 i = i - 1,直到 i == 1 时,再调用一次,就把原始数组构建为了一个二叉堆
- 堆调整方法 heapAdjust:把移走了最大值元素以后的剩余元素组成的序列再构造为一个新的堆积
-
优先队列的基本操作:
- 入队heappush:
- 先将待插入元素 value 插入到数组 nums 末尾
- 如果完全二叉树的深度为 d,则从 d - 1 层开始最右侧分支节点(序号为 ⌊n/2⌋)开始,初始时令 i = ⌊n/2⌋,从下向上依次查找插入位置
- 遇到 value 小于当前根节点时,将其插入到当前位置。否则继续向上寻找插入位置
- 如果找到插入位置或者到达根位置,将 value 插入该位置
- 出队heappop:
- 交换数组 nums 首尾元素,此时 nums 尾部就是值最大(优先级最高)的元素,将其从 nums 中弹出,并保存起来
- 弹出后,对 nums 剩余元素调用堆调整算法,将其调整为大顶堆
- 入队heappush:
-
手写二叉堆实现优先队列
- heapAdjust:将完全二叉树调整为二叉堆
- heapify: 将数组构建为二叉堆方法(初始堆建立方法)
- heappush:向堆中添加元素,也是优先队列的入队操作
- heappop:删除堆顶元素,也是优先队列的出队操作,弹出优先队列中优先级最高的元素
- heapSort:堆排序
class Heapq: # 堆调整方法:调整为大顶堆 def heapAdjust(self, nums: [int], index: int, end: int): left = index * 2 + 1 right = left + 1 while left <= end: # 当前节点为非叶子结点 max_index = index if nums[left] > nums[max_index]: max_index = left if right <= end and nums[right] > nums[max_index]: max_index = right if index == max_index: # 如果不用交换,则说明已经交换结束 break nums[index], nums[max_index] = nums[max_index], nums[index] # 继续调整子树 index = max_index left = index * 2 + 1 right = left + 1 # 将数组构建为二叉堆 def heapify(self, nums: [int]): size = len(nums) # (size - 2) // 2 是最后一个非叶节点,叶节点不用调整 for i in range((size - 2) // 2, -1, -1): # 调用调整堆函数 self.heapAdjust(nums, i, size - 1) # 入队操作 def heappush(self, nums: list, value): nums.append(value) size = len(nums) i = size - 1 # 寻找插入位置 while (i - 1) // 2 >= 0: cur_root = (i - 1) // 2 # value 小于当前根节点,则插入到当前位置 if nums[cur_root] > value: break # 继续向上查找 nums[i] = nums[cur_root] i = cur_root # 找到插入位置或者到达根位置,将其插入 nums[i] = value # 出队操作 def heappop(self, nums: list) -> int: size = len(nums) nums[0], nums[-1] = nums[-1], nums[0] # 得到最大值(堆顶元素)然后调整堆 top = nums.pop() if size > 0: self.heapAdjust(nums, 0, size - 2) return top # 升序堆排序 def heapSort(self, nums: [int]): self.heapify(nums) size = len(nums) for i in range(size): nums[0], nums[size - i - 1] = nums[size - i - 1], nums[0] self.heapAdjust(nums, 0, size - i - 2) return nums
-
使用heapq模块实现优先队列
Python 中的 heapq 模块提供了优先队列算法。函数 heapq.heappush() 用于在队列 queue 上插入一个元素。heapq.heappop() 用于在队列 queue 上删除一个元素需要注意的是:heapq.heappop() 函数总是返回「最小的」的元素。所以我们在使用 heapq.heappush() 时,将优先级设置为负数,这样就使得元素可以按照优先级从高到低排序, 这个跟普通的按优先级从低到高排序的堆排序恰巧相反。这样做的目的是为了 heapq.heappop() 每次弹出的元素都是优先级最高的元素
import heapq class PriorityQueue: def __init__(self): self.queue = [] self.index = 0 def push(self, item, priority): heapq.heappush(self.queue, (-priority, self.index, item)) self.index += 1 def pop(self): return heapq.heappop(self.queue)[-1]
5. 优先队列相关题目
239.滑动窗口最大值
- 思路:
随着窗口的不断右移,我们可以遍历窗口的每个位置来得到窗口的最大值,但这样时间复杂度为O(n * k),会超时,所以我们要进行优化
根据题目可知,我们需要找到每个窗口内的最大值,并且随着窗口位置的每次右移,窗口内只有一个元素会改变,所以,我们可以考虑使用优先队列(大顶堆)来帮助我们实时维护一系列元素中的最大值
当我们向右移动时,删除最左侧元素,并调整,顶部元素即为此窗口内部最大值。(使用(num,index)中的index来判断元素是否在窗口中) - 代码实现:
class Solution: def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: n = len(nums) # 注意,python中的堆默认为小顶堆,需要变为负值来得到结果 q = [(-nums[i], i) for i in range(k)] heapq.heapify(q) ans = [-q[0][0]] for i in range(k, n): heapq.heappush(q, (-nums[i], i)) while q[0][1] <= i - k: heapq.heappop(q) ans.append(-q[0][0]) return ans
347.前K个高频元素
- 思路:
我们先使用一个哈希表来统计每个数字出现的次数,之后建立一个大顶堆,依次从堆中取出k个元素即可
python中heapq模块中的元素可以是元组,元组的第一个元素表示权重
python中heapq模块中的元素可以是元组,元组的第一个元素表示权重
python中heapq模块中的元素可以是元组,元组的第一个元素表示权重(重要的事情说三遍,我之前不会这个) - 代码实现
class Solution: def topKFrequent(self, nums: List[int], k: int) -> List[int]: dic = {} for num in nums: if num in dic: dic[num] += 1 else: dic[num] = 1 num_set = set(nums) q = [] heapq.heapify(q) for num in num_set: heapq.heappush(q, (-dic[num], num)) ans = [] for i in range(k): ans.append(heapq.heappop(q)[1]) return ans