排序算法之堆排序
这里是传送门⇒总结:关于排序算法
平均时间复杂度 | 最优时间复杂度 | 最差时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|---|---|
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
堆排序是一种利用“堆”数据结构而设计的排序算法。在升序序列中,利用的是“最大堆”,“最大堆”是“一棵任一父结点均大于或等于其子结点的完全二叉树”。
- 算法描述
- 将待排序列初始化为一个“最大堆”,那么由“最大堆”的性质,现在的堆顶元素是待排序列中的最大值
- 把堆顶元素跟最后一个元素交换,堆长度-1
- 将堆中剩下的元素重新调整为“最大堆”
- 重复以上操作...
- 那么堆排序的过程其实就是不断调整堆的过程,每次调整成堆后都会得到未排序区间中的最大值
- 调整堆的具体实现
- 需要了解结点上浮(shiftUp)、结点下沉(shiftDown)
- 这里需要用到的是结点下沉
- 由于“最大堆”的性质是“任一父结点均大于或等于其子结点”
- 需要对所有的父结点进行检查,看是否满足性质,不满足则调整
- 检查顺序是从最后一个父结点开始,到根结点结束
- 顺序如此的原因是:若从根结点开始检查,那么根结点的值只能是根结点与其直接后继中的最大值,而不是整个堆的最大值;而若是从最后一个父结点开始检查,就可以把该子树的最大值放在该子树的根结点(即最后一个父结点)上,供上层父结点比较,就能把更大的值放在更上层
- 结点上浮的实现与下沉类似,这里仅给出代码,实际上在升序的堆排序算法中不需用到
- JS实现
// 结点上浮:在插入二叉树的最后一个位置时用到,此处没有用到
// 此处的i是完全二叉树里的顺序,与数组里的顺序相比大了1
function MaxHeapShiftUp(array, i) {
while (i / 2 >= 1) {
var index = i - 1;
var pIndex = Math.floor(i / 2) - 1;
if (array[index] > array[pIndex]) {
Swap(array, index, pIndex);
}
i = pIndex + 1;
}
}
// 结点下沉
// 此处的i是完全二叉树里的顺序,与数组里的顺序相比大了1
// 此处传入的array会被直接改变
function MaxHeapShiftDown(array, i, len) {
while (i * 2 <= len) {
var index = i - 1;
var sIndex = i * 2 - 1;
var max = sIndex;
if (sIndex + 1 < len && array[sIndex] < array[sIndex + 1]) {
max += 1;
}
if (array[index] < array[max]) {
Swap(array, index, max);
i = max + 1;
}else {
break;
}
}
}
// 初始化堆
// 此处传入的array会被直接改变
function BuildMaxHeap(array) {
var len = array.length;
for (var i = Math.floor(len / 2); i >= 1; i--) {
MaxHeapShiftDown(array, i, len);
}
}
// 此处传入的array会被直接改变
function HeapSort(array) {
var len = array.length;
BuildMaxHeap(array);
for (var i = len - 1; i > 0; i--) {
Swap(array, 0, i);
MaxHeapShiftDown(array, 1, --len);
}
}
为什么建堆的时候(即BuildMaxHeap函数中),最后一个非叶子结点是Math.floor(len / 2)
?原因看这个笔记:关于完全二叉树
- 结点下沉的递归做法
// 这个是递归的做法
// 此处的i是完全二叉树里的顺序,与数组里的顺序相比大了1
function MaxHeapShiftDown_v2(array, i, len) {
var left, right, max;
max = left = 2 * i;
if (left > len) {
return;
}
right = left + 1;
if (right < len && array[right] > array[left]) {
max = right;
}
if (array[i] < array[max]) {
Swap(array, i, max);
MaxHeapShiftDown_v2(array,max,len);
}
}
- 分析
- 上文表中堆排序的复杂度数据是非递归做法的复杂度
- 执行一次
MaxHeapShiftDown
函数的时间复杂度为O(logn) - 考虑需要时间最长的情况,即对根结点进行下沉,那么执行比较的次数不会超过完全二叉树的深度:[log2n]向下取整 + 1,(深度怎么算的看这个笔记:关于完全二叉树)其中n为结点个数,即时间复杂度为O(logn)
- 建堆的时间复杂度为O(n)(可以看这个别人家的博客)
- 而堆排序的时间 = 建堆的时间 + 调整堆的时间 = O(n) + nO(logn),即堆排序的时间复杂度为 O(nlogn)
- 该排序属于原地排序,空间复杂度S(n) = O(1)