目录
一、简介
堆排序(heapsort)。与归并排序一样,但不同于插入排序的是,堆排序的时间复杂度是。而与插入排序相同,但不同于归并排序的是,堆排序同样具有空间原址性:任何时候都只需要常数个额外的元素空间存储临时数据。因此堆排序是集合了插入排序和归并排序两种排序算法优点的一种排序算法。
二、算法原理
1、堆
(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树。树上的每一个元素对应数组中的一个元素。除了最底层外,该树是完全充满的,而且是从左向右填充。表示堆的数组A包括两个属性:A.length(通常)给出数组元素的个数,A.heapSize表示有多少个堆元素存储在该数组中,这里0 ≤ A.heapSize ≤ A.length。树的根节点是A[1],这样给定一个节点的下标i,可以计算得到它的父节点、左孩子和右孩子下标:
PARENT(i)
return ⌊i⌋
LEFT(i)
return 2i
RIGHT(i)
return 2i + 1
下面两张图为 以(a)二叉树和(b)数组形式展现的一个最大堆。每个节点圆圈内部的数字是它所存储的数据,结点上方的数字是它在数组中相应的下标。 数组上方和下方的连线显示的是父-子关系:父结点总是在它孩子结点的左边。
(a) (b)
二叉堆可以分为两种形式:最大堆和最小堆。在这两种堆中,结点的值都要满足堆得性质。
在最大堆中,最大堆性质是指除了根节点以外的所有节点 都要满足:
也就是说,某个节点的值至多与其父节点一样大。因此堆中的最大元素存放在根节点中;并且,在任一子树中,该子树所包含的所有结点的值都不大于该子树结点的值。最小堆得组织方式正好相反:最小堆性质是指除了根以外的所有结点 都有:
最小堆中的最小元素存放在根节点中。
在堆排序算法中我们使用的是最大堆。最小堆通常用于构造优先队列。
2、维护堆的性质
MAX-HEPIFY是用于维护最大堆性质的重要过程。它的输入为一个数组和一个下标
。在调用MAX-HEPIFY的时候,我们假定根节点为
和
的二叉树都是最大堆,但这时
有可能小于其孩子,这样就违背了最大堆的性质。MAX-HEPIFY通过让
的值在最大堆中“逐级下降”,从而使得以下标
为根节点的子树重新遵循最大堆的性质。
MAX-HEAPIFY(A,i)
l = LEFT(i)
r = RIGHT(i)
if l ≤ A.heap-size and A[l] > A[i]
largest = l
else
largest = i
if r ≤ A.heap-size and A[r] > A[largest]
largest = r
if largest ≠ i
exchange A[i] with A[largest]
MAX-HEAPIFY(A,largest)
对于一棵以 为根结点、大小为
的子树,MAX-HEPIFY的时间代价包括:调整
、
和
的关系的时间代价为
,加上在一棵树以
的一个孩子为根结点的子树上运行MAX-HEAPIFY的时间代价(这里假设递归调用会发生)。因为每个孩子的子树的大小至多
(最坏情况发生在树的最底层恰好半满的时候),我们可以用下面这个递归式刻画MAX-HEAPIFY的运行时间:
上述递归式的解为。也就是说,对于一个树高为h的结点来说,MAX-HEAPIFY的时间复杂度是
。
3、建堆
用自底向上的方法利用过程 MAX-HEAPIFY把一个大小为的数组A[1..n]转换为最大堆。子数组A(⌊n/2⌋ + 1..n)中的元素都是树的叶子节点,每个叶结点都可以看成只包含一个元素的堆。过程BUILD-MAX-HEAP对树中的其他结点都调用一次MAX-HEAPIFY。
BUILD-MAX-HEAP(A)
A.heap-size = A.length
for i = ⌊A.length/2⌋ downto 1
MAX-HEAPIFY(A,i)
4、堆排序算法
堆排序算法步骤:
1) 利用BUILD-MAX-HEAP将数组A[1..n]建成最大堆,其中n=A.length。
2) 因为数组中的最大元素总在根结点A[1]中,通过把它与A[n]进行互换,我们可以让该元素放到正确的位置。
3)去掉结点n(通过减少A.heap-size的值来实现),剩余的结点中,原来根的孩子结点仍然是最大堆,而新的根结点可能会违背最大堆的性质。
4)为了维护最大堆的性质,我们要做的是调用MAX-HEAPIFY(A,1),从而在A[1..n-1]上构造一个新的最大堆。、
5)重复这一过程,直到堆得大小从n-1降到2。
HEAPSORT(A)
BUILD-MAX-HEAP(A)
for i = A.length downto 2
exchange A[1] with A[i]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A,1)
三、算法分析
1、时间复杂度分析
HEAPSORT过程的时间复杂度是,因为每次调用BUILD-MAX-HEAP的时间复杂度是
,而n - 1次调用MAX-HEAPIFY,每次的时间为
。
2、算法稳定性分析
排序算法稳定性定义:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大,这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, ... 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序是不稳定的排序算法。
四、算法实现
语言:Java 环境:JDK1.8
/**
* @author YangJinyang
* @date 2018/8/12
*/
public class HeapSortClass {
public static void main(String[] args) {
int[] a = {4, 1, 3, 2, 16, 9, 10, 14, 8, 7};
heapSort(a);
// 输出排序后的数组元素
for(int i : a){
System.out.print(i + " ");
}
}
/**
* 堆排序
* 1)构造大顶堆(根,即数组第一个元素为数组最大值)
* 2)将根(最大值)与数组最后一个元素交换,最后一个元素有序
* 3)将前面数字重新调整为堆,重复2的过程
*
* @param a 待排序数组
*/
private static void heapSort(int[] a) {
int length = a.length;
int heapSize = length;
// 构建大顶堆
buildMaxHeap(a);
for (int i = length - 1; i >= 0; i--) {
//将根和最后一个元素交换
exchange(a, 0, i);
//将剩下的数重新调整为大顶堆
heapSize = heapSize - 1;
maxHeapify(a, 0, heapSize);
}
}
/**
* 建最大堆(自底向上)
*
* @param a 待排序数组
*/
private static void buildMaxHeap(int[] a) {
int length = a.length;
// 数组中length / 2 ~ n的元素都为叶结点(只含有一个元素的堆),故不参与堆调整。
for (int i = length / 2 - 1; i >= 0; i--) {
maxHeapify(a, i, length);
}
}
/**
* @param a 数组
* @param i 下标
*/
private static void maxHeapify(int[] a, int i, int heapSize) {
//(2 * i)左节点下标,因数组下标从0开始,所以(2 * i)+1
int l = (i << 1) + 1;
//(2 * i + 1)//右节点下标,因数组下标从0开始,所以(2 * i + 1)+1
int r = (i << 1) + 2;
int largest;
if (l < heapSize && a[l] > a[i]) {
largest = l;
} else {
largest = i;
}
if (r < heapSize && a[r] > a[largest]) {
largest = r;
}
if (largest != i) {
exchange(a, i, largest);
maxHeapify(a, largest, heapSize);
}
}
/**
* 交换数组中a[i]和a[j]
*
* @param a 数组
* @param i 待交换值索引i
* @param j 待交换值索引j
*/
private static void exchange(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
参考:《算法导论》第3版 第6章