树形结构和堆

树形结构和堆

可以改善优先队列的操作性能

一.线性和树形结构

首先分析效率低的原因:

  • 按序插入操作低效,其根源是需要沿着表顺序检索插入位置,对于顺序表,需要移动O(n)个元素,对于链接表,需要顺着链接爬行O(n)步.

  • 如果不改变数据的线性顺序存储方式,就不可能突破O(n)的复杂度限制, 要做出操作效率更高的优先队列,必须考虑其他数据结构组织方式

  • 利用树形结构的祖先/子孙序,有可能得到更好的操作效率

一般而言,确定最优先元素并不需要与所有其他元素比较,以体育比赛中的淘汰赛为例,假设有n名选手参加,首先需要进行N-1场比赛确定冠军,每个选手只需进行约log2n场比赛,决出冠军后,要确定真正的第二名,只需亚军与所有输给冠军的人比赛,只需要沿着冠军胜利的路线比赛,不超过log2n次

二. 堆及其性质

采用树形结构实现优先队列的一种有效技术称为堆,从结构上看,堆就是结点里存储数据的完全二叉树,但堆中数据的存储要满足一种特殊的堆序:任一个结点里所存的数据(按所考虑的序)先于或等于其子结点(如果存在)里的数据

  • 在一个堆中从树根到任何一个叶结点的路径上,各结点里所存的数据按规定的优先关系(非严格)递减
  • 堆中最优先的元素必定位于二叉树的根结点里(堆顶), O(1)时间就能得到
  • 位于树中不同路径上的元素,这里不关心其顺序关系

如果所要求的序是最小元素优先,构造出来的堆就是小顶堆,反之就是大顶堆.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5bcZhPc0-1612257238795)(C:\Users\93623\AppData\Roaming\Typora\typora-user-images\image-20210114152155871.png)]

上图是一个堆的形状(也就是完全二叉树的形状),以及堆中一条路径的情况,除最下一层右边可能有所欠缺,堆里各层结点全满.图中从根到叶的路径上越小的圆圈

一棵完全二叉树可以自然而且信息完全地存入一个连续的线性结构(例如连续表),一个堆也可以自然地存入一个连续表,通过下标就方便地找到树中任一结点的父结点/子结点

堆和二叉树有以下性质:

Q1:在一个堆的最后加上一个元素(在相应连续表的最后加一个元素),整个结构还是可以看作一棵完全二叉树,但它未必是堆(最后元素未必满足堆序)

Q2:一个堆去掉堆顶(表中位置0的元素), 其余元素形成两个"子堆", 完全二叉树的子结点/父结点下标计算规则仍然适用,堆序在路径上仍然成立

Q3:给由Q2得到的表(两个子堆)加入一个根元素(存入位置0),得到的结点序列又可看作完全二叉树,但它未必是堆(根结点未必满足堆序)

Q4: 去掉一个堆中最后的元素(最下层的最右结点,也就是对应的连续表里的最后一个元素), 剩下的元素仍构成一个堆

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ddnvulHP-1612257238798)(C:\Users\93623\AppData\Roaming\Typora\typora-user-images\image-20210115140627664.png)]

堆实现优先队列

  • 用堆作为优先队列,可以直接得到堆中的最优先元素,O(1)
  • 插入元素的操作(向上筛选):想堆(优先队列)中加入一个新元素, 必须能得到一个包含了所有原有元素和刚加入的新元素的堆, O(logn)
  • 从堆中弹出最小元素操作(向下筛选),从堆中弹出最小元素后,必须能把剩下的元素重新做成堆,O(logn)

三. 优先队列的堆实现

(一) 插入元素和向上筛选

根据Q1, 在一个堆的最后加入一个元素,得到的结果还是完全二叉树,但未必是堆,为了把这样的完全二叉树恢复为堆,只需做一次向上筛选

向上筛选的方法:

不断用新加入的元素(设成e)与其父结点的数据进行比较,如果e较小就交换两个元素的位置,通过这样的比较和交换,元素e不断上移,这一操作一直做到e的父结点的数据小于等于e时,或者e已经到达根结点时停止,这时经过e的所有路径上的元素满足所需顺序,其余路径仍保持有序,因此这棵完全二叉树满足堆序

  • 把新加入的元素放在(连续表里)已有元素之后,执行一次向上筛选操作
  • 向上筛选操作中比较和交换的次数不会超过二叉树中最长路径的长度,因此插入元素操作可以在O(logn)时间完成

(二)弹出元素和向下筛选

弹出堆顶元素,从原堆的最后取下一个元素,放在堆顶就得到了一棵完全二叉树,现在除了堆顶元素可能不满足堆序外,其余元素都满足堆序, 现在需要设法把结构重新恢复为一个堆

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0xl9bU8Z-1612257238801)(C:\Users\93623\AppData\Roaming\Typora\typora-user-images\image-20210115143047139.png)]

  • 用e与A,B两个"子堆"的顶元素(根)比较,最小者作为整个堆的顶
    • 若e不是最小,最小的比为A或B的根,设A的根最小,将其移到堆顶,相当于删掉了A的顶元素
    • 将e放入去掉堆顶的A,这是规模更小的同一问题
    • B的根最小的情况同样处理
  • 如果某次比较重e最小,以它为顶的局部树已经成为堆,整个结构也成为堆
  • 或者e已经落到底,这时它自身就是一个堆,整个结构也成为堆

总结:

  1. 弹出堆顶 O(1)
  2. 从堆最后取一个元素作为完全二叉树的根 O(1)
  3. 执行一次向下筛选 O (logn), 操作次数不超过树中路径的长度

下面用python实现基于堆的优先队列类,用list存储元素,应该在表尾端加入元素,以首端作为堆顶

class PrioQueue:
    """
    implementing priority queues using heaps
    """
    def __init__(self, elist=[]):
        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.append(None) # add a dummy element
        self.siftup(e,len(self._elems)-1)
    
    def shiftup(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.shiftdown(e, 0, len(elems))
        return e0
    
    def shiftdown(self, e, begin, end):
        elems, i, j = self._elems, 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
        
    def buildheap(self):
        end = len(self._elems)
        for i in range(end//2, -1, -1):
            self.shiftdowm(sele._elems[i], i, end)

list(elist)从elist出发做出一个表拷贝的意义:

  • 做拷贝使内部的表脱离原来的表,排除共享
  • 对默认情况也建立一个新的空表,避免了以可变对象作为默认值的python编程陷阱

在shiftup的实现里,并没有先存入元素后再考虑交换,而是"拿着它"去查找正确插入的位置,循环条件保证跳出的元素都是优先度较低的元素,在检查过程中把它们逐个下移

总结:

  1. 基于堆的概念实现优先队列,创建操作的时间复杂度是O(n), 这件事只需要做一次,
  2. 插入和弹出操作的复杂度是O(logn).插入操作的第一步是在表的最后加入一个元素,可能导致list对象替换元素存储区,因此可能出现O(n)的最坏情况
  3. 所有操作中都只用了一个简单变量,没用其他结构,所以空间复杂度都是O(1)

区,因此可能出现O(n)的最坏情况
3. 所有操作中都只用了一个简单变量,没用其他结构,所以空间复杂度都是O(1)

猜你喜欢

转载自blog.csdn.net/weixin_46129834/article/details/113567245