堆
概念
堆是具有以下性质的完全二叉树:每个节点的值都要大于或等于其左右孩子的值,称为大顶堆;或者每个节点的值都小于其左右孩子节点的值,称为小顶堆。
性质(完全二叉树的性质)
如果对一棵有n个节点的完全二叉树(其深度为logn + 1)的节点按层次编号(从第1层到第logn + 1 层,每层从左到右),对任一节点i(1 ≤ i ≤ n)有:
- 如果i=1,则节点i是二叉树的根,无双亲;如果i > 1,则其双亲是节点 i / 2 。
- 如果2i > n,则节点 i 无左孩子(i为叶子节点);否则其左孩子就是节点2i 。
- 如果 2i+1 > n,则节点 i 无右孩子;否则其右孩子就是 2i+1 。
如果将上图所示的大顶堆和小顶堆用层次遍历存入数组,则如下图所示:
基本思想
堆排序就是利用堆进行排序的方法(利用大顶堆完成从小到大的排序,利用小顶堆完成从大到小的排序)。其基本思想是,将待排序的序列构造成一个大顶堆,此时整个序列的最大值就是堆顶的根节点,将它一走(其实就是将其与数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,就会得到n个元素中的次小值。如此反复进行,就可以得到一个有序序列。(堆排序相当于简单排序算法的升级,同属于 选择排序类
)
堆调整函数-heapAdjust
// 已知array[start~end]中记录的关键字,除array[start]外均满足堆的定义
// 本函数用来调整array[start]关键字,使array[start~end]成为一个新的大顶堆
void heapAdjust(int[] array, int start, int end) {
int tmp = array[start];
// i*2+1是i的左子节点
for (int i = start * 2 + 1; i < end; i = i * 2 + 1) {
// 选择关键字较大的孩子节点
if (i < end - 1 && array[i] < array[i + 1]) {
i++;
}
// 若该元素大于子节点中较大的元素,那么他就应该在此位置
if (tmp > array[i]) {
break;
}
// 父节点的位置应该放置较大的元素
array[start] = array[i];
// 记录原始start的元素应该存放的位置
start = i;
}
array[start] = tmp;
}
堆排序算法
堆排序算法的实现包括两个问题:1、如何由一个无序序列构造一个堆;2、如何在输出堆顶元素后调整剩余的元素称为一个新的堆
void heapSort(int[] array) {
int length = array.length;
// 构造一个大顶堆
for (int i = length/2-1; i >= 0; i--) {
heapAdjust(array, i, length);
}
for (int i = length - 1; i > 0; i--) {
swap(array, 0, i);
// 重新调整为大顶堆
heapAdjust(array, 0, i - 1);
}
}
此处构造大顶堆时从length/2-1开始是因为后续的节点都是叶子节点,这里所谓的将待排序的序列构建成大顶堆其实就是从下往上,从右到左将每个非终端节点当作根节点,将其和其子树调整成大顶堆。
堆排序算法的时间复杂度分析
堆排序的运行时间主要消耗在初始构建堆和在重建堆时的反复筛选上。
在构建堆的过程中因为是完全二叉树从最下层最右边的非终端节点开始构建,将它与其孩子进行比较和若有必要的交换,对于每个非终端节点来说,其实最多进行两次比较和交换操作,因此整个构建过程的时间复杂度为O(n)
在正式排序时,第i次取堆顶记录重建堆时需要O(log i)的时间(完全二叉树的某个节点到根节点的距离为log i + 1),并且需要取n-1次堆顶记录,因此重建堆的时间复杂度为O(nlogn)
总体来说,堆排序的时间复杂度为O(nlogn)。由于堆排序对原始记录的排序状态并不敏感,故无论最好最坏和平均状态均为O(nlogn)。
由于初始构建堆所需的比较次数较多,所以它并不适合待排序序列的个数较少的情况