1. 优先队列知识
1.1 优先队列简介
优先队列:一种特殊的队列。在优先队列中,元素被赋予优先级,当访问队列元素时,具有最高优先级的元素最先删除。
优先队列与普通队列最大的不同点在于出队顺序
普通队列:先进先出
优先队列:最高级先出,优先队列是按照元素的优先级来决定出队顺序的,优先级高的优先出队,优先级低的元素后出队。
示意图如:
1.2 优先队列的应用场景
应用场景很多,比如:
- 数据压缩:赫夫曼编码算法;
- 最短路径算法:Dijkstra 算法;
- 最小生成树算法:Prim 算法;
- 任务调度器:根据优先级执行系统任务;
- 事件驱动仿真:顾客排队算法;
- 选择问题:查找第 k 个最小元素。
很多语言都提供了优先级队列的实现。比如,Java 的 PriorityQueue,C++ 的 priority_queue 等。Python 中也可以通过 heapq 来实现优先队列。下面我们来讲解一下优先队列的实现。
1.3 优先队列的基础操作
主要操作:入队操作和出队操作
实现优先队列的方法:「数组(顺序存储)实现」与「链表(链式存储)实现」之外,我们最常用的是使用 [二叉堆结构实现] 优先队列。
1.数组(顺序存储)实现优先队列:
入队操作直接插入到数组队尾,时间复杂度为O(1) 。出队操作需要遍历整个数组,找到优先级最高的元素,返回并删除该元素,时间复杂度为O(n) 。
2.链表(链式存储)实现优先队列:
链表中的元素按照优先级排序,入队操作需要为待插入元素创建节点,并在链表中找到合适的插入位置,时间复杂度为O(n) 。出队操作直接返回链表队头元素,并删除队头元素,时间复杂度为 O(1)。( 将最重要的元素放在链表头部,其它元素按重要性依次接连着)
3.二叉堆结构实现优先队列:
构建一个二叉堆结构,二叉堆按照优先级进行排序。入队操作就是将元素插入到二叉堆中合适位置,时间复杂度为 O(log2^n)。吹对操作则返回二叉堆中优先级最大节点并删除,时间复杂度也是 O(log2^n)。
三者的复杂度:
从上面的表格可以看出,使用「二叉堆」这种数据结构来实现优先队列是比较高效的。
1.4 二叉堆实现的优先队列
1.4.1 二叉堆的定义
二叉堆:符合以下两个条件之一的完全二叉树:
- 大顶堆:根节点值 ≥ 子节点值
- 小顶堆:根节点值 ≤ 子节点值
1.4.2 二叉堆的基本操作
二叉树主要涉及两个基本操作:堆调整方法和将数组构建为二叉堆方法
- 堆调整方法:把移走了最大元素以后的剩余元素组成的序列再构造为一个新的堆积。具体步骤如下:
- 从根节点开始,自上而下地调整节点的位置,使其成为堆积。即把序号为 i 的节点与其左子树节点(序号为 2 * i)、右子树节点(序号为 2 * i + 1)中值最大的节点交换位置。
- 因为交换了位置,使得当前节点的左右子树原有的堆积特性被破坏。于是,从当前节点的左右子树节点开始,自上而下继续进行类似的调整。
- 如此下去直到整棵完全二叉树成为一个大顶堆。
- 将数组构建为二叉堆方法(初始堆建立方法) :
- 如果原始序列对应的完全二叉树(不一定是堆)的深度为 d,则从 d - 1 层最右侧分支节点(序号为 ⌊n/2⌋)开始,初始时令 i = ⌊n/2⌋,调用堆调整算法。
- 每调用一次堆调整算法,执行一次 i = i - 1,直到 i == 1 时,再调用一次,就把原始数组构建为了一个二叉堆。
- 入队操作:
- 先将待插入元素value插入到数组nums末尾
- 如果完全二叉树的深度为d,则从d-1层开始最右侧分支节点(序号为n/2开始,初始时令i = n/2,从下向上依次查找插入位置。
- 遇到value小于当前根节点时, 将其插入到当前位置。否则继续向上寻找插入位置。
- 如果找到插入位置或者达到根位置,将value插入该位置。
- 出队操作:
- 交换数组nums首尾元素,此时nums尾部就是值最大(优先级最高)的元素,将其从nums中弹出,并保存起来。
- 弹出后,对nums剩余元素调用堆调整算法,将其调整为大顶堆。
1.4.3 手写二叉堆实现优先队列
主要实现以下五种方法:
-
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
1.4.4 Python使用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):
# -priority 为了pop推出最大元素
heapq.heappush(self.queue, (-priority, self.index, item))
self.index += 1
def pop(self):
# 推出最大元素
return heapq.heappop(self.queue)[-1]
2.优先队列的相关题目
2.1 题一:0215. 数组中的第K个最⼤元素
代码
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
heap = []
for num in nums:
heapq.heappush(heap, num)
# 只保留TOP-k数
if len(heap) > k:
heapq.heappop(heap)
return heap[0]
2.1 题二:0347. 前 K 个⾼频元素
347. 前 K 个高频元素
代码:
class Heapq:
# 堆调整方法:调整为大顶堆
def heapAdjust(self, nums: [int], nums_dict, index: int, end: int):
left = index * 2 + 1
right = left + 1
while left <= end:
# 当前节点为非叶子结点
max_index = index
if nums_dict[nums[left]] > nums_dict[nums[max_index]]:
max_index = left
if right <= end and nums_dict[nums[right]] > nums_dict[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], nums_dict):
size = len(nums)
# (size - 2) // 2 是最后一个非叶节点,叶节点不用调整
for i in range((size - 2) // 2, -1, -1):
# 调用调整堆函数
self.heapAdjust(nums, nums_dict, i, size - 1)
# 入队操作
def heappush(self, nums: list, nums_dict, value):
nums.append(value)
size = len(nums)
i = size - 1
# 寻找插入位置
while (i - 1) // 2 >= 0:
cur_root = (i - 1) // 2
# value 小于当前根节点,则插入到当前位置
if nums_dict[nums[cur_root]] > nums_dict[value]:
break
# 继续向上查找
nums[i] = nums[cur_root]
i = cur_root
# 找到插入位置或者到达根位置,将其插入
nums[i] = value
# 出队操作
def heappop(self, nums: list, nums_dict) -> int:
size = len(nums)
nums[0], nums[-1] = nums[-1], nums[0]
# 得到最大值(堆顶元素)然后调整堆
top = nums.pop()
if size > 0:
self.heapAdjust(nums, nums_dict, 0, size - 2)
return top
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# 统计元素频数
nums_dict = dict()
for num in nums:
if num in nums_dict:
nums_dict[num] += 1
else:
nums_dict[num] = 1
# 使用 set 方法去重,得到新数组
new_nums = list(set(nums))
size = len(new_nums)
heap = Heapq()
queue = []
for num in new_nums:
heap.heappush(queue, nums_dict, num)
res = []
for i in range(k):
res.append(heap.heappop(queue, nums_dict))
return res
参考
- https://algo.itcharge.cn
- 【博文】 浅入浅出数据结构(15)—— 优先队列(堆) - NSpt - 博客园
- 【博文】 堆(Heap)和优先队列(Priority Queue) - 简书
- 【博文】 漫画:什么是优先队列?- 吴师兄学编程
- 【博文】 Python3,手写一个堆及其简易功能,并实现优先队列,最小堆任务调度等 - pythonstrat 的博客
- 【文档】 实现一个优先级队列 - python3-cookbook 3.0.0 文档
- 【文档】 heapq - 堆队列算法 - Python 3.10.1 文档
- 【题解】 239. 滑动窗口最大值 (优先队列&单调栈) - 滑动窗口最大值 - 力扣