堆的定义
堆是一种特殊的树形数据结构,每个节点都有一个值,通常我们所说的堆的数据结构指的是二叉树。堆的特点是根节点的值最大(或者最小),而且根节点的两个孩子也能与孩子节点组成子树,亦然称之为堆。
堆分为两种,大根堆和小根堆是一颗每一个节点的键值都不小于(大于)其孩子节点的键值的树。无论是大根堆还是小根堆(前提是二叉堆)都可以看成是一颗完全二叉树。下面以图的形式直观感受一下:
heapq模块
在Python中也对堆这种数据结构进行了模块化,我们可以通过调用heapq模块来建立堆这种数据结构,同时heapq模块也提供了相应的方法来对堆做操作。
有兴趣的朋友可以直接导入heapq模块来查看它提供了哪些方法。在这里我们简单的来介绍一下。
heap = [] #创建了一个空堆
heappush(heap,item) #往堆中插入一条新的值
item = heappop(heap) #从堆中弹出最小值
item = heap[0] #查看堆中最小值,不弹出
heapify(x) #以线性时间讲一个列表转化为堆
item = heapreplace(heap,item) #弹出并返回最小值,然后将heapqreplace方法中item的值插入到堆中,堆的整体结构不会发生改变。这里需要考虑到的情况就是如果弹出的值大于item的时候我们可能就需要添加条件来满足function的要求
if item > heap[0]
item = heapreplace(heap, item)
heappushpop() #顾名思义,将值插入到堆中同时弹出堆中的最小值。
merge(*iterables) #合并多个堆然后输出
>>>list(merge([1,3,5,7],[0,2,4,8],[5,10,15,20],[],[25]))
[0, 1, 2, 3, 4, 5, 5, 7, 8, 10, 15, 20, 25]
nlargest(n , iterbale, key=None)
从堆中找出做大的N个数,key的作用和sorted( )方法里面的key类似,用列表元素的某个属性和函数作为关键字。
>>>a = [0, 1, 2, 3, 4, 5, 5, 7, 8, 10, 15, 20, 25]
>>>heapq.nlargest(5,a)
[25, 20, 15, 10, 8]
这样就返回了列表中前五个最大的数。
>>>b = [('a',1),('b',2),('c',3),('d',4),('e',5)]
>>>heapq.nlargest(1,b,key=lambda x:x[1])
[('e', 5)]
加入key之后的使用方法。
nsmallest(n, iterable, key=None) #找到堆中最小的N个数用法同上。
注意:在官方给出来的文档中写道使用复合操作方法会比使用单行为操作方法会更快一些。例如使用heappushpop( )方法会比先使用heappush( )再使用heappop( )这两单个方法效率来说会更高。
例子
在上篇文章中讲到的堆排序如果用到heapq模块就会变得非常好写。首先我们只需要建立一个空堆然后将数导入堆中,再进行弹出操作这样就形成了一个堆排序。
def heapsort(iterable):
h = []
for value in iterable:
heapq.heappush(h,value) #[0, 1, 2, 6, 3, 5, 4, 7, 8, 9]
return [heapq.heappop(h) for i in range(len(h))]
print heapsort([1,3,5,7,9,2,4,6,8,0])
在heappush方法的时候heapq就自动给你建立好了一个堆。当然如果对于已有的列表转换成堆也好办,heapq给我们提供了heapify( )方法。
def HeapSort(list):
heapq.heapify(list)
heap = []
while list:
heap.append(heapq.heappop(list))
list[:] = heap
return list
print HeapSort([1,3,5,7,9,2,4,6,8,0])
当然单个的数列明显不是堆的正常要求,我们可以使用数组的形式来筛选出我们想要的值。这样的话我们能够快速的对字典中我们想要的值做查找,利用刚刚我们讲到的nlargest和nsmallest。
>>> h = []
>>> heappush(h, (5, 'write code'))
>>> heappush(h, (7, 'release product'))
>>> heappush(h, (1, 'write spec'))
>>> heappush(h, (3, 'create tests'))
>>> heappop(h)
(1, 'write spec')
延伸
堆作为数据结构在内存和二级缓存中充当了重要的角色。优先队列中也会经常使用堆,这也就给堆数据结构提出了很多挑战。例如内存中存放了数多个计划任务的时候我们可以定义一个数列list(priority,task)来保存在堆结构中。但是这样就出现了很多问题
1.排序的稳定性:当任务加入到堆中时,如果两个任务有同等的优先级,两个任务实际上在列表里是没什么区别的,那我怎么得到返回值?
2.在Python3以后的版本中,如果元组(priority,task)priority是一样的,而且task没有一个默认的比较参照值,那这样我们其实是没有办法来比较的。
3.如果一个任务的优先级发生了改变,那么我们如何来处理该任务在相应堆中优先级的变化,堆中位置肯定会改变。
4.如果一个任务因为要等待其他的任务(最简单的比方,等待父进程)而照成悬挂状态,我们如何在堆中去找到它并且做相应的操作(降低优先级或者删除该任务)
解决前两个问题的方法我们可以采用三元数组的方法。设置一个优先级,一个条目值,一个任务值。即使当两个任务有相同优先级的时候,因为条目值不一样可以帮助cpu来裁决它们被加载的顺序。
剩下需要解决的问题是如何找到被悬挂而推迟的任务,然后尝试去修改优先级或者永久删除这个任务。我们可以使用字典,来指向堆中某个任务的条目值。
最后就是删除操作,删除会改变堆的结构。为了保证堆结构的特性,我们可以标记已有将被删除的任务的条目值,然后将该任务重新打标加入到堆中。
pq = [] # list of entries arranged in a heap
entry_finder = {} # mapping of tasks to entries
REMOVED = '<removed-task>' # placeholder for a removed task
counter = itertools.count() # unique sequence count
def add_task(task, priority=0):
'Add a new task or update the priority of an existing task'
if task in entry_finder:
remove_task(task)
count = next(counter)
entry = [priority, count, task]
entry_finder[task] = entry
heappush(pq, entry)
def remove_task(task):
'Mark an existing task as REMOVED. Raise KeyError if not found.'
entry = entry_finder.pop(task)
entry[-1] = REMOVED
def pop_task():
'Remove and return the lowest priority task. Raise KeyError if empty.'
while pq:
priority, count, task = heappop(pq)
if task is not REMOVED:
del entry_finder[task]
return task
raise KeyError('pop from an empty priority queue')