二叉堆概述
二叉堆(下文简称为堆)是一种优先队列,对于一个优先队列应该至少包含insert(插入)和deleteMin(删除最小者)两个操作。
堆具有 结构性 和 堆序性 两个性质。
堆的性质
结构性
堆是一颗 完全二叉树。所以对于一个高为$h$的完全二叉树有$ 2^h $到$2^{h+1}$个节点。这意味着有$N$个节点的完全二叉树的高为$\left\lfloor\log N\right\rfloor$($\log N$向下取整)。
观察完全二叉树的结构,可以发现,我们存储完全二叉树不需要使用链表,用数组即可,只需要将元素按从上到下,由左至右的顺序依次存入数组(为方便计算,数组从下标1开始存储元素)。这时,对于数组任意位置上$ i(i \neq 0) $的元素,其左子节点在位置$2i$上,右子节点在左子节点之后的位置,即$2i+1$,它的父节点在位置$\left\lfloor i/2 \right\rfloor$上(此时$i \neq 1$)。
堆序性
堆任意节点应该小于它的所有后裔节点,从而有了“对于每一个节点$X$,$X$的父节点元素应小于或等于$X$,根节点除外”,这就是堆序性(小堆,相反则是大堆)。
堆的实现
1 | public class BinaryHeap<T extends Comparable<? super T>> { |
堆排序
堆排序是使用二叉堆进行排序的一种排序算法。
堆排序实现
这里堆是从数组下标0开始存储的,所以子节点位置的计算和之前的有略微不同。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53public class HeapSort {
private static int leftChild(int i) {
return 2 * i + 1;
}
private static <T extends Comparable<? super T>> void percolateDown(T[] arr, int i, int n) {
T temp = arr[i];
int child;
for (; leftChild(i) < n; i = child) {
child = leftChild(i);
// child != n - 1意味着i还有右孩子
if (child != n - 1 && arr[child+1].compareTo(arr[child]) > 0) {
child++;
}
// 小于temp,空穴中应该放入temp
if (temp.compareTo(arr[child]) > 0) {
break;
} else {
// 空穴下移一层
arr[i] = arr[child];
}
}
arr[i] = temp;
}
private static <T extends Comparable<? super T>> void swap(T[] arr, int i, int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static <T extends Comparable<? super T>> void heapSort(T[] arr) {
// build max heap
for (int i = arr.length / 2 - 1; i >= 0; i--) {
percolateDown(arr, i, arr.length);
}
for (int i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i);
percolateDown(arr, 0, i);
}
}
public static void main(String[] args) {
Integer nums[] = {2, 83, 34, 12, 78, 39, 20};
heapSort(nums);
Stream.of(nums).forEach(System.out::println);
}
}
堆排序的时间复杂度
堆排序的时间复杂堆为$O(N\log N)$。堆排序需要进行$N$次deleteMin操作,每次deleteMin操作的时间复杂度为$O(\log N)$,因此总的时间复杂度为$O(N\log N)$。
源码
算法完整源码
参考
MarkAllenWeiss. 数据结构与算法分析:Java语言描述-第3版[M]. 机械工业出版社, 2016, 156-164,191-193