优先队列也是一种重要的缓存结构,基于二叉树,可以做出优先队列的一种高效实现。
概念
优先队列与栈和队列类似,将数据元素保存在其中,可以访问和弹出。优先队列的特点是存入其中的元素都另外附有一个值,表示这个元素的优先程度,称其为优先值。优先队列应该保证在任何时候,弹出的应该是保存在这个容器中优先值最高的元素,如果该元素不弹出,再次访问时仍弹出该元素。
也有可能出现这种情况:优先队列中有两个元素有一样的优先程度,这时就要考虑弹出的方法:一是相同优先程度的元素先进先出,那么就会做出效率比较低的实现;另一种是只要弹出的是最高优先程度的就可以,不保证哪个先出,这样可以做出效率较高的实现。
优先队列的操作:
- 创建,判断空,还可以有清空内容、确定当前元素个数等。
- 插入元素,访问和弹出当前当前优先程度最高的元素。
基于线性表的实现
数据项在连续表里的存储顺序可用于表示数据之间的某种顺序关系,对于优先队列,这个顺序可用于表示优先级关系,让数据的存储顺序按优先顺序排列。
这里有两种实现方案:
1、在存入数据时,保证表中元素始终按优先顺序排列,任何时候都可以直接访问当时表里优先最高的元素;采用有组织的存入方式,存入元素的操作效率可能比较低,但访问和弹出时比较方便。
2、存入数据时用最简单的方法,顺序表插入尾端,链接表插入首端;需要取用时,通过检索找到优先程度最高的的元素。采用这种方式存入效率高,但取用效率低。
基于list实现优先队列
假定需要存储的数据用“<=”比较优先级,值较小的元素优先级更高。
首先定义一个异常类:
class PrioQueueError(ValueError):
pass
将优先队列定义为一个类:
class PrioQue:
#这里用list转换,首先对实参做一个拷贝,以免共享;另外,可以使构造函数实参可以是任何可迭代对象。
def __init__(self, elist=[]):
self._elems = list(elist)
self._elems.sort(reverse=True)
#从最右边开始与e比较,当遇到比e大的元素时就插入到他的后边,同时也保证了相同优先级元素先进先出
def enqueue(self, e):
i = len(self.elems) - 1
while i >= 0:
if self._elems[i] <= e:
i -= 1
else:
break
self._elems.insert(i+1, e)
def is_empty(self):
return not self._elems
def peek(self):
if self.is_empty():
raise PrioQueueError('in top.')
return self._elems[-1]
def dequeue(self):
if self.is_empty():
raise PrioQueueError('in pop.')
return self._elems.pop()
采用这种方式实现的优先队列,插入元素是 O(n) O ( n ) 操作,其他都是 O(1) O ( 1 ) 操作。
树形结构和堆
由于线性表的特点,只要元素按优先级顺序排列,就无法避免线性复杂性问题,如果不改变数据的线性顺序存储方式,就不可能突破 O(n) O ( n ) 的复杂度限制。要做出效率更高的优先队列就必须考虑其他的数据结构组织方式。
堆及其性质
从结构上看,堆就是结点里存储数据的完全二叉树,但堆中存储数据需要满足一种特殊的堆序:任一个结点里所存的数据,先于或等于其子结点里的数据。
- 在一个堆中从树根到任何一个叶结点的路径上,各结点里所存的数据按规定的优先关系递减。
- 堆中最优先的元素必定位于二叉树的跟结点里,
O(1)
O
(
1
)
时间就能得到。
- 位于树中不同路径上的元素,这里不关心其顺序关系。
根据所要求的序,分为小顶堆和大顶堆。
一棵完全二叉树可以自然的存入一个连续线性结构,因此,一个堆也可以存入一个连续表,通过下标就可以方便的找到它的父/子结点。
堆和完全二叉树还有下面几个性质:
1、在一个堆的最后加入一个元素,整个结构还可以看成是完全二叉树,但不一定是堆。
2、一个堆去掉堆顶,其余元素形成两个子堆。
3、给由2得到的表加入一个根元素,得到的又可以看作完全二叉树,但不一定是堆。
4、去掉一个堆的最后元素,剩下的仍然是一个堆。
优先队列的堆实现
这里需要考虑两个问题:
1、如何实现插入元素的操作,向堆中插入一个元素,结果还为堆。
2、如何实现弹出元素的操作,弹出元素后,剩余元素重新做成堆。
解决堆插入和弹出的关键操作称为筛选,又分为向上筛选和向下筛选。
首先考虑堆中加入元素的操作,为了在堆的最后加入一个元素仍然回复称堆,就需要做一次向上筛选:不断用新加入的元素
e
e
与其父结点比较,如果较小就交换两个元素的位置,通过这样的比较和交换,元素
e
e
不断上移,直到到达特定位置才会停下,这样就得到了基于堆的优先队列插入操作:
- 把新加入元素放在已有元素之后,执行一次向上筛选。
- 向上筛选操作不会超过当前二叉树最长路径的长度,综合操作时间为。
由于堆顶元素就是最优先元素,应该弹出的元素就是它,但弹出后剩下的元素不再是堆,依据上边第四条,可以取出最后一个元素,把这个元素放在堆顶,就得到了一棵完全二叉树,但是由于根的元素不满足,所以不是堆,下面需要设法把结构重新恢复成堆:
这种情况下的操作称为向下筛选:设两个子堆A、B加上元素e构成一棵完全二叉树,把它们做成一个堆,操作步骤为:
1、用e与A、B两个子堆的堆顶比较,最小者作为整个堆的堆顶。
- 如果e不是最小,则为A或B,将最小的一个移到堆顶,相当于删去了一个子堆的堆顶。
- 下面把e放入到删去堆顶的子堆,然后同上方一样,继续循环。
2、如果某次比较中e最小,则整个结构就成为堆。
3、如果e已经落到低,整个结构也成为堆。
弹出操作和取最后一个元素操作都为
O(1)
O
(
1
)
时间,向下筛选操作为
O(logn)
O
(
l
o
g
n
)
,综合起来也是
O(logn)
O
(
l
o
g
n
)
时间操作。
基于堆的优先队列类
class PrioQueue:
def __init__(self, elisy=[]):
self._elems = list(elist)
if elist:
self.buildheap()
def is_empty(self):
return not self._elems
def peek(self):
if self.is_empty():
raise PrioQueueError("in peek.")
return self._elems[0]
def enqueue(self, e):
self._elems.qppend(None)
self.siftup(e, len(self._elems)-1)
#先插入一个空值,每次用该位置的父元素与e比较,知道找到合适的位置,再把e插入该位置。
def siftuo(self, e, last):
elems, i, j = self._elems, last, (last-1)//2
while i > 0 and e < elems[j]:
elems[i] = elems[j]
i, j = j, (j-1)//2
elems[i] = e
def dequeue(self):
if self.is_empty():
raise PrioQueueError("in dequeue.")
elems = self._elems
e0 = elems[0]
e = elems.pop()
if len(elems) > 0:
self.siftdown(e, 0, len(elems))
return e0
#
def siftdown(self, e, begin, end):
elems, i, j = self._elems, begin, begin*2+1
while j < end: #j不能超出堆外
if j+1 < end and elems[j+1] < elems[j]: #下一层的两个兄弟结点比较
j += 1
if e < elems[j]: #如果e比下一层两个兄弟结点较小的那一个还小,则不用再筛选
break
elems[i] = elems[j] #如果e比较大,则交换位置
i, j = j, 2*j+1
elems[i] = e
最后考虑堆的初始构建,一个元素的序列是一个堆,从下标 end//2 e n d / / 2 的位置开始,后面的元素都为叶结点,都是一个个单独的堆,从这里开始往前,向左一个个建堆,直到整个表建成一个堆。
def buildheap(self):
end = len(self._elems)
for i in range(end//2, -1, -1): #从最后一个分支节点开始,把他和他的子结点看成需要向下筛选的一个整体,依次往前,直到每个分支节点都向下筛选一次
self.siftdown(self._elems[i], i, end)
堆的应用:堆排序
如果一个连续表里存储的数据是一个小顶堆,按优先队列的操作方式反复弹出堆顶元素,能够udedao一个递增序列,这样可以实现一种连续表中元素的排序工作。
def heap_sort(elems):
def siftdown(elems, e, begin, end):
i, j = begin, begin*2+1
while j < end:
if j + 1 < end and elems[j+1] , elems[j]:
j += 1
if e < elems[j]:
break
elems[i] = elems[j]
i, j = j, 2*j+1
elems[i] = e
end = len(elems)
for i in range(end//2, -1, -1): #第一次循环把列表变成堆
siftdown(elems, elems[i], i, end)
for i in range((end-1), 0, -1): #第二次循环
e = elems[i] #把最后一个元素取出,把第一个元素放在最后,就变成了以第i个元素为跟,和前边两个子堆向下筛选构成新堆的问题
elems[i] = elems[0]
siftdown(elems, e, 0, i)