Python中heapq与优先队列【详细】

本文始发于个人公众号:TechFlow, 原创不易,求个关注

今天的文章来介绍Python当中一个蛮有用的库——heapq

heapq的全写是heap queue,是堆队列的意思。这里的堆和队列都是数据结构,在后序的文章当中我们会详细介绍,今天只介绍heapq的用法,如果不了解heap和queue原理的同学可以忽略,我们并不会深入太多,会在之后的文章里详细阐述。

在介绍用法之前,我们需要先知道优先队列的定义。队列大家应该都不陌生,也是非常基础简单的数据结构。我们可以想象成队列里的所有元素排成一排,新的元素只能从队尾加入队列,元素要出队列只能通过队首,不能中途从队列当中退出。而优先队列呢,是给队列当中的元素每一个都设置了优先级,使得队伍当中的元素会自动按照优先级排序,优先级高的排在前面。

也就是说Python当中的heapq就是一个维护优先队列的library,我们通过调用它可以轻松实现优先队列的功能。

最大或最小的K个元素

我们来看一个实际的问题,假设我们当下有N个杂乱无章的元素,但是我们只关心其中最大的K个或者是最小的K个元素。我们想从整个数组当中将这部分抽取出来,应该怎么办呢?

这个问题在实际当中非常常见,随便就可以举出例子来。比如用户输入了搜索词,我们根据用户的搜索词找到了大量的内容。我们想要根据算法筛选出用户最有可能点击的文本来,机器学习的模型可以给每一个文本一个预测的分数。之后,我们就需要选出分数最大的K个结果。这种类似的场景还有很多,利用heapq库里的nlargest和nsmallest接口可以非常方便地做到这点。

我们一起来看一个例子:

import heapq

nums = [14, 20, 5, 28, 1, 21, 16, 22, 17, 28]
heapq.nlargest(3, nums)
# [28, 28, 22]
heapq.nsmallest(3, nums)
# [1, 5, 14]

heapq的nlargest和nsmallest接受两个参数,第一个参数是K,也就是返回的元素的数量,第二个参数是传入的数组,heapq返回的正是传入的数组当中的前K大或者是前K小。

这里有一个问题,如果我们数组当中的元素是一个对象呢?应该怎么办?

其实也很简单,有了解过Python自定义关键词排序的同学应该知道,和排序一样,我们可以通过匿名函数实现。

匿名函数

我们都知道,在Python当中通过def可以定义一个函数。通过def定义的函数都有函数名,所以称为有名函数。除了有名函数之外,Python还支持匿名函数。顾名思义,就是没有函数名的函数。也就是说它其他方面都和普通函数一样,只不过没有名字而已。

初学者可能会纳闷,函数没有名字应该怎么调用呢

会有这个疑惑很正常,这是因为习惯了面向过程的编程,对面向对象理解不够深入导致的。在许多高级语言当中,一切皆对象,一个类,一个函数,一个int都是对象。既然函数也是对象,那么函数自然也可以用来传递,不仅可以用来传递,还可以用来返回。这是函数式编程的概念了,我们这里不多做深入。

当然,普通函数也一样可以传递,起到的效果一样。只不过在编程当中,有些函数我们只会使用一次,没必要再单独定义一个函数,使用匿名函数会非常方便。

举个例子,比方说我有一个这样的函数:

def operate(x, func):
  return func(x)

这个operate函数它接受两个参数,第一个参数是变量x,第二个参数是一个函数。它会在函数内部调用func,返回func调用的结果。我现在要做这样一件事情,我希望根据x这个整数对4取余的余数来判断应该用什么样的func。如果对4的余数为0,我希望求一次方,如果余数是2,我希望求平方,以此类推。如果按照正常的方法,我们需要实现4个方法,然后依次传递。

这当然是可以的,不过非常麻烦,如果使用匿名函数,就可以大大简化代码量:

def get_result(x):
  if x % 4 == 0:
    return operate(x, lambda x: x)
  elif x % 4 == 1:
    return operate(x, lambda x: x ** 2)
  elif x % 4 == 2:
    return operate(x, lambda x: x ** 3)
  else:
    return operate(x, lambda x: x ** 4)

在上面的代码当中,我们通过lambda关键字定义了匿名函数,避免了定义四种函数用来传递的情况。当然,这个问题还有更简单的写法,可以只用一个函数解决。

我们来看lambda定义匿名函数的语法,首先是lambda关键字,表示我们当下定义的是一个匿名函数。之后跟的是这个匿名函数的参数,我们只用到一个变量x,所以只需要写一个x。如果我们需要用到多个参数,通过逗号分隔,当然也可以不用参数。写完参数之后,我们用冒号分开,冒号后面写的是返回的结果。

我们也可以把匿名函数赋值给一个变量,之后我们就可以和调用普通函数一样来调用了:

square = lambda x: x ** 2

print(square(3))
print(operate(3, square))

自定义排序

回到之前的内容,如果我们想要heapq排序的是一个对象。那么heapq并不知道应该依据对象当中的哪个参数来作为排序的衡量标准,所以这个时候,需要我们自己定义一个获取关键字的函数,传递给heapq,这样才可以完成排序。

比如说,我们现在有一批电脑,我们希望heapq能够根据电脑的价格排序:

laptops = [
    {'name': 'ThinkPad', 'amount': 100, 'price': 91.1},
    {'name': 'Mac', 'amount': 50, 'price': 543.22},
    {'name': 'Surface', 'amount': 200, 'price': 21.09},
    {'name': 'Alienware', 'amount': 35, 'price': 31.75},
    {'name': 'Lenovo', 'amount': 45, 'price': 16.35},
    {'name': 'Huawei', 'amount': 75, 'price': 115.65}
]

cheap = heapq.nsmallest(3, portfolio, key=lambda s: s['price'])
expensive = heapq.nlargest(3, portfolio, key=lambda s: s['price'])

在调用nlargest和nsmallest的时候,我们额外传递了一个参数key,我们传入的是一个匿名函数,它返回的结果是这个对象的price,也就是说我们希望heapq根据对象的price来进行排序。

优先队列

heapq除了可以返回最大最小的K个数之外,还实现了优先队列的接口。我们可以直接调用heapq.heapify方法,输入一个数组,返回的结果是根据这个数组生成的堆(等价于优先队列)。

当然我们也可以从零开始,直接通过调用heapq的push和pop来维护这个堆。接下来,我们就通过heapq来自己动手实现一个优先队列,代码非常的简单,我想大家应该可以瞬间学会

首先是实现优先队列的部分:

import heapq

class PriorityQueue:

  def __init__(self):
    self._queue = []
    self._index =0

  def push(self, item, priority):
    # 传入两个参数,一个是存放元素的数组,另一个是要存储的元素,这里是一个元组。
    # 由于heap内部默认有小到大排,所以对priority取负数
    heapq.heappush(self._queue, (-priority, self._index, item))
    self._index += 1

  def pop(self):
    return heapq.heappop(self._queue)[-1]

其次我们来实际看一下运用的情况:

q = PriorityQueue()

q.push('lenovo', 1)
q.push('Mac', 5)
q.push('ThinkPad', 2)
q.push('Surface', 3)

q.pop()
# Mac
q.pop()
# Surface

到这里,关于heapq的应用方面就算是介绍完了,但是还没有真正的结束。

我们需要分析一下heapq当中操作的复杂度,关于堆的部分我们暂时跳过,我们先来看nlargest和nsmallest。我在github当中找到了这个库的源码,在方法的注释上,作者写下了这个方法的复杂度,和排序之后取前K个开销五五开

def nlargest(n, iterable, key=None):
    """Find the n largest elements in a dataset.

    Equivalent to:  sorted(iterable, key=key, reverse=True)[:n]
    """

我们都知道排序的复杂度的期望是\(O(nlogn)\),如果你了解堆的话,会知道堆一次插入元素的复杂度是\(logn\)。如果我们限定堆的长度是K,我们插入n次之后也只能保留K个元素。每次插入的复杂度是\(logK\),一共插入n次,所以整体的复杂度是\(nlogK\)。

如果K小一些,可能开销会比排序稍小,但是程度有限。那么有没有什么办法可以不用排序并且尽可能快地筛选出前K大或者是前K小的元素呢?

我这里先卖个关子,我们之后的文章当中再来讲解。

今天的文章就到这里,如果觉得有所收获,请顺手点个关注吧,你的举手之劳对我很重要。

参考资料

Python CookBook Version3

维基百科

原文地址:https://www.cnblogs.com/techflow/p/12293767.html

时间: 2024-10-12 07:37:56

Python中heapq与优先队列【详细】的相关文章

(数据科学学习手札32)Python中re模块的详细介绍

一.简介 关于正则表达式,我在前一篇(数据科学学习手札31)中已经做了详细介绍,本篇将对Python中自带模块re的常用功能进行总结: re作为Python中专为正则表达式相关功能做出支持的模块,提供了一系列方法来完成几乎全部类型的文本信息的处理工作,下面一一介绍: 二.re.compile() 在前一篇文章中我们使用过这个方法,它通过编译正则表达式参数,来返回一个目标对象的匹配模式,进而提高了正则表达式的效率,主要参数如下: pattern:输入的欲编译正则表达式,需将正则表达式包裹在''内传

python中的dict的详细介绍

一.dict的特性 dict是python中的一个可变的数据类型,用{}表示,dict的key必须是不可变的数据类型,而value的数据类型可以任意 格式:{key:value,key:value,key:value} 注:键值对如果是字符串使用单引号,最后一个键值对没有逗号 dict的优点: ①:查询速度快,可以二分查找 ②:key是不可以重复的 注: 不可变数据类型: 元组,bool,int , str 可以hash 可变数据类型: dict ,list, set 二.dict的方法: (1

python中的logger模块详细讲解

logger 提供了应用程序可以直接使用的接口handler将(logger创建的)日志记录发送到合适的目的输出filter提供了细度设备来决定输出哪条日志记录formatter决定日志记录的最终输出格式 logging模块介绍 Python的logging模块提供了通用的日志系统,熟练使用logging模块可以方便开发者开发第三方模块或者是自己的Python应用.同样这个模块提供不同的日志级别,并可以采用不同的方式记录日志,比如文件,HTTP.GET/POST,SMTP,Socket等,甚至可

python中模块的__all__详细使用

python模块中的__all__,用于模块导入时限制,如:from module import * 此时被导入模块若定义了__all__属性,则只有__all__内指定的属性.方法.类可被导入:若没定义,则导入模块内的所有公有属性,方法和类. 1.实例1 #bb.pyclass A(): def __init__(self,name,age): self.name=name self.age=ageclass B(): def __init__(self,name,id): self.name

Python中字典的详细用法

#字典 #字典是Python中唯一内建的映射类型.字典中没有特殊的顺序,但都是存储在一个特定的键(key)下面,键可以是数字,字符串,甚至是元组 #一.字典的使用 #在某些情况下,字典比列表更加适用: #1.表示一个游戏棋盘的状态,每个键都是由坐标值组成的元组 #2.存储文件修改时间,用文件名作为键; #3.数字电话\地址薄 #1.使用列表创建一个电话本,(这里用字符串表示电话号码,以0开头的数字回会被编译成8进制数字) name=["A","B","C&

python中的数据结构

1.列表 (1)   建立列表 list('python') ['p', 'y', 't', 'h', 'o', 'n'] (2)列表的常用方法: append   在列表末尾添加元素 >>>l=['a','b','c'] >>>l.append('d') >>>l ['a','b','c','d'] count   统计某个元素在列表中出现的次数 >>>['a','a','b','c'].count('a') 2 extend  扩

python学习笔记29(python中堆的使用)

堆(heap):优先队列的一种,使用优先队列能够以任意顺序增加对象,并且能在任意时间(可能在增加对象的同时)找到(也可能是移除)最小元素,比用于列表中min的方法要高效. Python中并没有独立的堆类型,只有一个包涵一些堆操作函数的模块,这个模块叫heapq. import heapq 1.heapq.heappush(heap,item)  #heap为定义堆,item 增加的元素; eg. heap=[]   heapq.heappush(heap, 2) 2.heapq.heapify(

python中的堆排序peapq模块

heapq模块实现了python中的堆排序,并提供了有关方法.让用Python实现排序算法有了简单快捷的方式. heapq的官方文档和源码:8.4.heapq-Heap queue algorithm 下面通过举例的方式说明heapq的应用方法 实现堆排序 #! /usr/bin/evn python #coding:utf-8 from heapq import * def heapsort(iterable): h = [] for value in iterable: heappush(h

python中的那些“神器”

"武林至尊,宝刀屠龙,号令天下,莫敢不从,倚天不出,谁与争锋",这是神器.不过今天要说的python中的"神器"就没有这么厉害了,这里要说的"神器"其实就是名称里面带了个"器"的,如下: 列表解析器 迭代器 生成器 装饰器 列表解析器 现在遇到了这样一个问题需要解决:"有一个数字的列表,要求对该列表中的奇数乘以2,返回处理完成后的列表(不改变原来列表的顺序,仅对列表中的奇数乘以2)",比较传统的方法可能会是