堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
关于数据结构堆相关内容我们在堆(二叉堆)-优先队列-数据结构和算法(Java)已经讲解。
堆排序可以分为2个阶段。在堆的构造阶段中,我们可以将原数组重新组织安排进一个堆中;然后在下沉排序阶段,我们从堆中按递减(或递增)顺序取出所有元素并得到排序结果。以从大到小排序为例,我们将使用一个最大堆并重复删除最大元素。为了排序的需要我们不在将堆的具体表示隐藏,并将直接使用swim()和sink()操作。这样我们在排序时就可以将需要排序的数组本身作为堆,因此无需任何额外的空间。
由N个给定的元素来构造一个堆如何做呢?
命题R:用下沉操作由N个元素构造堆只需小于2N次比较以及少于N次交换。
堆排序的主要工作在第二阶段完成。这里我们将堆顶元素删除,然后堆缩小后数组中空出的位置。
命题S:将N个元素排序,堆排序只需( 2 N lg N + 2 N ) 2N\lg N+2N) 2NlgN+2N)次比较(以及一半的交换)
证明:2N项来自于堆的构造。 2 N lg N 2N\lg N 2NlgN来自于每次下沉操作最大可能需要 2 lg N 2\lg N 2lgN次比较
堆索引从0开始的实现代码4-1如下:
import java.util.Arrays;
import java.util.Comparator;
/**
* 堆排序
* 默认使用最大堆从小到大排序
* 传入从大到小比较器为使用最小堆从大到小排序
* @author Administrator
* @date 2022-12-06 22:15
*/
public class Heap {
/**
* 比较器
* 默认从小到大
*/
private static Comparator<Comparable> comparator = Comparable::compareTo;
/**
* 堆排序
* @param a 数组
* @param comparator 比较器
*/
public static void sort(Comparable[] a, Comparator<Comparable> comparator) {
Heap.comparator = comparator;
sort(a);
}
/**
* 堆排序,默认最大堆,从小到大排序
* @param a 数组
*/
public static void sort(Comparable[] a) {
int s = a.length - 1;
for (int i = (s - 1) / 2 ; i >= 0; i--) {
sink(a, i, s);
}
while (s > 0) {
exch(a, 0, s--);
sink(a, 0, s);
}
}
/**
* 下沉元素
* @param a 二叉堆
* @param i 下沉元素索引
* @param s 最大索引
*/
private static void sink(Comparable[] a, int i, int s) {
while (2 * ( i+1 ) - 1 <= s) {
int j = 2 * (i + 1) - 1;
if (j < s && compare(a, j, j+1)) j++;
if (!compare(a, i, j)) break;
exch(a, i, j);
i = j;
}
}
/**
* 交换元素
* @param a 数组
* @param i 索引
* @param j 索引
*/
private static void exch(Comparable[] a, int i, int j) {
// 索引-1目的使堆索引对应数组0开始的索引
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
/**
* 比较数组2个位置元素
* @param a 数组
* @param i 索引
* @param j 索引
* @return
*/
private static boolean compare(Comparable[] a, int i, int j) {
return comparator.compare(a[i], a[j]) < 0;
}
public static void main(String[] args) {
Integer[] a = {-1, 132, 234, 2, 22, 92992, 992, 991};
// 从小到大排序
sort(a);
// 从大到小排序
// sort(a, ((o1, o2) -> o2.compareTo(o1)));
System.out.println(Arrays.toString(a));
}
}
堆索引从1开始的实现(算法第4版)代码4-2如下所示:
public class Heap {
private Heap() { }
public static void sort(Comparable[] pq) {
int n = pq.length;
for (int k = n/2; k >= 1; k--)
sink(pq, k, n);
int k = n;
while (k > 1) {
exch(pq, 1, k--);
sink(pq, 1, k);
}
}
private static void sink(Comparable[] pq, int k, int n) {
while (2*k <= n) {
int j = 2*k;
if (j < n && less(pq, j, j+1)) j++;
if (!less(pq, k, j)) break;
exch(pq, k, j);
k = j;
}
}
private static boolean less(Comparable[] pq, int i, int j) {
return pq[i-1].compareTo(pq[j-1]) < 0;
}
private static void exch(Object[] pq, int i, int j) {
Object swap = pq[i-1];
pq[i-1] = pq[j-1];
pq[j-1] = swap;
}
public static void main(String[] args) {
String[] a = {-1, 132, 234, 2, 22, 92992, 992, 991};
Heap.sort(a);
System.out.println(Arrays.toString(a));
}
}
大多数在下沉排序期间重新插入堆的元素会被直接加入到堆底。Floyd在1964年观察发现,我们正好可以通过免去检查元素是否到达正确位置来节省时间。在下沉中总是直接提升较大的子结点直至到达堆底,然后在使元素上浮到正确的位置。这个想法几乎可以将比较的次数减少一半-接近归并排序的比较次数(随机数组)。这种方法需要额外的空间,因此在实际应用中只有当比较操作代价较高时才有用(例如,当我们在将字符串或者其他键值较长类型的元素进行排序时)。
如果小伙伴什么问题或者指教,欢迎交流。
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/algorithm
[1][美]Robert Sedgewich,[美]Kevin Wayne著;谢路云译.算法:第4版[M].北京:人民邮电出版社,2012.10