此博客用于个人学习,来源于算法的书籍和网上的资料,对知识点进行一个整理。
1. 概述:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。
2. 算法:
- 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
- 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
- 新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
3. 代码实现:
/**
* 堆排序
*/
public class HeapSort {
public void sort(Comparable[] a){
int N = a.length;
//构造堆
for (int k = N/2;k >= 1;k --){
sink(a,k,N);
}
//通过循环将最大元素 a[1] 到 a[N] 交换并且修复离了堆
while (N > 1){
exchange(a,1,N--);
sink(a,1,N);
}
}
//下沉操作
private void sink(Comparable[] a, int k, int n) {
while (2*k <= n){
int j = 2*k;
//找出子节点最大的那个
if (j < n && less(a,j,j+1)){
j++;
}
//最大的子节点小于父节点,不需要交换,跳出循环
if (!less(a,k,j)){
break;
}
exchange(a,k,j);
k = j;
}
}
private boolean less(Comparable[] a,int i, int j){
return a[i].compareTo(a[j]) < 0;
}
private void exchange(Comparable[] a,int i, int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
}
4. 特点:
-
优点: 所知的唯一能同时最优的利用空间和时间的方法,在最坏的情况下它也能保证使用 2NlgN 次比较和恒定的额外空间。
-
缺点:无法利用缓存。数组元素很少和相邻的其他元素进行比较,因此缓存命中的次数要远远高于大多数比较都在相邻元素间进行的算法。
5. 算法分析:
- 时间复杂度:在构建堆(初始化堆)的过程中,完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和必要的互换,对于每个非终端结点来说,其实最多进行两次比较和一次互换操作,因此整个构建堆的时间复杂度为: O(n)。大概需进行 n/2 * 2 = n 次比较和 n/2 次交换。在正式排序时,n 个结点的完全二叉树的深度为 ⌊log2n⌋+1,并且有 n 个数据则需要取 n-1 次调整堆的操作,每次调整成大顶堆的时间复杂度为O(log2n)。因此,重建堆的时间复杂度可近似看做: O(nlogn)。
这里说明一下:快速排序是平均复杂 O (nlogn),实际上,快速排序的最坏时间复杂度是O(n^2。),而像归并排序,堆排序,都稳定在O(nlogn)
- 空间复杂度:因为堆排序是就地排序,空间复杂度为常数:O(1)。
- 稳定性:堆的结构是节点i的孩子为 2*i 和 2*i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为 n 的序列,堆排序的过程是从第 n / 2 开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为 n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第 n / 2 个父节点交换把后面一个元素交换过去了,而第 n / 2 - 1 个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。