堆排序
堆排序是利用最大/最小堆来进行排序,如果你还不熟悉堆,请看这里:juejin.cn/post/722114…。具体思路:
首先构建一个最大堆,然后将堆的根节点与堆的最后一个元素交换,这样最大值就放到了正确的位置上,接着,将堆的大小减一,并将剩余的元素重新构建成一个最大堆。不断重复这个过程直到堆的大小为1。
首先实现原地建堆:
// 原地建堆
function buildHeap(arr: number[]) {
const len = arr.length;
// 从最后一个非叶子节点开始下沉
for(let i = Math.floor((arr.length - 2) / 2); i >= 0; i--){
shiftDown(arr, i, len);
}
}
// 下沉
function shiftDown(arr: number[], index: number, length: number) {
// 获取左子结点索引
let leftIndex = index * 2 + 1;
// 如果左子结点索引大于等于数组长度 则说明没有子结点
while (leftIndex < length) {
// 获取右节点索引
const rightIndex = leftIndex + 1;
// 获取左右节点中较大元素的索引
let maxIndex = leftIndex;
if (rightIndex < length && arr[rightIndex] >= arr[leftIndex]) {
// 存在右子节点
maxIndex = rightIndex;
} else {
// 不存在右子节点
maxIndex = leftIndex;
}
// 如果当前节点大于等于左右子节点中最大的值 则不需要下沉
if (arr[index] >= arr[maxIndex]) {
break;
}
// 如果当前节点小于左右子节点中最大的值 则交换位置
[arr[index], arr[maxIndex]] = [arr[maxIndex], arr[index]];
index = maxIndex;
leftIndex = index * 2 + 1;
}
}
然后是排序主体:
// 堆排序
function heapSort(arr: number[]): number[] {
// 原地建堆
buildHeap(arr);
// 堆长度
let heapLen = arr.length;
while(heapLen > 1) {
// 交换堆顶元素和最后一个元素
[arr[0], arr[heapLen - 1]] = [arr[heapLen - 1], arr[0]];
// 堆长度减一
heapLen--;
// 从堆顶开始下沉
shiftDown(arr, 0, heapLen);
}
return arr
}
堆排序总结
时间复杂度:堆的建立需要进行n/2次下沉操作,每次下沉操作需要进行logn步,所以建堆的时间复杂度是O(nlogn);建堆后排序操作需要进行n-1次,每次的下沉操作是logn,所以这里的时间复杂度也是O(nlogn);总体来看堆排序的时间复杂度为O(2nlogn),对于常数2我们一般会忽略,所以时间复杂度为O(nlogn);可见堆排序的效率也是非常高的。
希尔排序
希尔排序是在插入排序的基础上做了一些优化:插入排序是需要在左边已经排好序的元素中一个一个查找、移动,然后插到合适的位置,如果一个元素需要移动到最左边那么就会非常消耗性能。希尔排序是先设置一个步长,然后使用插入排序的方法来排相隔步长长度的几组数据。设置步长的行为可能不止一次,但是最后一次的步长一定要是1。看下图可能会更好理解:
先设置步长为5,这时就是排81、35、41;94、17、75;11、95、15;96、28;12、58这几组数据。完成后修改步长为3,排步长为3的几组数据,以此类推,直到步长设置为1完成对应排序即可。
这里的步长我们称之为增量,一般我们会定义一个增量序列d1、d2...dk,dk为1,对于增量的具体取值其实有一些经过验证,相对效率较高的几种计算方式,比如希尔增量、Hibbard增量、Knuth增量等。拿希尔增量来说,其计算公式为:dk=floor(n/(2^k))
,其中n为待排序数量,k为增量序列的元素下标。
具体实现:
// 希尔排序
function shellSort(arr: number[]): number[] {
let len = arr.length;
// 增量
let gap = Math.floor(len / 2);
while (gap > 0) {
// 从增量开始遍历 遍历增量集合
for (let i = gap; i < len; i++) {
// 增量集合的插入排序操作
// 记录当前元素
const newNum = arr[i];
let j = i - gap;
// 如果新元素比已排序的元素小 则将已排序的元素后移
while (j >= 0 && arr[j] > newNum) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = newNum;
}
// 缩小增量
gap = Math.floor(gap / 2);
}
return arr;
}
希尔排序总结
希尔排序的时间复杂度是和增量有密切关系的,所以它的时间复杂度并不是固定的,在增量使用n/2^k
时,最坏的情况下的时间复杂度达到O(n^2)。但是如果使用效率较高的增量计算公式时,它的时间复杂度可以达到O(nlog²n),有时甚至在小数组中比快速排序和堆排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。总之,希尔排序大多数情况下效率都高于冒泡排序、插入排序、选择排序,但是由于受增量影响,我们通常还是会选择快速、递归或者堆排序。