堆排序是一种基于二叉堆数据结构的排序算法。它的主要思想是将待排序的序列构建成一个大顶堆(或小顶堆),然后 依次取出堆顶元素,将其与堆中最后一个元素交换,并调整堆,使得剩余元素仍满足堆的性质。重复这个过程,直到堆中只剩下一个待调整元素,即可得到一个有序序列。
上面说,大顶堆依次将堆顶(当前最大)元素与堆中最后一个元素交换。因此,基于大顶堆的堆排序后的结果是一个升序序列,基于小顶堆的堆排序后的结果是一个降序序列。
上面调整堆的堆化过程,是将「剩余的元素」重新构造成一个堆(通过交换的方式放在堆尾的元素不再参与堆化调整)。
按文章开头的描述,堆排序的过程可以分为两个主要步骤:构建堆和调整堆。下面以大顶堆的堆排序来进行描述。
构建堆:
首先,将待排序的序列构建成一个初始堆。可以 从最后一个非叶子节点开始,依次向前遍历 ,对每个节点进行调整,使得该节点的值大于其子节点的值。这个过程称为堆化,可以使用 下沉操作 来实现。
调整堆:
将堆顶元素与堆中最后一个元素交换位置,并将堆的大小减一 。然后对堆顶元素进行 下沉操作,使得剩余元素仍满足堆的性质。重复这个过程,直到堆中只剩下一个元素,即可得到一个有序序列。
堆排序在实际应用中具有较高的效率和稳定性,尤其适用于大规模数据的排序。
堆化的两个主要操作是下沉(弹出堆顶元素时使用)和上浮(往堆中添加新元素时使用)操作,可以参考 数据结构之堆基础与堆结构(数组实现)。
堆排序的时间复杂度为 O(nlogn)
,其中 n
是待排序序列的长度。由于堆排序只需要常数的辅助空间,因此它是一种原地排序算法,空间复杂度为 O(1)
。但 堆排序是不稳定的排序算法,即相同元素的顺序可能会发生改变。
对于给定的长度为 的无序序列进行堆排序的第一个过程是:构建堆——将无序序列构建成一个二叉堆数据结构。
上面说「从最后一个非叶子节点开始,依次向前遍历」,最后一个非叶子节点的数组索引(从 开始)为 。
为了完成从无序序列构建堆的这一过程。我们首先给出堆化中的下沉操作。假设,我们现在已经有一个满足堆属性的数组,当我们修改了数组某一个索引位置的值时,就需要重新对这个位置,以及之前的所有位置都进行堆化调整。
例如,对于下面的大顶堆:
1 | 60 |
如果把索引 1 位置的值 30 修改为 70,那么索引 1 和 索引 0 都需要进行堆化调整。
1 | // 调整索引 1 位置 |
如果把索引 1 位置的值 30 修改为 15,那么索引 1 和 索引 0 也都需要进行堆化调整。
1 | // 调整索引 1 位置 |
对于给定的长度为 n
的数组(满足堆属性),当修改了索引 i
位置的值时,堆化调整以索引 i
为根节点的子树,使这棵以索引 i
为根节点的树重新满足堆属性(但不再能保证整个数组满足堆属性,若需要整棵树都满足堆属性,还需要按顺序对以索引 i-1
到以索引 0
为根的子树进行堆化调整)。
1 | // 下沉操作,使以索引 i 为根节点的子树满足堆属性 |
现在开始从最后一个非叶子节点,依次向前遍历进行下沉堆化。每遍历一个节点,都会使得以这个节点为根节点的树满足堆属性,那么当遍历完索引为 0
的位置后,整个数组将满足堆属性,构成一棵二叉堆。
1 | // 构建堆(从最后一个非叶子节点开始,依次向前遍历进行下沉堆化) |
将堆顶元素与堆中最后一个元素交换位置,并将堆的大小减一 。然后对堆顶元素进行 下沉操作,使得剩余元素仍满足堆的性质。重复这个过程,直到堆中只剩下一个元素,即可得到一个有序序列。
1 | // 依次取出堆顶元素,并调整这个更小的堆 |
1 | void heapify(int arr[], int n, int i); |
1 |
|
1 | 原始数组:12 11 13 5 6 7 |