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

这篇具有很好参考价值的文章主要介绍了Python - 优先队列(queue.PriorityQueue & heapq)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

什么是优先队列

为什么需要优先队列?

优先队列是个啥?

优先队列的工作原理

Python实现一个优先队列

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

基本操作

多条件优先级实现

Python内置库中的heapq

heapq的常用操作

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


什么是优先队列

为什么需要优先队列?

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

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

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

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

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

相当于下面实现:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

假设这组数初始有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的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

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

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

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

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

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

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

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

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

优先队列的工作原理

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

当下沉到没有左右孩子,或者大于等于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) - 1
        while child != 0:
            father = (child - 1) // 2
            # 小顶堆,即小的的上浮
            if self.queue[child] < self.queue[father]:
                self.swap(child, father)
                child = father
            else:
                break

    # 出队
    def get(self):
        self.swap(0, len(self.queue) - 1)
        ans = self.queue.pop()
        self.sink()
        return ans

    # 下沉
    def sink(self):
        f = 0
        while True:
            l = 2 * f + 1
            r = l + 1

            t = None
            if len(self.queue) > l >= 0 and len(self.queue) > r >= 0:
                t = r if self.queue[l] > self.queue[r] else l
            elif 0 <= l < len(self.queue) <= r:
                t = l
            else:
                break

            # 小顶堆,即大的下沉
            if self.queue[t] < self.queue[f]:
                self.swap(t, f)
                f = t
            else:
                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) - 1
        while child != 0:
            father = (child - 1) // 2
            if self.queue[child] < self.queue[father]:
                self.swap(child, father)
                child = father
            else:
                break

    # 出队
    def get(self):
        self.swap(0, len(self.queue) - 1)
        ans = self.queue.pop()
        self.sink()
        return ans

    # 下沉
    def sink(self):
        f = 0
        while True:
            l = 2 * f + 1
            r = l + 1

            t = None
            if len(self.queue) > l >= 0 and len(self.queue) > r >= 0:
                t = r if self.queue[l] > self.queue[r] else l
            elif 0 <= l < len(self.queue) <= r:
                t = l
            else:
                break

            if self.queue[t] < self.queue[f]:
                self.swap(t, f)
                f = t
            else:
                break


class Apple:
    def __init__(self, apple, day):
        self.apple = apple
        self.day = day

    def __lt__(self, other):
        return self.day < other.day


class Solution(object):
    def eatenApples(self, apples, days):
        """
        :type apples: List[int]
        :type days: List[int]
        :rtype: int
        """
        pq = PriorityQueue()
        count = 0

        i = 0
        while 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:
                    break

                head = pq.queue[0]
                if head.day <= i or head.apple == 0:
                    pq.get()
                    continue
                else:
                    head.apple -= 1
                    count += 1
                    break

            i += 1

        return count

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

基本操作

queue.PriorityQueue类主要

有方法如下:

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

有属性如下:

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

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

多条件优先级实现

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

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

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

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

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

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

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

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

Python内置库中的heapq

heapq的常用操作

  • heappush
  • heappop

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

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

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

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

因此,我们完全可以基于heapq来实现一个PriorityQueue,实现如下文章来源地址https://www.toymoban.com/news/detail-479351.html

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)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【JavaDS】优先级队列(PriorityQueue),堆,Top-k问题

    ✨ 博客主页: 心荣~ ✨ 系列专栏: 【Java实现数据结构】 ✨ 一句短话: 难在坚持,贵在坚持,成在坚持! 如果有一个 关键码的集合K = {k0,k1, k2,…,kn-1} ,把它的所有元素 按完全二叉树的顺序存储方式存储 在一 个一维数组中 ,并满足: Ki = K2i+1 且 Ki= K2i+2 (Ki = K2i+1 且 Ki = K2i

    2024年02月01日
    浏览(46)
  • Java【优先级队列】详细图解 / 模拟实现 + 【PriorityQueue】常用方法介绍

    📕各位读者好, 我是小陈, 这是我的个人主页 📗小陈还在持续努力学习编程, 努力通过博客输出所学知识 📘如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽 📙 希望我的专栏能够帮助到你: JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统

    2024年02月07日
    浏览(49)
  • 优先级队列priority_queue

    关于less建大根堆,输出降序序列,greater建小根堆,输出升序序列,这点和sort()函数相反,参考我的这篇博客 底层原理 priority_queue底层维护着一个对应类型的,vector物理结构,但相当于堆排序的结构,这个vector逻辑结构是一个二叉堆; 每次 插入数据 ,我们插在堆尾(vector尾),

    2024年02月16日
    浏览(40)
  • C++的stack和queue+优先队列

    适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总 结),该种模式是将一个类的接口转换成客户希望的另外一个接口。 stack和queue都是容器适配器,底层都是通过去适配双端队列deque去实现的,STL中没有把stack和queue划分在容

    2024年02月13日
    浏览(34)
  • 【C++】——栈和队列(stack、queue)及优先队列(priority_queue)的介绍和模拟实现

    今天我们来学习C++stl六大组件的其中一种,容器适配器,stack、queue及priority_queue都是容器适配器。我们循序渐进,接下来让我们先认识一下什么是容器适配器。 适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该

    2024年02月08日
    浏览(51)
  • 【二】一起算法---队列:STL queue、手写循环队列、双端队列和单调队列、优先队列

    纸上得来终觉浅,绝知此事要躬行。大家好!我是霜淮子,欢迎订阅我的专栏《算法系列》。 学习经典算法和经典代码,建立算法思维;大量编码让代码成为我们大脑的一部分。 ⭐️已更系列  1、基础数据结构        1.1、链表➡传送门        1.2、队列➡本章 专栏直达

    2023年04月08日
    浏览(54)
  • C++优先队列(priority_queue)详解

    目录 一、 定义 二、优先队列内元素访问 三、优先队列常用函数 四、优先队列内元素的优先级          优先队列(priority_queue),底层的数据结构为 堆(heap) ,以此 保证队首元素一定是当前队列所有元素中优先级最高的。 我们也可以随时往优先队里面加入(push)元素,其队

    2024年02月16日
    浏览(42)
  • 优先级队列priority_queue模拟实现

    🌟🌟hello,各位读者大大们你们好呀🌟🌟 🚀🚀系列专栏:【C++的学习】 📝📝本篇内容:C++容器优先级队列priority_queue模拟实现 ⬆⬆⬆⬆上一篇:string模拟实现 💖💖作者简介:轩情吖,请多多指教( •̀֊•́ ) ̖́- ①优先级队列是一种容器适配器,它的第一个元素总是

    2024年02月02日
    浏览(44)
  • C++中的优先队列(priority_queue)

    什么是优先队列 优先队列(priority queue) 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有优先级最高先出的性质。通常采用堆数据结构来实现。

    2024年02月15日
    浏览(47)
  • C++ 优先队列 priority_queue 使用篇

    目录 1.储备知识    (1)数据结构:堆   (2)仿函数(函数对象)     [1]理解仿函数     [2]实现仿函数   (3)priority_queue理解     [1]什么是priority_queue (优先队列)?     [2]优先队列性质 2.priority_queue的参数理解(重要!!!)   (1)priority_queue的参数     [1]priority_queue类模板参数     [

    2024年03月12日
    浏览(83)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包