堆排序(Heapsort)是指利用二叉堆这种数据结构所设计的一种排序算法。
二叉堆
二叉堆是一种特殊的堆,其有以下特性:
- 二叉堆是完全二叉树或者是近似完全二叉树
- 每个节点的左子树和右子树都是一个二叉堆
- 父节点的键值总是与子节点的键值保持固定的序关系(parent>=son || parent<=son)
当父节点的键值总是大于或等于任何一个子节点的键值时为 最大堆。 当父节点的键值总是小于或等于任何一个子节点的键值时为 最小堆。
二叉堆一般用数组来表示。如果根节点在数组中的位置是1,则:
- leftSon(n) = 2n
- rightSon(n) = 2n+1
- parent(n) = n/2
如果存储数组的下标基于0,则:
- leftSon(n) = 2n+1
- rightSon(n) = 2n+2
- parent(n) = (n-1)/2
以[49,38,65,97,76,13,27,55,04]为例,构建二叉堆初始状态:
图A展示的并不是真正的二叉堆,因为它不满足二叉堆的特性: 父节点的键值总是与子节点的键值保持固定的序关系
。
二叉堆的操作
以下说明基于最大堆
从下至上的重新建堆(sift-up)
在一个 最大堆 中,如果一个节点的值大于其父节点的值(son > parent),不满足
固定的序关系(parent >= son)
,
那么该节点就需要上移,直到满足该节点大于其两个子节点,而小于其父节点为止,从而达到使整个堆实现二叉堆的要求。
简单的说,比较子节点与父节点,不满足堆性质则交换。从而使得满足二叉堆的性质。在二叉堆中插入节点时需要用到此操作,如下:
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* 二叉堆从下至上的重新建堆(sift-up)
*
* @param array 堆数组,index前的元素已经构成最大堆
* @param index 新插入节点的索引
* @param <T>
*/
private static <T extends Comparable<? super T>> void siftUp(T[] array, int index) {
int son = index;
T key = array[son];
while (son > 0) {
int parent = getParent(son);
if (key.compareTo(array[parent]) <= 0) {
break;
}
array[son] = array[parent];
son = parent;
}
array[son] = key;
}由上至下的重新建堆(sift-down)
在一个 最大堆 中,如果一个节点的值小于其子节点的值(parent < son),不满足
固定的序关系(parent >= son)
,就违反了二叉堆的定义,那么该节点就需要下移,直到满足该节点大于其两个子节点为止,从而达到使整个堆实现二叉堆的要求。需要注意的是,要与两个子节点中较大的节点进行比较
。
简单的说,比较父节点与子节点,不满足堆性质则交换。从而使得满足二叉堆的性质。
在二叉堆中删除根元素时,会用到此操作。a = 100
删除根元素的步骤:
- 删除根元素
- 将堆中最后一个元素放到根元素位置
- 对新的根元素进行sift-down操作
代码实现:
1 | /** |
堆排序
利用二叉堆的性质根节点必定是最大值(最大堆)或者最小值(最小堆)
可以很容易实现排序。
- 将给定元素集合构成一个二叉堆
- 将根节点与最后一个节点替换位置,二叉堆的size - 1
- 对新的根节点进行sift-down操作
- 重复2 3 操作,直到二叉堆内仅剩一个元素
至此,排序完成
java实现
1 |
|
分析
- 最差时间复杂度 O(n log n)
- 最优时间复杂度 O(n log n)
- 平均时间复杂度 θ(n log n)
- 最差空间复杂度 O(n) total, O(1) auxiliary
- 堆排序属于就地排序
- 堆排序可以在集合中查找最大或者最小的N个值,而不需要对全部数据进行排序
优先级队列
利用二叉堆的性质根节点必定是最大值(最大堆)或者最小值(最小堆)
同样可以很容易实现优先级队列。
- 队列添加元素:二叉堆对新元素进行sift-up
- 队列获取(poll)元素:二叉堆删除根元素 sift-down
JDK中的PriorityQueue、PriorityBlockingQueue都是基于二叉堆实现的。