Python - 优先队列(queue.PriorityQueue heapq)

2023-10-13 23:50

本文主要是介绍Python - 优先队列(queue.PriorityQueue heapq),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

什么是优先队列

为什么需要优先队列?

优先队列是个啥?

优先队列的工作原理

Python实现一个优先队列

Python内置库中的queue.PriorityQueue的使用

基本操作

多条件优先级实现

Python内置库中的heapq

heapq的常用操作

基于heapq实现一个优先队列类


什么是优先队列

为什么需要优先队列?

有一个小需求:请取出一组数中的最大数,比如该组数为:1,5,2,8,6,4,3,7,9,0

要是你该如何实现该需求呢?

最简单的策略是:将这组数存入列表,然后调用max取列表的最大值

大家可以去网上搜索下max函数的时间复杂度是O(n)。

相当于下面实现:

我们再来变更下需求,我们需要:取出这组数中前三大的值

那么此时max函数就不太适合了。

最简单的策略是:将这组数存入列表,然后对列表进行降序,然后取前三个数:

那么上面这种策略取出前三个最大数的时间复杂度是多少呢?

这个大家可以去网上搜下Python的sorted方法的使用的算法是timsort,时间复杂度最优是O(n),最差是O(nlogn),平均是O(nlogn)。

 可以发现,其实max函数的性能要比sorted函数要好。

好的,现在我们再变更下需求:

现在有一组数,初始时为:3,5,1,每次我们取出一个最大值后,都会加入一个新的随机数到该组数中,然后再取出这组数中的最大值,然后再加入一个新的随机数,依次往复。

上面这个需求,在现实中有很多场景,比如:

打印机队列,比如打印机总是打印优先级最高的任务,而在其打印过程中,我们随时会加入新的任务进去,当打印机打印完当前任务后,会从任务队列中取出最高优先级的任务打印,而不是先来后到的顺序打印

那么此时该如何实现上面的需求呢?

可能大部分的人的策略会如下:

由于要取出最大值,因此,将这组数存入列表lst,然后lst升序,lst.pop()

加入一个新的数后,继续:lst升序,lst.pop()

那么上面这个逻辑的时间复杂度是多少呢?

假设这组数初始有N个,然后执行M次:取出最大值后,加入一个随机值。

那么时间复杂度为:M * NlogN

其中列表的pop和append都是尾部操作,可以看成O(1)时间复杂度。

那么上面这种类似于打印机工作原理的案例是否有更高效的算法策略呢?

答案是:有的。那就是基于优先队列数据结构。

优先队列是个啥?

我们要从一组数中取出最大值,使用:

  • max函数需要O(n)时间
  • sorted函数需要O(nlogn)时间

这两个方法其实都是基于列表工作的,即数组。我们知道数组是一个线性表数据结构,它在内存上是一段连续的空间,可以基于元素大小,和索引值快速找到对应元素的内存空间。即基于索引获取列表中某个元素只需要O(1)的时间。

但是要找列表中的最大值,则至少需要把所有元素遍历一遍,才能找到,即至少O(n)的时间。这是由于列表的底层数据结构决定的。

而优先队列本质是一个堆结构。

什么是堆呢?

堆其实就是一颗完全二叉树。

什么是完全二叉树呢?

一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

 由上图我们可知,完全二叉树的最深的一层如果节点不满的话,则会优先填满左边。 

并且完全二叉树中某节点的序号为k的话,则其左孩子节点的序号必然为2k+1,其右孩子节点的序号必然为2k+2。因此上面的完全二叉树可以用数组来进行模拟:

可以发现数组的索引刚好就是完全二叉树节点的序号。 

堆结构对应的完全二叉树需要满足以下两个条件之一:

  • 父节点要大于或等于其左右孩子节点,此时堆称为最大堆
  • 父节点要小于或等于其左右孩子节点,此时堆称为最小堆

这样的话,堆结构才能快速地找到最值节点,即堆结构的顶点。

优先队列的工作原理

优先队列底层其实就是一颗完全二叉树。我们向优先队列中加入新元素,其实就是向完全二叉树中加入新元素。

优先队列中每个元素都具有一个优先级属性,该属性会决定元素在完全二叉树中的位置。

而优先队列总是保证最高优先级的元素,处于树根位置。

那么,优先队列如何实现最高优先级的元素,总是处于树根位置呢?

而这其实有涉及到了优先队列的两个常用操作:

  • 出队:取出优先队列的最高优先级元素
  • 入队:加入一个新元素

上面两个操作一旦发生,都可能破坏优先队列的底层数据结构的优先级顺序(即树根是否为最高优先级元素)。而为了防止优先队列的优先级顺序被破坏,上面两个操作又有两个缓冲动作:

  • 出队:下沉
  • 入队:上浮

当我们向优先队列中入队一个新元素,需要先将新元素加入到底层堆结构(实现:数组)的尾部,但是这样的话可能会破坏堆结构的顺序性,因此我们需要通过上浮操作,来调整堆的顺序。

关于上浮操作,请看下面示例:

如下图,是一个最大堆,父节点的值总是大于其左右子孩子节点的值

现在我们需要向堆中新增一个元素29,则先放在尾部,假设此时29的序号为k,则其父节点的序号必然为 Math.floor((k-1)/2) 

然后比较29和其父节点值得大小,如果29 > 父节点值,则交换节点的值,完成29的上浮行为

 

 然后继续比较,29和其父节点值的大小, 如果29 > 父节点值,则交换节点的值,完成29的上浮行为

直到,29发现其小于等于父节点值时,停止上浮,或者29已经上浮到k=0序号位置,即顶点位置时,停止上浮。

当我们需要优先队列出队时,相当于堆结构删除树根元素,但是我们不能冒失的直接将堆顶元素删除,这样会让堆结构散架。

好的做法,是将堆顶元素和堆尾元素值交换,然后将堆尾元素弹出(堆结构可以用数组模拟,因此可以使用pop操作) ,但是此时堆顶元素的值其实并非最大值,因此我们需要使用下沉操作来调整堆结构,维护其顺序性。

关于下沉操作,我们可以看如下示例:

下图是一个最大堆,我们现在需要删除堆顶30

则第一步是交换堆顶元素和堆尾元素的值,然后将堆尾元素弹出

此时最大堆的顺序性被破坏,我们开始执行下沉操作,所谓下沉操作,即将破坏顺序性的节点12和max(左孩子值,右孩子值) 比较,若12< max(左孩子值,右孩子值),则交换

当下沉到没有左右孩子,或者大于等于max(左孩子,右孩子)时,即停止下沉。

我们可以发现,使用堆结构模拟的优先队列,每次入队都会触发上浮操作,每次出队都会触发下沉操作,但是上浮和下沉的次数最多就是完全二叉树的深度,而完全二叉树的深度为logN,也就是说优先队列每次入队和出队的时间复杂度为O(logN)。 

Python实现一个优先队列

下面优先队列类的方法设计参考Python的queue.PriorityQueue

# 小顶堆优先队列
class PriorityQueue:def __init__(self):self.queue = []# 交换元素def swap(self, i, j):self.queue[i], self.queue[j] = self.queue[j], self.queue[i]# 获取优先队列中元素个数def qsize(self):return len(self.queue)# 入队def put(self, ele):self.queue.append(ele)self.swim()# 上浮def swim(self):child = len(self.queue) - 1while child != 0:father = (child - 1) // 2# 小顶堆,即小的的上浮if self.queue[child] < self.queue[father]:self.swap(child, father)child = fatherelse:break# 出队def get(self):self.swap(0, len(self.queue) - 1)ans = self.queue.pop()self.sink()return ans# 下沉def sink(self):f = 0while True:l = 2 * f + 1r = l + 1t = Noneif len(self.queue) > l >= 0 and len(self.queue) > r >= 0:t = r if self.queue[l] > self.queue[r] else lelif 0 <= l < len(self.queue) <= r:t = lelse:break# 小顶堆,即大的下沉if self.queue[t] < self.queue[f]:self.swap(t, f)f = telse:break

实现是否正确的验证:

1705. 吃苹果的最大数目 - 力扣(LeetCode)

class PriorityQueue:def __init__(self):self.queue = []# 交换元素def swap(self, i, j):self.queue[i], self.queue[j] = self.queue[j], self.queue[i]# 入队def put(self, ele):self.queue.append(ele)self.swim()# 上浮def swim(self):child = len(self.queue) - 1while child != 0:father = (child - 1) // 2if self.queue[child] < self.queue[father]:self.swap(child, father)child = fatherelse:break# 出队def get(self):self.swap(0, len(self.queue) - 1)ans = self.queue.pop()self.sink()return ans# 下沉def sink(self):f = 0while True:l = 2 * f + 1r = l + 1t = Noneif len(self.queue) > l >= 0 and len(self.queue) > r >= 0:t = r if self.queue[l] > self.queue[r] else lelif 0 <= l < len(self.queue) <= r:t = lelse:breakif self.queue[t] < self.queue[f]:self.swap(t, f)f = telse:breakclass Apple:def __init__(self, apple, day):self.apple = appleself.day = daydef __lt__(self, other):return self.day < other.dayclass Solution(object):def eatenApples(self, apples, days):""":type apples: List[int]:type days: List[int]:rtype: int"""pq = PriorityQueue()count = 0i = 0while i < len(apples) or len(pq.queue) > 0:if i < len(apples) and apples[i] > 0:pq.put(Apple(apples[i], i + days[i]))while True:if len(pq.queue) == 0:breakhead = pq.queue[0]if head.day <= i or head.apple == 0:pq.get()continueelse:head.apple -= 1count += 1breaki += 1return count

Python内置库中的queue.PriorityQueue的使用

基本操作

queue.PriorityQueue类主要

有方法如下:

  • put:入队
  • get:出队
  • qsize:获取优先队列存储的元素个数

有属性如下:

  • queue:获取优先队列底层堆结构对应的数组,常用于获取最值,而不取出

上面几个方法和属性的含义,可以参考前面实现优先队列的代码。

多条件优先级实现

加入优先队列的元素的优先级可能是多条件的,什么意思呢?

比如一个班级若干个学生,然后要从这些学生中选出一个综合素质最好的学生,条件如下:

  • 文化课成绩越高,综合素质越高
  • 如果文化课成绩相同,则体育成绩越高,综合素质越高

此时我们如何依赖于queue.PriorityQueue来从这些学生中选择最高综合素质的学生呢?

 此时我们可以将加入优先队列的元素设计为一个类,然后将该类的对象加入优先队列。

而对象之间的大小比较,可以基于类定义的__lt__魔术方法实现,__lt__魔术方法可以实现同一个类的两个对象基于比较运算符进行大小比较。

此时优先队列中元素的多条件优先级,就变成了单条件优先级,即元素本身就是优先级。

Python内置库中的heapq

heapq的常用操作

  • heappush
  • heappop

我们可以发现,上面代码中,

  • heapq将我们自定义的pq列表当成了容器,类似于queue.PrioirtyQueue的queue属性。
  • heapq.heappush操作,类似于queue.PrioirtyQueue的put操作
  • heapq.heappop操作,类似于queue.PriorityQueue的get操作

基于heapq实现一个优先队列类

因此,我们完全可以基于heapq来实现一个PriorityQueue,实现如下

import heapq# 小顶堆优先队列
class PriorityQueue:def __init__(self):self.queue = []# 获取优先队列中元素个数def qsize(self):return len(self.queue)# 入队def put(self, ele):heapq.heappush(self.queue, ele)# 出队def get(self):return heapq.heappop(self.queue)

这篇关于Python - 优先队列(queue.PriorityQueue heapq)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/206707

相关文章

Python调用Orator ORM进行数据库操作

《Python调用OratorORM进行数据库操作》OratorORM是一个功能丰富且灵活的PythonORM库,旨在简化数据库操作,它支持多种数据库并提供了简洁且直观的API,下面我们就... 目录Orator ORM 主要特点安装使用示例总结Orator ORM 是一个功能丰富且灵活的 python O

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import

Python Jupyter Notebook导包报错问题及解决

《PythonJupyterNotebook导包报错问题及解决》在conda环境中安装包后,JupyterNotebook导入时出现ImportError,可能是由于包版本不对应或版本太高,解决方... 目录问题解决方法重新安装Jupyter NoteBook 更改Kernel总结问题在conda上安装了

Python如何计算两个不同类型列表的相似度

《Python如何计算两个不同类型列表的相似度》在编程中,经常需要比较两个列表的相似度,尤其是当这两个列表包含不同类型的元素时,下面小编就来讲讲如何使用Python计算两个不同类型列表的相似度吧... 目录摘要引言数字类型相似度欧几里得距离曼哈顿距离字符串类型相似度Levenshtein距离Jaccard相

Python安装时常见报错以及解决方案

《Python安装时常见报错以及解决方案》:本文主要介绍在安装Python、配置环境变量、使用pip以及运行Python脚本时常见的错误及其解决方案,文中介绍的非常详细,需要的朋友可以参考下... 目录一、安装 python 时常见报错及解决方案(一)安装包下载失败(二)权限不足二、配置环境变量时常见报错及

Python中顺序结构和循环结构示例代码

《Python中顺序结构和循环结构示例代码》:本文主要介绍Python中的条件语句和循环语句,条件语句用于根据条件执行不同的代码块,循环语句用于重复执行一段代码,文章还详细说明了range函数的使... 目录一、条件语句(1)条件语句的定义(2)条件语句的语法(a)单分支 if(b)双分支 if-else(