第一章 数据结构和算法(4-9)

本文介绍如何使用Python的heapq模块高效查找最大或最小N个元素,实现优先级队列,以及如何利用collections模块中的defaultdict和OrderedDict进行复杂数据结构的操作,包括字典中的键映射多个值、字典排序和字典的运算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.4 找到最大或最小的N个元素

问题:想在某个集合中找出最大或最小的N个元素
解决方案:
heapq模块的基本情况如下:
在Python中对堆这种数据结构进行了模块化,可通过调用heapq模块来建立堆,同时heapq模块也提供了相应的方法来对堆做操作。基本方法如下:
heap = [] #创建了一个空堆
heappush(heap,item) #往堆中插入一条新的值
item = heappop(heap)#从堆中弹出最小值
item = heap[0] #查看堆中最小值,不弹出
heapify(x) #以线性时间将一个列表转化为堆
item = heapreplace(heap,item) #弹出并返回最小值,然后将heapqreplace方法中item的值插入到堆中,堆的整体结构不会发生改变。
heapq模块中有两个函数——nlargest()nsmallest()。例如:

>>> import heapq
>>> nums=[1,8,2,23,7,-4,18,23,42,37,2]
>>> print(heapq.nlargest(3,nums))
[42, 37, 23]
>>> print(heapq.nsmallest(2,nums))
[-4, 1]

两个print函数分别输出了nums中最大的3个值和最小的2个值。
这两个函数都可以接受一个参数key,从而允许它们工作在更加复杂的数据结构之上。例如:

>>> portfolio=[{'name':'IBM','shares':100,'price':91.1},\
	       {'name':'AAPL','shares':50,'price':543.22},\
	       {'name':'FB','shares':200,'price':21.09},\
	       {'name':'HPQ','shares':35,'price':31.75},\
	       {'name':'YHOO','shares':45,'price':16.35}]
>>> cheap=heapq.nsmallest(2,portfolio,key=lambda s: s['price'])
>>> cheap
[{'name': 'YHOO', 'shares': 45, 'price': 16.35}, {'name': 'FB', 'shares': 200, 'price': 21.09}]

讨论
如果正在寻找最大或最小N个元素,且同几何中元素的总数目相比,N很小,则下列函数可提供更好的性能。这些函数首先会在底层将数据转化成列表,且元素会以堆的顺序排序。

nums=[1,8,2,23,7,-4,18,23,42,37,2]
>>> heap=list(nums)
>>> heap
[1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
>>> heapq.heapify(heap)
>>> heap
[-4, 2, 1, 23, 7, 2, 18, 23, 42, 37, 8]

堆的重要特性:heap[0]总是最小元素。此外,接下来的元素可依次通过 heapq.heappop() 方法轻松找到。该方法会将第一个元素弹出,然后以第二小的元素取而代之。
当所要找的元素数量相对较小时,函数nlargest()nsmallest() 才是最适用的。若只想找到最大值和最小值,则应使用max()和min()

1.5 实现优先级队列

问题:想要实现一个队列,能够以给定的优先级对元素排序,且每次pop操作时都会返回优先级最高的元素
解决方案
下面的类利用heapq模块实现了一个简单的优先级队列:

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]

class Item:
    def __init__(self,name):
        self.name=name
    def __repr__(self):
        return 'Item({!r})'.format(self.name)

下面是对其的应用:

q=PriorityQueue()
q.push(Item('foo'),1)
q.push(Item('bar'),5)
q.push(Item('spam'),4)
q.push(Item('grok'),1)
q.pop()
Item('bar')
q.pop()
Item('spam')
q.pop()
Item('foo')

若两个有着相同优先级的元素,pop操作按照它们被插入到队列的顺序返回。
讨论
上面的代码中,队列包含了一个(-priority,index,item)的元组。优先级为负数的目的是使得元素按照优先级从高到低排序。这与普通的按优先级从低到高排序的堆排序恰巧相反。
index变量的作用是保证同等优先级元素的正确排序。通过保存一个不断增加的index下标变量,可以确保元素按照它们插入的顺序排序。而且,index变量也在相同优先级元素比较的时候起到重要作用。

1.6 字典中的键映射多个值

问题:怎样实现一个键对应多个值的字典(multidict)
解决方案
若想要一个键映射多个值,需将这多个值放到另外的容器中,比如列表或集合中。若想保持元素插入的顺序,则使用列表,若需要数据去重,则使用集合。
可以很方便的使用collections模块中的default来构造这样的字典。defaultdict的一个特征是它会自动初始化每个key刚开始对应的值,所以只需关注添加元素操作。例如:

>>> from collections import defaultdict
>>> d=defaultdict(list)
>>> d['a'].append(1)
>>> d['a'].append(2)
>>> d['a'].append(4)
>>> d['b'].append(5)
>>> d
defaultdict(<class 'list'>, {'a': [1, 2, 4], 'b': [5]})

1.7 字典排序

问题:创建一个字典,并在迭代或序列化这个字典时能控制元素的顺序。
解决方案
可使用collections模块中的OrderedDict类。在迭代操作时它会保持元素被插入时的顺序。示例如下:

>>> from collections import OrderedDict
>>> d=OrderedDict()
>>> d['foo']=1
>>> d['bar']=2
>>> d['spam']=3
>>> d['grok']=4
>>> d
OrderedDict([('foo', 1), ('bar', 2), ('spam', 3), ('grok', 4)])
>>> d
OrderedDict([('foo', 1), ('bar', 2), ('spam', 3), ('grok', 4)])

当想构建一个将来需要序列化或编码成其他格式的映射时,OrderedDict很有用。
讨论
OrderedDict内部维护这一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来时,它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。
一个OrderedDict的大小是一个普通字典的两倍,因其内部维护着另外一个链表。

1.8 字典的运算

问题:怎样在数据字典中执行一些计算操作,如求最大值、最小值、排序等。
解决方案
考虑下面的股票名称和价格映射字典:

>>> prices={\
	'ACME':45.23,
	'AAPL':612.78,
	'IBM':205.55,
	'HPQ':37.20,
	'FB':10.75}

为了对字典值进行计算操作,通常需使用zip()函数先将键和值反转。例如,下面是查找最小和最大股票价格和股票值的代码:

>>> min_price=min(zip(prices.values(),prices.keys()))
>>> min_price
(10.75, 'FB')
>>> max_price=max(zip(prices.values(),prices.keys()))
>>> max_price
(612.78, 'AAPL')

类似地,可使用zip()和sorted()排序:

>>> prices_sorted=sorted(zip(prices.values(),prices.keys()))
>>> prices_sorted
[(10.75, 'FB'), (37.2, 'HPQ'), (45.23, 'ACME'), (205.55, 'IBM'), (612.78, 'AAPL')]

需要注意的是,zip()函数创建的是一个只能方位一次的迭代器。
讨论
如果在一个字典上执行普通的数学运算,会发现它们仅仅作用于键,而不是值。而前面的zip()函数方案通过将字典反转为(值,键)元组序列来解决上述问题。当比较两个元组时,值会先进行比较,然后才是键。
但当多个实体拥有相同的值时,键会决定返回结果。

1.9 查找两字典的相同点

问题:怎样在两个字典中寻找相同点(比如相同的键,值等)
解决方案
考虑下列字典:

>>> a={
	'x':1,
	'y':2,
	'z':3}
>>> b={
	'w':10,
	'x':11,
	'y':2}

为寻找两个字典的相同点,可以简单的在两字典的keys()或者items()方法返回结果上执行集合操作。例如:

>>> a.keys()&b.keys()
{'y', 'x'}
>>> a.keys()-b.keys()
{'z'}
>>> a.items()&b.items()
{('y', 2)}

这些操作也可用于修改或过滤字典元素。比如,假如你想以现有字典构造一个排除几个指定键的新字典。下面利用字典推导来实现这样的需求:

>>> c={key:a[key] for key in a.keys()-{'z','w'}}
>>> c
{'y': 2, 'x': 1}

讨论
键视图的一个很少被了解的特性即它们也支持集合操作,比如集合并、交、差运算。故若想对集合的键执行一些普通的集合操作,可直接使用键视图对象而不必先将它们转换成一个set。
字典的items()方法的返回值也支持集合操作。但values()方法的返回值不支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值