堆的准备学问
- 堆是一个完整二叉树。
- 完整二叉树: 二叉树除开末了一层,其他层结点数都到达最大,末了一层的一切结点都集合在左侧(左侧结点分列满的情况下,右侧才缺失结点)。
- 大顶堆:根结点为最大值,每一个结点的值大于或即是其孩子结点的值。
- 小顶堆:根结点为最小值,每一个结点的值小于或即是其孩子结点的值。
- 堆的存储: 堆由数组来完成,相当于对二叉树做层序遍历。以下图:
关于结点 i ,其子结点为 2i+1 与 2i+2 。
堆排序算法
如今须要对如上二叉树做升序排序,统共分为三步:
- 将初始二叉树转化为大顶堆(heapify)(本质是从第一个非恭弘=叶 恭弘子结点最先,从下至上,从右至左,对每一个非恭弘=叶 恭弘子结点做shiftDown操纵),此时根结点为最大值,将其与末了一个结点交流。
- 除开末了一个结点,将其他节点构成的新堆转化为大顶堆(本质上是对根节点做shiftDown操纵),此时根结点为次最大值,将其与末了一个结点交流。
- 重复步骤2,直到堆中元素个数为1(或其对应数组的长度为1),排序完成。
下面细致图解这个历程:
步骤1:
初始化大顶堆,起首拔取末了一个非恭弘=叶 恭弘子结点(我们只须要调解父节点和孩子节点之间的大小关联,恭弘=叶 恭弘子结点之间的大小关联无需调解)。设数组为arr,则第一个非恭弘=叶 恭弘子结点的下标为:i = Math.floor(arr.length/2 – 1) = 1,也就是数字4,如图中虚线框,找到三个数字的最大值,与父节点交流。
然后,下标 i 顺次减1(即从第一个非恭弘=叶 恭弘子结点最先,从右至左,从下至上遍历一切非恭弘=叶 恭弘子节点)。背面的每一次调解都是云云:找到父子结点中的最大值,做交流。
这一步中数字6、1交流后,数字[1,5,4]构成的堆递次不对,须要实行一步调解。因而须要注重,每一次对一个非恭弘=叶 恭弘子结点做调解后,都要视察是不是会影响子堆递次!
此次调解后,根节点为最大值,形成了一个大顶堆,将根节点与末了一个结点交流。
步骤2:
除开当前末了一个结点6(即最大值),将其他结点[4,5,3,1]构成新堆转化为大顶堆(注重视察,此时根节点之外的其他结点,都满足大顶堆的特性,所以能够从根节点4最先调解,即找到4应当处于的位置即可)。
步骤3:
接下来重复实行步骤2,直到堆中元素个数为1:
堆中元素个数为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)。
而完成优先行列采纳一般数组、递次数组和堆的差别复杂度以下:
运用堆来完成优先行列,能够使入队和出队的复杂度都很低。