学习笔记--- 比较排序之堆排序
堆排序是一种集合了插入排序与归并排序的优点的排序算法,即有不错的渐近运算上限,又不用占用额外的运行空间。简单的说,它的排序思想如下:从一个数组中选出最大的数,然后在剩余的数里选出最大的数,如此循环,直到数组被穷尽,即可得到有序的数组。
根据这个思路,很容易想到其复杂度:第一步,从n个数里选出最大的数,需要比较n-1次;第二步,从n-1个数里选出最大的数,比较n-2次……那么总共需要比较的次数为:(n-1)+(n-2)+(n-3)+...+2 = O(n^2)。这与插入排序的复杂度并无区别。
而堆排序之所以能够打破这个界限,关键在于它引用的“二叉堆”的概念。
所谓“二叉堆”,就是将一个数组,按照从左到右,从上到下的顺序,将每个元素填入一个二叉树所形成的数据结构。
【此处应有示意图】
接着就可以引入很重要的“最大堆”的概念:“最大堆”表示这样一个二叉堆:任意节点的值总不小于其子节点的值。
而堆排序的主要步骤可以分为三步:
1 将给定的数组经过变换得到一个最大堆。
2 将最大堆的根节点(即最大的数)与末尾的数互换,然后对除了最后一个节点以外的新二叉堆进行维护以形成一个新的最大堆。
3 对剩下的数重复第二步,直到将数组穷尽为止。
第2,3步也就是上面所讲的排序思想的实现。而堆排序的关键在于,经过了第一步的调整之后,接下里你从n个数里找出最大的数不需要再比较n次,而只需要比较lg n次即可。因此第2,3步只需n lg n次运算即可结束。同时可以证明,将任意数组重构成一个最大堆只需要O(n)的运行时间,因此总的运行时间为O(n lg n)。
算法的实现首先需要构架一个维护最大堆的算法heapBuilder,它的作用在于,如果一个节点的左右子树均为最大堆,这个算法将调整这个节点的位置,使得包括这个节点在内的新的二叉堆成为一个最大堆。
第二步构建一个实现二叉堆的算法buildHeap,这个算法通过由下至上,对数组的每个元素执行heapBuilder过程,可以使任意数组成为一个最大堆。
这样就完成了第一步,接下来只需要构建一个完成第三步的过程即可。
以下是简单的JS实现:
function heapBuilder(arr, i){ var left = 2*i+1; var right = 2*i+2; var largest; if(arr[left]>arr[i]&&arr[left]!=undefined) largest = left; else largest = i; if(arr[right]>arr[largest]&&arr[right]!=undefined) largest = right; if(largest != i) { [arr[i],arr[largest]] = [arr[largest],arr[i]]; heapBuilder(arr, largest); } return arr; } function buildHeap(arr){ var bound = Math.floor(arr.length/2); for(var i=bound;i>=0;i--) heapBuilder(arr, i); return arr; } function final(arr){ var temp = buildHeap(arr); var result = []; for(i=temp.length;i>0;i--){ [temp[i-1],temp[0]] = [temp[0],temp[i-1]]; result.push(temp.pop()); heapBuilder(temp, 0); } return result; }
堆的概念的引入,提供了一种“寻找某个数组中最大(小)元素的“。即首先进行一次O(n)的整理,然后再进行O(lg n)次比较即可找出最大(小)数。显然,若是只需要进行常数次查找,这样的方式显然多余,但是如果需要多次查找,乃至于第二步查找的时间超过了O(n),那么采用这种方法就可以极大的减少时间复杂度。
待续……