算法(三):排序算法(下)

45 阅读4分钟

堆排序

堆排序是利用最大/最小堆来进行排序,如果你还不熟悉堆,请看这里: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。看下图可能会更好理解:

image.png 先设置步长为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),有时甚至在小数组中比快速排序和堆排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。总之,希尔排序大多数情况下效率都高于冒泡排序、插入排序、选择排序,但是由于受增量影响,我们通常还是会选择快速、递归或者堆排序。