图解堆排序及其Python实现

图解堆排序及其Python实现


什么是堆?

堆(heap,也称优先队列)是一种数据结构,通常可以看做是一棵完全二叉树的广度优先遍历数组对象。

分类:

  • 根节点的值总是不小于子节点值的堆称为最大堆、大顶堆或大根堆。

  • 根节点的值总是不大于子节点值的堆称为最小堆、小顶堆或小根堆。

    在这里插入图片描述
    在这里插入图片描述
    其满足以下性质:

    • 堆总是一棵完全二叉树;
    • 堆中的某个节点的值总是不大于或不小于其父节点的值;
    • 下标为 i 的节点的父节点下标为 (i-1)// 2 [整数除法]
    • 下标为 i 的节点的左孩子下标为 i * 2 + 1
    • 下标为 i 的节点的右孩子下标为 i * 2 + 2

如何构建一个堆?

以最小堆为例。
对于输入的数组,首先将其构建为无序的完全二叉树,然后从末尾节点开始调整(从下往上操作),根据最小堆的性质,比较子节点和其父节点的值大小并进行移动,即如果子节点中有值小于父节点的就选择子节点中值最小的节点与父节点交换位置。注意,被调整的节点如果有子节点,则需要进行递归调整。

  1. 将输入数组构建为完全二叉树:
    在这里插入图片描述

  2. 从末尾开始调整二叉树,对有子节点的节点进行调整时需要进行递归:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    注意,对于含有子节点的节点调整时要进行递归操作。
    在这里插入图片描述
    在这里插入图片描述

构造完成。

插入:当有新的元素插入数组时,将新元素放在数组结尾,然后重新调整排列二叉树。

删除:删除时每次都删除0号元素,即堆顶元素。为了便于重建,实际的操作是:每次将堆顶元素替换为数组中最后一个元素,然后从堆顶开始由上往下进行调整。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

由于一个完全二叉树的高度不会超过 l o g 2 n log{2^n} log2n ,而堆化操作其实就是沿着节点路径进行对比、交换。因此它的时间复杂度与树的高度成正比,所以往堆里面插入一个元素、删除堆顶元素的时间复杂度是 l o g n log{n} logn


什么是堆排序?

堆排序(heap sort)是利用堆这种数据结构设计的一种原地排序算法,时间复杂度为 O ( n l o g n ) O(nlog{n}) O(nlogn)。它可以利用数组的特点快速定位指定索引的元素。

由堆的分类及性质可知:对于大顶堆,根节点是数组中的最大值;对于小顶堆,根节点是数组中的最小值。

根据这个特点,我们每次从排好序的堆中取出堆顶元素并将最后一个元素放到堆顶,然后重排堆,直到元素全部取出,就可以得到这个数组从大到小(大顶堆)或从小到大(小顶堆)的排序。


代码实现:

堆排序可以实现原地操作,不过下面的代码中创建了新的对象来存储排序后的数组。

  1. 手动实现:实现大顶堆即其排序

    import copy
    
    
    class Heap(object):
        """实现大顶堆及堆排序"""
        def __init__(self, arr: list):
            """arr: 用户输入的待排序数组"""
            self.arr = arr
            self.len = len(arr)
            self.sorted_arr = []
            self.create_heap()
        
        def heapify(self, parent_index):
            """维护堆的性质"""
            # 假设当前父节点是子树中最大的值下标
            largest = parent_index
            # 左右孩子节点下标,有可能不存在
            left_child_index = parent_index * 2 + 1
            right_child_index = parent_index * 2 + 2
            # 判断左右子节点是否存在,并找出其中最大的值下标
            if left_child_index < self.len and self.arr[largest] < self.arr[left_child_index]:
                largest = left_child_index
            if right_child_index < self.len and self.arr[largest] < self.arr[right_child_index]:
                largest = right_child_index
            # 需要进行位置调整,并进行递归调整
            if not (largest == parent_index):
                self.arr[parent_index], self.arr[largest] = self.arr[largest], self.arr[parent_index]
                self.heapify(largest)
    
        def create_heap(self):
            """初始化堆"""
            last_parent_index = (self.len - 1) // 2  # 最后一个包含子节点的父节点下标
            for i in range(last_parent_index, -1 , -1):
                self.heapify(i)
    
        
        def pop(self):
            peak = self.arr[0]
            # 交换堆顶与最后一个元素,然后重新建堆
            self.arr[0] = self.arr[-1]
            self.arr.pop(-1)
            self.len -= 1
            self.heapify(0)  # 从上到下维护堆
            return peak
        
        
        def heap_sort(self):
            """堆排序,输出排序后的数组"""
            self.arr2 = copy.deepcopy(self.arr)
            for i in range(len(self.arr)):
                self.sorted_arr.insert(0, self.pop())
            self.arr = self.arr2
        
    h = Heap([18, 34, 26, 25, 30, 8, 28, 13])
    print(h.arr)
    h.heap_sort()
    print(h.sorted_arr)
    
    

    执行结果:

    [34, 30, 28, 25, 18, 8, 26, 13]
    [8, 13, 18, 25, 26, 28, 30, 34]
    
  2. 使用heapq库调用:
    heapq默认的实现是小顶堆,使用其提供的heapq.nsmallest(*n*, *iterable*, *key=None*)方法即可返回iterable中前n个最小的元素。

    import heapq
    
    def heap_sort(arr):
        n = len(arr)
        return heapq.nsmallest(n, arr)
    
    
    arr = [18, 34, 26, 25, 30, 8, 28, 13]
    
    print(arr)
    heapq.heapify(arr)
    print(arr)
    print(heap_sort(arr))
    

    执行结果:

    [18, 34, 26, 25, 30, 8, 28, 13]
    [8, 13, 18, 25, 30, 26, 28, 34]
    [8, 13, 18, 25, 26, 28, 30, 34]
    

参考

图解:最小堆构建、存储、插入、删除过程
快速入门堆排序
heapq:堆队列算法

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值