JS完成堆排序

堆的准备学问

  • 堆是一个完整二叉树。
  • 完整二叉树: 二叉树除开末了一层,其他层结点数都到达最大,末了一层的一切结点都集合在左侧(左侧结点分列满的情况下,右侧才缺失结点)。
  • 大顶堆:根结点为最大值,每一个结点的值大于或即是其孩子结点的值。
  • 小顶堆:根结点为最小值,每一个结点的值小于或即是其孩子结点的值。
  • 堆的存储: 堆由数组来完成,相当于对二叉树做层序遍历。以下图:

《JS完成堆排序》

《JS完成堆排序》

关于结点 i ,其子结点为 2i+1 与 2i+2 。

堆排序算法

《JS完成堆排序》

如今须要对如上二叉树做升序排序,统共分为三步:

  1. 将初始二叉树转化为大顶堆(heapify)(本质是从第一个非恭弘=叶 恭弘子结点最先,从下至上,从右至左,对每一个非恭弘=叶 恭弘子结点做shiftDown操纵),此时根结点为最大值,将其与末了一个结点交流。
  2. 除开末了一个结点,将其他节点构成的新堆转化为大顶堆(本质上是对根节点做shiftDown操纵),此时根结点为次最大值,将其与末了一个结点交流。
  3. 重复步骤2,直到堆中元素个数为1(或其对应数组的长度为1),排序完成。

下面细致图解这个历程:

步骤1:

初始化大顶堆,起首拔取末了一个非恭弘=叶 恭弘子结点(我们只须要调解父节点和孩子节点之间的大小关联,恭弘=叶 恭弘子结点之间的大小关联无需调解)。设数组为arr,则第一个非恭弘=叶 恭弘子结点的下标为:i = Math.floor(arr.length/2 – 1) = 1,也就是数字4,如图中虚线框,找到三个数字的最大值,与父节点交流。

《JS完成堆排序》

然后,下标 i 顺次减1(即从第一个非恭弘=叶 恭弘子结点最先,从右至左,从下至上遍历一切非恭弘=叶 恭弘子节点)。背面的每一次调解都是云云:找到父子结点中的最大值,做交流。

《JS完成堆排序》

这一步中数字6、1交流后,数字[1,5,4]构成的堆递次不对,须要实行一步调解。因而须要注重,每一次对一个非恭弘=叶 恭弘子结点做调解后,都要视察是不是会影响子堆递次!

《JS完成堆排序》

此次调解后,根节点为最大值,形成了一个大顶堆,将根节点与末了一个结点交流。

步骤2:

除开当前末了一个结点6(即最大值),将其他结点[4,5,3,1]构成新堆转化为大顶堆(注重视察,此时根节点之外的其他结点,都满足大顶堆的特性,所以能够从根节点4最先调解,即找到4应当处于的位置即可)。

《JS完成堆排序》

《JS完成堆排序》

步骤3:

接下来重复实行步骤2,直到堆中元素个数为1:

《JS完成堆排序》

《JS完成堆排序》

《JS完成堆排序》

堆中元素个数为1, 排序完成。

JavaScript完成

// 交流两个节点
function swap(A, i, j) {
  let temp = A[i];
  A[i] = A[j];
  A[j] = temp; 
}

// 将 i 结点以下的堆整顿为大顶堆,注重这一步完成的基本实际上是:
// 假定 结点 i 以下的子堆已是一个大顶堆,shiftDown函数完成的
// 功用是实际上是:找到 结点 i 在包含结点 i 的堆中的准确位置。背面
// 将写一个 for 轮回,从第一个非恭弘=叶 恭弘子结点最先,对每一个非恭弘=叶 恭弘子结点
// 都实行 shiftDown操纵,所以就满足了结点 i 以下的子堆已是一大
//顶堆
function shiftDown(A, i, length) {
  let temp = A[i]; // 当前父节点
// j<length 的目标是对结点 i 以下的结点悉数做递次调解
  for(let j = 2*i+1; j<length; j = 2*j+1) {
    temp = A[i];  // 将 A[i] 掏出,全部历程相当于找到 A[i] 应处于的位置
    if(j+1 < length && A[j] < A[j+1]) { 
      j++;   // 找到两个孩子中较大的一个,再与父节点比较
    }
    if(temp < A[j]) {
      swap(A, i, j) // 假如父节点小于子节点:交流;不然跳出
      i = j;  // 交流后,temp 的下标变成 j
    } else {
      break;
    }
  }
}

// 堆排序
function heapSort(A) {
  // 初始化大顶堆,从第一个非恭弘=叶 恭弘子结点最先
  for(let i = Math.floor(A.length/2-1); i>=0; i--) {
    shiftDown(A, i, A.length);
  }
  // 排序,每一次for轮回找出一个当前最大值,数组长度减一
  for(let i = Math.floor(A.length-1); i>0; i--) {
    swap(A, 0, i); // 根节点与末了一个节点交流
    shiftDown(A, 0, i); // 从根节点最先调解,而且末了一个结点已为当
                         // 前最大值,不须要再介入比较,所以第三个参数
                         // 为 i,即比较到末了一个结点前一个即可
  }
}

let Arr = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
heapSort(Arr);
alert(Arr);

顺序解释: 将 i 结点以下的堆整顿为大顶堆,注重这一步完成的基本实际上是:假定 结点 i 以下的子堆已是一个大顶堆,shiftDown函数完成的功用是实际上是:找到 结点 i 在包含结点 i 的堆中的准确位置。背面做第一次堆化时,heapSort 中写了一个 for 轮回,从第一个非恭弘=叶 恭弘子结点最先,对每一个非恭弘=叶 恭弘子结点都实行 shiftDown操纵,所以就满足了每一次 shiftDown中,结点 i 以下的子堆已是一大顶堆。

复杂度剖析:adjustHeap 函数中相当于堆的每一层只遍历一个结点,由于
具有n个结点的完整二叉树的深度为[log2n]+1,所以 shiftDown的复杂度为 O(logn),而外层轮回共有 f(n) 次,所以终究的复杂度为 O(nlogn)。

堆的运用

堆主如果用来完成优先行列,下面是优先行列的运用示例:

  • 操纵系统动态挑选优先级最高的使命实行。
  • 静态问题中,在N个元素中选出前M名,运用排序的复杂度:O(NlogN),运用优先行列的复杂度: O(NlogM)。

而完成优先行列采纳一般数组、递次数组和堆的差别复杂度以下:

《JS完成堆排序》

运用堆来完成优先行列,能够使入队和出队的复杂度都很低。

    原文作者:Leon
    原文地址: https://segmentfault.com/a/1190000015487916
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞