堆排序


堆排序是从 二叉堆结构 衍生出来的排序算法,复杂度为 $O(NlogN)$。堆排序主要分两步,第一步是在待排序数组上原地创建二叉堆(Heapify),然后进行原地排序(Sort)。

想法

堆排序的两个关键步骤:

1、原地建堆(Heapify):直接把待排序数组原地变成一个二叉堆。

2、排序(Sort):将元素不断地从堆中取出,最终得到有序的结果。

下面这些函数就是从 二叉堆实现优先级队列 中的优先级队列实现里抠出来的,把数组作为函数参数传入,其他的逻辑完全一样:

def min_heap_swim(heap, node):
    # 小顶堆的上浮操作,时间复杂度是树高 O(logN)
    while node > 0 and heap[parent(node)] > heap[node]:
        swap(heap, parent(node), node)
        node = parent(node)


def min_heap_sink(heap, node, size):
    # 小顶堆的下沉操作,时间复杂度是树高 O(logN)
    while left(node) < size or right(node) < size:
        # 比较自己和左右子节点,看看谁最小
        min_index = node
        if left(node) < size and heap[left(node)] < heap[min_index]:
            min_index = left(node)
        if right(node) < size and heap[right(node)] < heap[min_index]:
            min_index = right(node)
        if min_index == node:
            break
        # 如果左右子节点中有比自己小的,就交换
        swap(heap, node, min_index)
        node = min_index


def max_heap_swim(heap, node):
    # 大顶堆的上浮操作
    while node > 0 and heap[parent(node)] < heap[node]:
        swap(heap, parent(node), node)
        node = parent(node)


def max_heap_sink(heap, node, size):
    # 大顶堆的下沉操作
    while left(node) < size or right(node) < size:
        # 小顶堆和大顶堆的唯一区别就在这里,比较逻辑相反
        # 比较自己和左右子节点,看看谁最大
        max_index = node
        if left(node) < size and heap[left(node)] > heap[max_index]:
            max_index = left(node)
        if right(node) < size and heap[right(node)] > heap[max_index]:
            max_index = right(node)
        if max_index == node:
            break
        swap(heap, node, max_index)
        node = max_index


def parent(node):
    # 父节点的索引
    return (node - 1) // 2


def left(node):
    # 左子节点的索引
    return node * 2 + 1


def right(node):
    # 右子节点的索引
    return node * 2 + 2


def swap(heap, i, j):
    # 交换数组中两个元素的位置
    heap[i], heap[j] = heap[j], heap[i]

简单直接的堆排序实现


def sort(nums):
    # 第一步,原地建堆,注意这里创建的是大顶堆
    # 只要从左往右对每个元素调用 swim 方法,就可以原地建堆
    for i in range(len(nums)):
        max_heap_swim(nums, i)

    # 第二步,排序
    # 现在整个数组已经是一个大顶了,直接模拟删除堆顶元素的过程即可
    heap_size = len(nums)
    while heap_size > 0:
        # 从堆顶删除元素,放到堆的后面
        swap(nums, 0, heap_size - 1)
        heap_size -= 1
        # 恢复堆的性质
        max_heap_sink(nums, 0, heap_size)
        # 现在 nums[0..heap_size) 是一个大顶堆,nums[heap_size..) 是有序元素

这里面一个关键点是要用大顶堆来完成 nums 从小到大的排序,因为从堆顶删除的元素是从后往前填到 nums 数组中的,最终 nums 中的元素是从小到大排序的。

如果你用小顶堆的话,最终 nums 中的元素是从大到小排序的,还需要再翻转一下数组,没有大顶堆的效率高。

堆排序的复杂度分析

我们来分析一下上述代码的时间复杂度,假设 nums 的元素个数为 N

  • 第一步建堆的过程中,swim 方法的时间复杂度是
    $O(logN)$,算法对每个元素调用一次 swim 方法,所以总时间复杂度是 $O(NlogN)$。
  • 第二步排序的过程中,每次 sink 方法的时间复杂度是 $O(logN)$,算法对每个元素调用一次 sink 方法,所以总时间复杂度是 $O(NlogN)$。

综上,整个堆排序的时间复杂度是 $2NlogN$,用 Big O 表示就是 $O(NlogN)$。与 快速排序、归并排序 是一个级别的排序算法。

堆排序的稳定性

堆排序是一种不稳定的排序算法,原因在于它在排序过程中交换元素时,可能会改变相同元素的相对顺序。


文章作者: Mealsee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Mealsee !
  目录