Python Cookbook-5.7 在增加元素时保持序列的顺序

发布于:2025-04-08 ⋅ 阅读:(16) ⋅ 点赞:(0)

任务

你需要维护一个序列,这个序列不断地有新元素加入,但始终处于排序完毕的状态这样你可以在任何需要的时候检查或者删除当前序列中最小的元素。

解决方案

假设有一个未排序的列表,比如:

the_list = [903, 10, 35, 69, 933, 485, 519, 379, 102, 402, 883, 1]

可以调用 the_list.sort( )将列表排序,然后用result = the_list.pop(0)来获得和删除最小的元素。但是,每当加入一个元素(比如the_list.append(0)),都需要再次调用 the_list.sort来排序。

可以使用 Python 标准库的 heapg 模块:

import heapq
heapq.heapify(the_list)

现在列表并不一定完成了排序,但是它却满足堆的特性(若所有涉及的索引都是有效的,则the_list[i] <= the_list[2i+1]且 the_list[i] <= the list[2i+2]),所以,the_list[0]就是最小的元素。为了保持堆特性的有效性,我们使用result = heapq.heappop(the_list)来获取并删除最小的元素,用 heapq.heappush(the_list,newitem)来加入新的元素。如果需要同时做这两件事:加入一个新元素并删除之前的最小的元素,可以用result = heapg.heapreplace(the_list, newitem)。

讨论

当需要以一种有序的方式获取数据时(每次都选择你手中现有的最小元素),可以选择在获取数据时付出运行时代价,或者在加入数据时付出代价。一种方式是将数据放入列表并对列表排序。这样,可以很容易地让你的数据按照顺序从小到大排列。然而,你不得不每次在加入新数据时调用 sort,以确保每次在增加新元素之后仍能够获取最小的元素。Python 列表的sort 实现采用了一种不太有名的自然的归并排序,它的排序开销已经被尽力地压缩了,但仍然难以让人接受:每次添加(和排序)及每次获取(以及删除,通过 pop)的时间,与当前列表中元素的数目成正比(ON),准确地说)。

另一种方案是使用一种叫做堆的组织数据的结构,这是一种简洁的二叉树,它能确保父节点总是比子节点小。在 Python 中维护一个堆的最好方式是使用列表,并用库模块heapq来管理此列表,如同本节解决方案所示的那样。这个列表无须完成排序,但你却能够确保每次你调用 heappop 从列表中获取元素时,总是得到当前最小的元素,然后所有节点会被调整,以确保堆特性仍然有效。每次通过heappush添加元素,或者通过heappop 删除元素,它们所花费的时间都正比于当前列表长度的对数(O(logN),准确地说)。只需要付出一点点代价(从总体来说,代价也非常小)。

举例来说,很适合使用堆方式的场合是这样的:假设有一个很长的队列,并且周期性地有新数据到达,你总是希望能够从队列中获取最重要的元素,而无须不断地重新排序或者在整个队列中搜索。这个概念叫做优先级队列,而堆正是最适合实现它的数据结构。注意,本质上,heapg模块在每次调用heappop时向你提供最小的元素,因此需要安排你的元素的优先级值,以反映出元素的这个特点。举个例子,假设你每次收到数据都付出一个价钱,而任何时候最重要的元素都是队列中价钱最高的那个;另外,对于价钱相同的元素,先到达的要重要一些。下面是一个创建“优先级队列”的类,我们遵循上面提到的要求并使用了 heapq模块的函数:

class priog(object):
	def __init__(self):
		self.q = [ ]
		self.i = 0
	def push(self,item,cost):
		heapq.heappush(self.q, (-cost,self.i, item))
		self.i += 1
	def pop(self):
		return heapq.heappop(self.q)[-1]

这段代码的意图是将价钱设置为负,作为元组的第一个元素,并将整个元组压入堆中,这样更高的出价会产生更小的元组(基于Python 的自然比较方式);在价钱之后,我们放置了一个递增的索引,这样,当元素拥有相同的价钱时,先到达的元素将会处于更小的元组中。在 Python 2.4 中,heapg 模块又被重新实现和进一步优化了,见 5.8 节中更多有关 heapq的信息。