社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

【常见算法Python描述】堆排序简介及实现

TakingCoding4Granted • 4 年前 • 413 次点击  

【数据结构Python描述】二叉堆(heap)简介和Python手工实现及使用二叉堆实现优先级队列 中,我们学习了什么是 二叉堆 ,以及具体什么是最小堆和最大堆。

通过 【数据结构Python描述】自底向上构建二叉堆实现及其 O ( n ) O(n) O ( n ) 时间复杂度分析 ,我们又学习了如何在给定一个序列(如:列表)的情况下,高效地将其转换为一个二叉堆。

本文将基于上述两篇文章的内容,来介绍如何通过最大堆来进行排序,即 堆排序

一、堆排序简介

1. 理论

如果给定输入列表 A[1..n] ,其中 n=len(A) ,则 堆排序 算法需要先构建一个最大堆,即最大元素总是位于根节点处,且所有子节点不大于其父节点。由于最大元素位于根节点 A[1] 处,因此我们可以通过如下步骤对A进行排序:

  • 首先,将根节点和 A[n] 进行交换,则最大值的位置确定;
  • 其次,由于新的根节点可能使得当前完全二叉树违背 堆序性质 ,因此可以采用 元素交换的方式使其重新满足堆序性质 ,需要注意的是,此时元素交换时无需再考虑元素 A[n]
  • 然后,将新的根节点和 A[n-1] 进行交换,则次大值的位置确定;
  • 再次,由于新的根节点可能使得当前完全二叉树违背 堆序性质 ,再次采用元素交换的方式使其重新满足堆序性质,需要注意的是,此时元素交换时无需再考虑元素 A[n] A[n-1]
  • 最后,重复上述过程,直到 A[1..n] 按照非递减顺序排列。

2. 图解

2.1 建最大堆

实际上,给定一个列表,建一个最大堆的流程和 建一个最小堆 的流程基本一致,具体地,针对给定的10个元素列表,建堆步骤如下:

  • 首先,同 建最小堆 一样,如图 ( a ) (a) ( a ) 所示,自底向上,先确保以位置5为根节点的完全二叉树是一个二叉堆,此时满足最大堆的堆序性质,不涉及元素交换;
  • 其次,确保以位置4为根节点的完全二叉树是一个二叉堆,此时违背了最大堆的堆序性质,需对位置4和位置8的元素进行交换,如结果如图 ( c ) (c) ( c ) 所示;
  • 紧接着,依次确保以位置3,2,1处为根节点的完全二叉树是一个二叉堆,最终建堆结果如图 ( f ) (f) ( f ) 所示。

在这里插入图片描述

2.2 用最大堆排序

下面是利用上述创建的最大堆进行排序的过程,具体地:

  • 首先,将根节点元素16和最底层最右侧元素1交换,然后通过元素交换使得不包括16在内的其他节点均满足堆序性质,结果如图 ( b ) (b) ( b ) 所示;
  • 然后,将根节点元素14和最底层最右侧元素1交换,然后通过元素交换使得不包括16和14在内的其他节点均满足堆序性质,结果如图 ( c ) (c) ( c ) 所示;
  • 重复上述过程,直到所有元素排好序,如图 ( j ) (j) ( j ) 所示。

在这里插入图片描述

二、堆排序实现

由于堆一定是一个二叉树,而二叉树的实现可以是基于链式结构,也可以基于数组结构,为降低实现难度,且避免使用链式结构导致额外的内存开销(如一个节点保存其父、子节点引用所需的变量),这里使用 基于数组结构来描述二叉树

def __down_heap(arr, num, j):
    """
    通过元素交换,确保列表arr[0:num]中,元素arr[j]及其子孙节点满足堆序性质
    :param arr: 列表形式给出的待排序列
    :param num: 列表索引上限
    :param j: 列表元素的索引
    :return: None
    """
    left = 2 * j + 1  # 元素arr[j]左子节点的索引
    right = 2 * j + 2  # 元素arr[j]右子节点的索引
    if left < num:  # 判断arr[j]是否有左子节点
        large_child = left
        if right < num:  # 判断arr[j]是否有右子节点
            if arr[right] > arr[left]:
                large_child = right
        if arr[large_child] > arr[j]:  # 确保最终建成最大堆
            arr[large_child], arr[j] = arr[j], arr[large_child]  # 交换两个节点处的值使满足堆序性质
            __down_heap(arr, num, large_child)


def heapify(arr):
    """将列表形式给出的元素视为完全二叉树,对其进行建堆"""
    for j in range((len(arr) - 1) // 2, -1, -1):  # 建堆
        __down_heap(arr, len(arr), j)


def heap_sort(arr):
    """堆排序"""
    heapify(arr)  # 构建最大堆
    # 依次将最大值,次大值...最小值从右往左排好
    for i in range(len(arr) - 1, 0, -1):
        arr[i], arr[0] = arr[0], arr[i]  # 每一次迭代,将当前堆中最大元素排好序
        __down_heap(arr, i, 0)  # 每迭代一次后元素arr[i]已排好序,仅需确保前i个元素满足堆序性质


if __name__ == '__main__':
    arr = [4, 1, 3, 2, 16, 9, 10, 14, 8, 7]
    heap_sort(arr)
    print(arr)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

三、堆排序复杂度

1. 时间复杂度

堆排序的时间复杂度分为两个部分:

  • 一部分是调用 heapify 函数的建最大堆所需的时间开销,由 之前的理论分析 可知为 O ( n ) O(n) O ( n )
  • 另一部分是循环调用 __down_heap 方法确保未排序的完全二叉树为最大堆的时间开销,可按照如下计算:

l o g ( n − 1 ) + l o g ( n − 2 ) + ⋅ ⋅ ⋅ + l o g ( 1 ) = l o g ( ( n − 1 ) ! ) log(n-1)+log(n-2)+\cdot\cdot\cdot+log(1)=log((n-1)!) l o g ( n 1 ) + l o g ( n 2 ) + + l o g ( 1 ) = l o g ( ( n 1 ) ! )

而:

l o g ( ( n − 1 ) ! ) < l o g ( n ! ) < n l o g ( n ) log((n-1)!)<log(n!)<nlog(n) l o g ( ( n 1 ) ! ) < l o g ( n ! ) < n l o g ( n )

最后忽略低阶项 O ( n ) O(n) O ( n ) ,故最坏时间复杂度为 n l o g ( n ) nlog(n) n l o g ( n )

2. 空间复杂度

由上述堆排序的实现代码可以看出,在堆排序过程中,除了输入的待排序列和排序过程中使用的辅助变量外,没有额外的内存开销,因此这是典型的原地排序,故其最坏空间复杂度为 Θ ( n ) \Theta{(n)} Θ ( n )

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/100786
 
413 次点击