堆排序(Python)

1、堆

  • 堆是一个完全二叉树。
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。

2、堆的操作

2.1、往堆中插入一个元素

堆化(heapify

下图是从下往上的堆化,在结尾插入一个元素22

2.2、删除堆顶元素

删除堆顶的元素之后,把最后一个节点放到堆顶,然后利用同样的父子节点对比方法。

 

3、堆排序

  • 借助于堆这种数据结构实现的排序算法,就叫作堆排序。时间复杂度非常稳定,是 O(nlogn),并且还是原地排序算法(所谓“原地”,就是不借助另一个数组,就在原数组上操作。)。
  • 堆排序的过程大致分解成两个大的步骤:建堆排序

3.1、建堆

下图中,从后往前处理数组,并且每个数据都是从上往下堆化。

下标从 n/2​ 开始到 1 的数据进行堆化,下标是 n/2​+1 到 n 的节点是叶子节点,我们不需要堆化。实际上,对于完全二叉树来说,下标从 n/2+1 到 n 的节点都是叶子节点。

建堆的时间复杂度就是 O(n)

3.2、排序

建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。

数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。

这个过程有点类似上面讲的“删除堆顶元素”的操作,当堆顶元素移除之后,我们把下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。

堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。

整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。

堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn)

所以,堆排序整体的时间复杂度是 O(nlogn)

'''建堆'''
def buildMaxHeap(a):
    import math
    # 对下标从n/2开始到0的数据进行堆化,因为之后的节点都是叶子节点
    for i in range(math.floor(len(a)/2), -1, -1):  # 下舍整数:8.7为8
        heapify(a, i, len(a))

'''堆化'''
def heapify(a, i, length):  # length作为参数,有助于在堆排序时改变堆的大小
    # 堆的下标从0开始
    left = 2*i+1
    right = 2*i+2
    largest = i
    # 看此非叶子节点的左右孩子是否比其大,取最大的交换
    if left < length and a[left] > a[largest]:
        largest = left
    if right < length and a[right] > a[largest]:
        largest = right

    if largest != i:
        swap(a, i, largest)
        heapify(a, largest, length)  # 递归:看孩子的孩子节点

def swap(a, i, j):
    a[i], a[j] = a[j], a[i]
'''堆排序'''
def heap_sort(a):

    length = len(a)  # 堆的下标范围[0, len(a)-1]
    buildMaxHeap(a)
    # 从len(a)-1到1
    for i in range(len(a)-1, 0, -1):
        swap(a, 0, i)  # 将堆顶的元素(最大数),放在数组的末尾
        length -= 1  # 堆中元素-1
        heapify(a, 0, length)  # 对第0位元素进行堆化
    return a
if __name__ == "__main__":
    a1 = [3, 5, 6, 7, 8]
    a2 = [2, 2, 2, 2]
    a3 = [4, 3, 2, 1]
    a4 = [5, -1, 9, 3, 7, 8, 3, -2, 9]
    a5 = [1]
    heap_sort(a1)
    print(a1)
    heap_sort(a2)
    print(a2)
    heap_sort(a3)
    print(a3)
    heap_sort(a4)
    print(a4)
    heap_sort(a5)
    print(a5)

 

4、小结

堆是一种完全二叉树,它最大特性:每个节点的值都大于等于(或小于等于)其子树节点的值。

堆中比较重要的两个操作是插入一个数据和删除堆顶元素。这两个操作都要用到堆化。

  • 插入一个数据的时候,我们把新插入的数据放到数组的最后,然后从下往上堆化(Shift Up)
  • 删除堆顶数据的时候,我们把数组中的最后一个元素放到堆顶,然后从上往下堆化(Shift Down)
  • 这两个操作时间复杂度都是 O(logn)。

堆排序

  • 堆排序包含两个过程,建堆和排序。我们将下标从 ​n/2 到 1 的节点,依次进行从上到下的堆化操作,然后就可以将数组中的数据组织成堆这种数据结构。
  • 接下来,我们迭代地将堆顶的元素放到堆的末尾,并将堆的大小减一,然后再堆化,重复这个过程,直到堆中只剩下一个元素,整个数组中的数据就都有序排列了。

5、实际开发中,堆排序不如快速排序性能好的原因

① 堆排序数据访问的方式没有快速排序友好

  • 对于快速排序来说,数据是顺序访问的。而对于堆排序来说,数据是跳着访问的。
  • 比如,堆排序中,最重要的一个操作就是数据的堆化。比如对堆顶节点进行堆化,会依次访问数组下标是 1,2,4,8 的元素(举例),而不是像快速排序那样,局部顺序访问,所以,这样对 CPU 缓存是不友好的。

② 对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序

  • 对于基于比较的排序算法来说,整个排序过程就是由两个基本的操作组成的,比较和交换(或移动)。快速排序数据交换的次数不会比逆序度多。
  • 但是堆排序的第一步是建堆,建堆的过程会打乱数据原有的相对先后顺序,导致原数据的有序度降低。比如,对于一组已经有序的数据来说,经过建堆之后,数据反而变得更无序了。

参考文献

https://time.geekbang.org/column/article/69913

https://zhuanlan.zhihu.com/p/80098042

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值