堆排序(Heapsort)
作者:互联网
堆排序是很经典的一种排序,今天用JS手写了一下,过程并不顺利,好在最后用自己的方法实现一遍,记录一下,最近也把几种今典的排序算法过了一遍,后面也会继续更新。
堆(Heap)的简单介绍
堆是一种数据结构,但是这里讲的堆和真正意义上的计算机内存中的堆区别还是很大的。
我们这里讲的堆从抽象上看是一颗完全二叉树或者是近似完全二叉树,而且每个结点的值都要比子节点的大或者小,如果父亲节点的值比子节点大,则叫做最大堆,反之叫做最小堆。
一个最大堆
刚刚是抽象的看,在写堆排序的时候,不会真的用二叉树去表示,而是用了一维数组,这里有一个很重要的结点之间的关系公式。
假设数组arr[ ]表示一个最大堆,那么下标为i的节点,它的父节点下标是Math.floor((i-1)/2),左子节点是i*2+1,右子节点是i*2+2,最一个非叶子节点的下标为Math.floor(arr.length/2-1)
这个堆的数组表示就是广度优先遍历二叉树的结果:[100,60,50,20,55,30]。
这里我们发现了最大堆只能确保父亲节点比两个子节点大,两个子节点之间的大小关系不得而知,而且不同父亲节点的子节点之间也没有可比性。数组最大的值是根节点。
堆排序的过程:
- 先把一个数组变成一个堆(最大堆的数组表示),此时整个数组最大的值就是根节点arr[0]。
- 把根节点和最后一个节点互换,那么最大值就在数组的最末尾arr[arr.length-1]了。
- 经过上面的互换,这个堆被打乱了,需要重新建堆,但是这次建的堆不会包括我们刚刚移到最后的最大值,而是除了这个之外的剩下的数组的值arr[0]~arr[arr.length-1-1]。
- 经过再一次建堆后我们又找到了一个最大值(剩下里面的最大值),然后再次与arr[0]互换位置,然后再建堆,再互换……
- 直到剩下的数据只有一个的时候,数组就有序了。
通过上面的描述,可以看出,我们主要是用堆的根节点是最大值这一特性来找到最大的值,然后排除这个最大值,再找一个最大值……那么现在最关键的问题就是如何建堆了,这也是最难的地方。
如何把一个数组变成一个堆Heap?
方向:从下向上,从右往左
大致思路:
- 把这个无序数组也先看成一棵完全二叉树或近似完全二叉树,广度优先遍历的二叉树。
- 先找到最后一个非叶子节点,如果它的两个子节点有比它大的,就交换它和比他大的那个子节点的位置。
举个例子:数组arr[100, 20, 105, 30, 50, 40]
第一个非叶子子节点是105,发现105比它的子节点都大,那就不用换了,继续找上一个非叶子节点,也就是20,20的两个子节点都比它大,那就找最大的一个,找到了50,把50与20交换
然后再找上一个非叶子节点,找到了100,按照上面的规则,100要和105交换
然后我们就得到了一个最大堆,也叫大顶堆
上面的步骤看上去都很顺利,感觉也很简单,但是我们还是漏了一种情况,那就是在交换了某两个节点后,会不会影响后面的子节点之间的大小关系,这句话可能很难理解,来看个例子
假设我们要建堆的数组是[4,6,8,5,9]
先找到第一个非叶子节点:6,发现它的子节点9比它大,交换6和9
继续找上一个非叶子节点:4,发现它的子节点9比它大,交换4和9
这时候出现了一个问题,那就是4的子节点有数字比它大,这就要求我们回头检查(调整),这时候你会发现当4和9交换了位置后,只要检查以4为根节点的树就可以了,并不需要检查以8为根节点的子树,因为我们是从下向上建堆的,以8为根节点的子树在被构建的时候肯定满足了最大堆的要求。那么如何回头检查(调整)呢,无非就是看看4的两个子节点有没有比它大的,有就交换,然后再向下检查这个和4互换的节点……很明显这是一个递归的过程。
代码实现的分析
代码的实现分为两个部分:建堆的实现、排序的实现
先来建堆:
前面说了,建堆要遍历所有非叶子节点,所以最外层肯定是一个for循环
-
function buildHeap(arr, length) { for(let i = Math.floor(length/2-1);i>=0;i--){ } }
这个for循环里要做点什么呢?当然是调整当前找到的这个非叶子节点和它两个子节点的位置,所以我们单独写一个函数用来调整。
function buildHeap(arr, length) {
for(let i = Math.floor(length/2-1);i>=0;i--){
adjust(arr,i,length)
}
}
function adjust(arr, i, length) {
let p = i
if(2*i+2 > length-1){
// 一个子节点
if(arr[2*i+1]>arr[i]){
p = 2*i+1
}
}else{
// 两个子节点
if(arr[2*i+1]>arr[2*i+2]){
p = 2*i+1
}else{
p = 2*i+2
}
if(arr[p]<arr[i]){
p = i
}
}
if(p != i){
let temp = arr[i]
arr[i] = arr[p]
arr[p] = temp
}
}
可能有人会对这个length参数感到困惑,为什么不直接arr.length,这是因为堆排序是要不断的建堆的,每次建堆,并交换首尾,这个数组的最后一位在下一次建堆的时候就不能算进去了,所以我们后面要通过这个length参数来把已经交换到最后的元素排除在外 。
前面说到了,调整后还需要回头看,这个回头看是一个递归的过程,那么哪里可以放递归函数呢?这个调整函数要对被换下来的节点进行递归检查,那么我们就应该把它放到有节点交换的地方,也就是最后一个if里面。
但是写递归必须要设置一个出口,要不然这个程序就死了,那么什么时候停止调整呢?当然是找到叶子节点的时候,叶子节点是没子节点的,自然就不用检查了。
function buildHeap(arr, length) {
for(let i = Math.floor(length/2-1);i>=0;i--){
adjust(arr,i,length)
}
}
function adjust(arr, i, length) {
// 叶子节点就退出
if(2*i+1>length-1){
return
}
let p = i
if(2*i+2 > length-1){
// 一个子节点
if(arr[2*i+1]>arr[i]){
p = 2*i+1
}
}else{
// 两个子节点
if(arr[2*i+1]>arr[2*i+2]){
p = 2*i+1
}else{
p = 2*i+2
}
if(arr[p]<arr[i]){
p = i
}
}
if(p != i){
let temp = arr[i]
arr[i] = arr[p]
arr[p] = temp
adjust(arr,p,length)
}
}
这时候最难的地方已经写好了,剩下的就是对数组不断地建堆,交换首尾,排除末尾,剩下地继续建堆,交换首尾,排除末尾……当要建堆地数组只有一个元素时候,数组就有序了。
我们这里的排除末尾不是真的把末尾的元素给取走或删除,而是用这个length变量的不断减少来让代码无法访问这些数。
function buildHeap(arr, length) {
for(let i = Math.floor(length/2-1);i>=0;i--){
adjust(arr,i,length)
}
}
function adjust(arr, i, length) {
// 叶子节点就退出
if(2*i+1>length-1){
return
}
let p = i
if(2*i+2 > length-1){
// 一个子节点
if(arr[2*i+1]>arr[i]){
p = 2*i+1
}
}else{
// 两个子节点
if(arr[2*i+1]>arr[2*i+2]){
p = 2*i+1
}else{
p = 2*i+2
}
if(arr[p]<arr[i]){
p = i
}
}
if(p != i){
let temp = arr[i]
arr[i] = arr[p]
arr[p] = temp
adjust(arr,p,length)
}
}
function heapSort(arr) {
let len = arr.length
buildHeap(arr, len)
while (len!=1) {
//交换首尾
let temp = arr[0]
arr[0] = arr[len-1]
arr[len-1] = temp
//排除末尾
len--
//剩下的继续建堆
buildHeap(arr,len)
}
}
总结
标签:arr,Heapsort,堆排序,叶子,length,建堆,数组,节点 来源: https://blog.csdn.net/m0_51218245/article/details/121318909