1. 原理
1.1 什么是堆
要理解堆排序,首先要先理解什么是堆。堆是一颗顺序存储的完全二叉树,堆又分为最大堆和最小堆。
- 完全二叉树:设二叉树的深度为h,除h层外,其他各层的节点数都达到最大个数,第h层的所有节点都连续集中在最左边。
- 最小堆:每个节点的值都不大于其子节点的值的完全二叉树
- 最小堆:每个节点的值都不小于其子节点的值的完全二叉树
根据上面的描述我们可以用一个数学描述来定义最大最小堆:
对于数组[D(0), D(1), …., D(n)]当且仅当满足下列关系时称之为堆
- 最小堆:D(i) <= D(2 * i + 1)并且D(i) <= D(2 * i + 2)
- 最大堆:D(i) >= D(2 * i + 1)并且D(i) >= D(2 * i + 2)
- 其中整数i = 0, 1, 2, …, n/2
举个栗子:[3, 4, 7, 12, 15, 18]就是一个典型的最小堆, i <= 2。
- i取0时:3 > 4, 3 > 4
- i取1时:4 > 12, 4 > 15
- i取2时:7 > 18
1.2 堆排序
理解了堆的概念之后,堆排序就是利用最大堆和最小堆的特性进行排序
- 将数组初始化成最大堆
- 交换最大堆的第一个和最后一个数字,输出最后一个数字(最大值)
- 将破坏后的最大堆重新调整为最大堆
- 接着重复2~3步直到交换堆的第一和第二个节点,此时结束排序
2. 实现
此算法的关键在于如何构建最大堆。根据上诉最大最小堆的定义,可以写出针对某一parent的调整算法。
public void adjustMaxHeap(int[] data, int parent, int length) {
int temp = data[parent];
int child = 2 * parent + 1;
while (child < length) {
//如有有右子节点,并且右子节点大于左子节点,则选取右子节点和parent比较
if (child + 1 < length && data[child] < data[child + 1]) {
child++;
}
//如果父节点的值大于子节点的值,则跳出循环
if (temp >= data[child]) {
break;
}
//把孩子节点的值赋给父节点
data[parent] = data[child];
//选取子节点的左子节点,继续向下调整
parent = child;
child = 2 * child + 1;
}
data[parent] = temp;
}
此函数为调整某个parent的值,初始化的时候需要从n/2开始一直循环调整到0;
for (int i = data.length / 2; i >= 0; i--) {
adjustMaxHeap(data, i);
}
完整对堆排序算法如下:
@Override
public int[] sort(int[] data) {
if (data == null || data.length <= 1) {
return data;
}
for (int i = data.length / 2; i >= 0; i--) {
adjustMaxHeap(data, i, data.length);
}
for (int i = data.length - 1; i > 0; i--) {
//最后一个数字和第一个数字交换
swap(data, 0, i);
//由于parent=0的节点发生了变化,因此需要重新调整堆
adjustMaxHeap(data, 0, i);
}
return data;
}
完整实现可查看:
3. 复杂度
堆排序是一种不稳定的排序算法。平均,最坏和最好的时间复杂度都是O(nlogn)。