二叉堆--堆排序,优先队列

介绍

二叉堆是一种特殊的数据结构,类似于一棵完全二叉树,并且非叶子结点的值都大于或小于左右孩子结点所对应的值。二叉堆可以分为最大堆和最小堆,两者的区别在于非叶子结点的值是大于还是小于孩子结点的值,若大于,则为最大堆,若小于,则为最小堆。如图(a)就不是一个二叉堆,因为第2个结点的值为6,其孩子结点的值都大于它,而其它非叶子结点的值都大于孩子结点。图(b)为最大堆,图©为最小堆。

图(a)图(b)图©

使用二叉堆数据结构,可以实现堆排序,时间复杂度为O(nlgn),并且可以实现就地排序,不需要开辟太多额外的空间,可以算是非常优秀的排序算法。除了能够应用于排序外,还可以构建优先队列,利用最大堆构建,能够优先输出等级最高的结点,例如共享计算机系统的作业调度,优先队列记录将要执行的各个作业以及它们之间的相对优先级,当一个作业被完成或者中断时,调度器选择下一个优先级最高的作业去执行。

应用一:堆排序

在实现堆(最大堆)排序之前,需要完成两个关键过程:

  1. 维持最大堆:维持最大堆的性质
  2. 创建最大堆:从一个无序的数组构建最大堆

维持最大堆

给定一个数组和结点索引,在该结点的左右子树为最大堆的前提下,要保证以该结点为根节点的树依然是个最大堆。可以简单的理解为随意更改某个结点的值后,破坏了原来最大堆的性质,因为可能修改的值小于左右孩子结点的值,所以需要通过一个方法维持最大堆的性质,在调用该方法后,以该结点为根节点的树依然要是个最大堆。例如图(a),第二个结点的值为6,小于左右孩子结点的值10和9,当调用该方法后,6和10两值交换,保证了以第二个结点为根节点的树为最大堆。时间复杂度为O(lgn)

def Max_Heapify(arr:List[int],i:int,heap_size:int):
    # 双亲结点的索引
    def Parent(i:int):
        return int(i>>1)
    # 左孩子的索引
    def Left(i:int):
        return int(i<<1)
    # 右孩子的索引
    def Right(i:int):
        return int(i<<1)+1

    while True:
        l,r = Left(i),Right(i)

        if l<=heap_size and arr[l-1] > arr[i-1]:
            larger = l
        else:
            larger = i
        if r<=heap_size and arr[r-1] > arr[larger-1]:
            larger = r

        if larger != i:
            # 交换根结点与最大孩子结点的值
            arr[i-1],arr[larger-1] = arr[larger-1],arr[i-1]
            i = larger
        else:
            break

已知一个结点索引,需要获得该结点的双亲结点、左右孩子结点的索引,由于二叉堆为一个完全二叉树,根据其性质,很容易获得。这里并没有采用递归调用的方式,而是采用一个while循环实现值的交换,据算法导论一书所说,递归调用可能会使某些编译器产生低效的代码。

创建最大堆

在创建最大堆之前,需要知道一个定理:当用数组存储一个最大堆时,叶子结点所对应的索引为[n/2]+1,[n/2]+2,……,n。由于叶子结点没有左右孩子结点,所以以叶子结点为根节点的树为一个最大堆。通过该定理,我们可以自底向上的创建一个最大堆,以[n/2],[n/2]-1,……,2的顺序来维持最大堆,保证了左右子树为最大堆的前提,最终完成最大堆的创建。时间复杂度为O(n)

def BuildMaxHeap(arr:List[int],heap_size:int):
    for i in range(1,int(heap_size//2))[::-1]:
        Max_Heapify(arr,i,heap_size)

堆排序

前面两个过程实现后,堆排序就很容易实现了。首先从一个无序数组创建一个最大堆,由于最大堆的根节点的值是堆中所有结点值中最大的,所以我们每次将根节点的值与最后一个结点的值交换,并删除最后一个结点(可以通过减少heap_size的值来实现对结点的删除),由于根节点的值被修改,为了保持最大堆,调用维持最大堆方法。时间复杂度为O(nlgn)

def HeapSort(arr:List[int]):
    heap_size = len(arr)
    # 同一数组初始顺序不同,构建的最大堆也不一样,但不影响最终的排序结果
    BuildMaxHeap(arr,heap_size)
    for i in range(1,len(arr))[::-1]:
        # exchange arr[i] with arr[1]
        arr[0],arr[i] = arr[i],arr[0]
        heap_size -= 1
        Max_Heapify(arr,1,heap_size)

应用二:优先队列

优先队列具有四个基本方法,以最大堆为例,

  1. Insert(value):在队列中插入一个值,时间复杂度为O(lgn)
  2. Maximum():返回队列中最大的值,时间复杂度为O(1)
  3. Extract_Max():去掉并返回队列中最大的值,时间复杂度为O(lgn)
  4. Increase_Key(i,key):将堆中其中一个元素的关键值增加到key,其中key要大于原键值,时间复杂度为O(lgn)

第二个方法很容易实现,直接返回根节点的值。
第三个方法不仅要返回值,还要将其在队列中删除。这里先将堆中最后的结点值与根节点值交换,然后删除最后的结点,最后调用维持最大堆的方法。与堆排序方法中循环体内部的过程类似。
第四个方法由于增大了某结点的值,不影响以该结点为根节点的树为最大堆的性质,但是会影响双亲结点。所以需要不断地比较修改的结点与双亲结点的值,若不满足最大堆条件,则交换两个结点的值
第一个方法,首先增加一个值为-inf的叶子结点,然后对该叶子结点调用第四个方法即可。

class PriorityQueue(object):
    """优先队列具有四个基本方法:
        Insert:把元素插入到堆中
        Maximum:返回堆中的最大键值的元素
        Extract_Max:删除堆中的最大键值的元素并返回该元素
        Increase_Key:将堆中其中一个元素的关键值增加到k,其中k要大于原键值
    """

    def __init__(self,arr:List[int]):
        self.heap_size = len(arr)
        self.arr = arr
        BuildMaxHeap(self.arr,self.heap_size)

    def Is_empty(self):
        return self.heap_size == 0

    def Maximum(self):
        if self.Is_empty():
            raise IndexError("the queue is empty! Please insert a new element")
        return self.arr[0]

    def Extract_Max(self):
        if self.Is_empty():
            raise IndexError("the queue is empty! Please insert a new element")
        # 存储堆中最大键值
        max = self.arr[0]
        # 用堆中最后一个叶子结点的关键值替换根节点的关键值
        self.arr[0] = self.arr[self.heap_size-1]
        # 堆元素数量减一
        del self.arr[self.heap_size-1]
        self.heap_size -= 1
        # 维持最大堆
        MAX_Heapify2(self.arr,1,self.heap_size)

        return max

    def Increase_Key(self,i:int,key:int):
        """
        :param i: 要操作元素的索引
        :param key: 关键值要增加到值
        """
        if self.Is_empty():
            raise IndexError("the queue is empty! Please insert a new element")
        if i > self.heap_size:
            raise IndexError("the index over the max number of element")
        if key<=self.arr[i-1]:
            raise ValueError("key must greater than value")

        self.arr[i-1] = key
        # 为了维持最大堆,需要不断地比较修改的结点与双亲结点的关键值,若不满足最大堆条件,则交换两个结点的关键值
        while i>1 and self.arr[int(i>>1)-1] < self.arr[i-1]:
            self.arr[i-1],self.arr[int(i>>1)-1] = self.arr[int(i>>1)-1],self.arr[i-1]
            i = int(i>>1)

    def Insert(self,value:int):
        self.arr.append(float("-inf"))
        self.heap_size += 1
        self.Increase_Key(self.heap_size,value)

if __name__ == '__main__':
	arr = [1,5,7,12,4,8,9,0,27,17,3,16,13,10]
    P_queue = PriorityQueue(arr)
    print(P_queue.Maximum())
    print(P_queue.Extract_Max())
    P_queue.Increase_Key(6,30)
    P_queue.Insert(14)
    print(P_queue.Maximum())

部分内容引用于算法导论一书

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值