前端必会算法(十):堆排序

571 阅读4分钟

前言

阅读《学习JavaScript数据结构与算法(第3版)》有感
希望自己每次学习算法都能输出一篇博客,收入专栏,检查自身学习情况~ 文章有错欢迎各路大神指正,别喷,硬要喷的话,麻烦轻点,谢谢大神们~

ps: 这书《学习JavaScript数据结构与算法(第3版)》也没有讲堆排序

开始

堆排序可以认为是选择排序的改进版,像选择排序一样将输入划分为已排序和待排序

什么是堆,堆是满足下几点的完全二叉树(什么是二叉树?)

  • 每个节点的值都大于等于(或者小于等于)其左右子节点的值
    • 每个结点的值都大于或等于其左右子结点的值,称为大顶堆;
    • 或每个结点的值都小于或等于其左右子结点的值,称为小顶堆。
  • 除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列
  • 堆可以用一个数组表示,给定最后一个非叶子节点的下标 i (i假设为2)令初始索引为 i = Math.floor(array.length / 2) - 1 ,那么左子节点为 A[2i+1] ,右子节点为 A[2i+2] image.png

思路

  • 创建一个堆 H[0,1,2, ...., n-1];
  • 以大顶堆为例把堆顶元素(最大值)(第一个有效序列)与最后一个子元素(最后一个有效序列)交换,有效序列长度减1
  • 堆化有效序列,使有效序列重新形成一个大顶堆
  • 重复步骤2和3,直到有效序列长度为1,则排序完成

实现

1.创建一个大顶堆

// 将原数组调整为一个对应数组的大顶堆
function buildMaxHeap(array) {
    // 大顶堆的构建是从最后一个非叶子节点开始,从下往上,从右往左调整
    // 最后一个非叶子节点的位置为: Math.floor(array.length/2) - 1
    for (var i = Math.floor(array.length / 2) - 1; i >= 0; i--) {
      adjustHeap(array, i, array.length)
    }
}
/**
* 调整
* @param i 最后一个非叶子节点
* @param length 数组的长度
*/
function adjustHeap(array, i, len) {
    var maxIndex = i; // 最大值索引
    var left = i * 2 + 1; // 左子节点索引
    var right = i * 2 + 2; // 右子节点索引
    // 判断是否有子节点,再比较父节点和左右子节点的大小
    // 因为i是最后一个非叶子节点,所以如果有左右子节点则节点的位置都小于数组的长度
    if (left < len && array[left] > array[maxIndex]) { // 若左子节点比父节点大
      maxIndex = left
    }
    if (right < len && array[right] > array[maxIndex]) { // 若右子节点比父节点大
      maxIndex = right
    }
    // maxIndex为父节点,若发生改变则说明不是最大节点,需要交换
    if (maxIndex != i) {
      [array[maxIndex], array[i]] = [array[i], array[maxIndex]]
      adjustHeap(array, maxIndex, len); // 交换之后递归再次调整比较
    }
}

2.堆排序

调用sort方法进行排序

var arr = [3, 4, 6, 5, 1, 2]
sort(arr)
function sort(array) {
    var length = array.length;
    buildMaxHeap(array); // 构建一个大顶堆
    // 调整为大顶堆后,顶元素为最大元素并与末尾元素交换
    while (length > 0) { // 当length <= 0时,说明已经到堆顶
      [array[0], array[length - 1]] = [array[length - 1], array[0]]; // 交换
      length--; // 交换之后相当于把树中的最大值弹出去了,所以length--
      adjustHeap(array, 0, length); // 交换值并剔除了最大值后,继续进行调整使之再次成为大顶堆
    }
    return array;
}

完整代码

function heapSort(array) {
  if (array.length <= 1) return array; // 如果数组长度为1,直接返回
  function sort(array) {
    var length = array.length;
    buildMaxHeap(array); // 构建一个大顶堆
    // 调整为大顶堆后,顶元素为最大元素并与末尾元素交换
    while (length > 0) { // 当length <= 0时,说明已经到堆顶
      [array[0], array[length - 1]] = [array[length - 1], array[0]]; // 交换
      length--; // 交换之后相当于把树中的最大值弹出去了,所以length--
      adjustHeap(array, 0, length); // 交换值并剔除了最大值后,继续进行调整使之再次成为大顶堆
    }
    return array;
  }

  // 将原数组调整为一个对应数组的大顶堆
  function buildMaxHeap(array) {
    // 大顶堆的构建是从最后一个非叶子节点开始,从下往上,从右往左调整
    // 最后一个非叶子节点的位置为: Math.floor(array.length/2) - 1
    for (var i = Math.floor(array.length / 2) - 1; i >= 0; i--) {
      adjustHeap(array, i, array.length)
    }
  }

  /**
   * 调整
   * @param i 最后一个非叶子节点
   * @param length 数组的长度
   */
  function adjustHeap(array, i, len) {
    var maxIndex = i; // 最大值索引
    var left = i * 2 + 1; // 左子节点索引
    var right = i * 2 + 2; // 右子节点索引
    // 判断是否有子节点,再比较父节点和左右子节点的大小
    // 因为i是最后一个非叶子节点,所以如果有左右子节点则节点的位置都小于数组的长度
    if (left < len && array[left] > array[maxIndex]) { // 若左子节点比父节点大
      maxIndex = left
    }
    if (right < len && array[right] > array[maxIndex]) { // 若右子节点比父节点大
      maxIndex = right
    }
    // maxIndex为父节点,若发生改变则说明不是最大节点,需要交换
    if (maxIndex != i) {
      [array[maxIndex], array[i]] = [array[i], array[maxIndex]]
      adjustHeap(array, maxIndex, len); // 交换之后递归再次调整比较
    }
  }

  return sort(array)
}

优化

堆排序是选择排序的改进版
暂无其他优化想法,欢迎各路大神评论

复杂度

排序算法平均时间复杂度最好情况最坏情况空间复杂度排序方式稳定性
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)in-place不稳定
  • 时间复杂度 建堆过程的时间复杂度是 O(n) ,排序过程的时间复杂度是 O(nlogn) ,整体时间复杂度是 O(nlogn)
  • 空间复杂度 本地排序,空间复杂度O(1)

其他

# 前端算法学习-算法复杂度
# 前端必会算法(一):冒泡排序
# 前端必会算法(二):选择排序
# 前端必会算法(三):插入排序
# 前端必会算法(四):归并排序
# 前端必会算法(五):快速排序
# 前端必会算法(六):希尔排序
# 前端必会算法(七):计数排序
# 前端必会算法(八):桶排序
# 前端必会算法(九):基数排序

最后

渣渣一个,欢迎各路大神多多指正,不求赞,只求监督指正

参考

# 前端进阶算法9:看完这篇,再也不怕堆排序、Top K、中位数问题面试了
# 搞定JavaScript算法系列--堆排序
# 十大排序算法---堆排序