堆排序
-
算法基本思路:
将待排序数组,看成是一个数组形式的完全二叉树(节点在层序遍历中的顺序,即是元素在数组中的顺序),先对数组进行调整,使其成为一个大根堆,或者小根堆。
调整完后,每次将堆顶元素(数组首元素),与最后一个元素交换,然后把最后一个元素从堆中删除(逻辑上删除),将剩余元素进行调整,形成一个新的堆,再把堆顶元素和堆尾元素交换,把堆尾元素删除,再调整剩余元素… 一直到堆为空,即得到一个有序的数组。
若要对数组进行升序排列,则需要构建一个大根堆(每次交换,会把堆顶的最大元素,替换到最后面)
若要进行降序排列,则构建一个小根堆。
-
过程:
-
建堆
从后往前,从最后一个非叶子节点开始,对节点进行向下调整,调整到数组首元素位置。
-
排序
每次将堆顶元素和堆尾元素交换,并将堆尾元素从堆中移除,再对剩余元素进行向下调整,形成新的堆…一直到堆为空,排序结束
-
-
代码
/** 一开始我是从0号位置,依次上浮调整,其实这应该是使用二叉堆实现优先队列时的入队操作 **/ /** 从最后一个非叶子节点开始进行下沉调整,叶子节点无需下沉 **/ /** 假设父节点的数组下标为 n * 那么其左儿子的下标为 2n + 1 * 右儿子下标为 2n + 2 * 那么从后往前数,第一个非叶子节点,肯定是最后一个叶子节点的父节点 * 最后一个叶子节点下标为 arr.length - 1 * 那么其父节点的下标为 (arr.length - 2) / 2 * **/ public void buildHeap(int[] arr){ for (int i = (arr.length - 2) / 2; i >= 0; i--){ adjustDown(arr,i,arr.length - 1); } } /** 对某个节点进行下沉 **/ public void adjustDown(int[] arr,int target,int last){ int value = arr[target]; int cur = target; while (cur <= last){ int leftSon = cur * 2 + 1; int rightSon = leftSon + 1; int max; //取左右儿子中最大的那个 if (rightSon <= last) max = arr[leftSon] > arr[rightSon] ? leftSon : rightSon; else if (leftSon <= last) max = leftSon; else break; //若子节点比父节点大,则父节点下沉 //这里使用单向覆盖,不使用交换,提高效率 if (arr[max] > value){ arr[cur] = arr[max]; cur = max; }else break; } arr[cur] = value; } /** 进行排序 **/ public void heapSort(int[] arr){ //先建堆 buildHeap(arr); for (int i = arr.length - 1; i >= 0; i--){ //将堆顶与堆尾交换 int temp = arr[0]; arr[0] = arr[i]; arr[i] = temp; //逻辑上把i从堆中删除掉后 //i-1为新的堆尾,对新的堆顶元素进行下沉 adjustDown(arr,0,i - 1); } }
堆排的时间复杂度:
更正一下,建堆时的复杂度是O(n),大致推导过程为:假设有一颗满二叉树,高度为h,那么建堆时,从第h-1层往上,每一层需要向下调整。第h-1层的节点,都需要往下调整一层,第h-2层的节点,都需要往下调整二层,… 第1层的节点,需要往下调整h-1层。通过计算得知,所有需调整的节点,向下调整的总次数为2h - 1 - h,而总结点数n = 2h - 1,那么总的调整次数就为 n - log(n) ,故建堆复杂度为O(n)
排序时每次取堆顶元素,再对堆顶元素进行下沉,是O(nlogn)
堆排的平均时间复杂度,最坏时间复杂度都是O(nlogn)
与快排相比:
快排在极端情况下会退化成冒泡,最坏时间复杂度为O(n2)
堆排和快排都是不稳定的。可以想象,一个叶子节点,和它的父节点,值是相同的,但是由于堆顶和最后一个元素交换,最后叶子节点一定会跑到父节点前面,顺序变了。
快排的空间复杂度为O(logn),因为存在递归,或是用栈模拟的递归。而堆排的空间复杂度是O(1),无需使用额外的空间
二叉堆的其他应用
优先队列
优先队列使用二叉堆实现,和堆排序有共通的地方。
不过使用优先队列,入队操作就是将一个新的节点添加到堆中,这个新节点一定是最后一个叶子节点,插入之后,需要对该节点进行向上调整。也就是说,优先队列的建堆过程,是将节点一个一个的插入到堆里,每次插入后向上调整。
而堆排一开始就知道了所有节点,所以是直接从最后的一个非叶子节点往前,每个节点进行向下调整
优先队列的出队操作就是将堆顶元素删除,这和堆排中的排序过程是类似的,不过优先队列的出队,是直接把堆顶元素删掉,然后将堆尾元素补上堆顶的位置,再进行向下调整,堆中是真的少了一个元素;而堆排中,是将堆顶和堆尾元素交换,后从逻辑上删除一个节点,堆中元素实际没有减少,只是进行向下调整时,堆的结束位置会逐渐向前移动,也就是堆的大小逐渐变小。
/** 对某个节点,进行上浮 **/
public void adjustUp(int[] arr,int target){
int cur = target;
int value = arr[target];
while (cur > 0){
int parent = (cur - 1) / 2;
if (arr[parent] > value){
arr[cur] = arr[parent];
cur = parent;
}else
break;
}
arr[cur] = value;
}
求解海量数据的TOP K问题
这是之前在面试中遇到过的一道题目。假设给定2000万个元素,要求用尽可能快的方式,找出其中最大的100个。
大概思路:二叉堆 + 分治归并
可以先将2000万个元素,拆分成2000组,每组1万个元素。每组用这1万个元素,进行建堆,建一个大小为100的大根堆(建堆时超出100范围的节点,下沉后直接扔掉即可)。这样,第一轮就得到了2000个大小为100的大根堆。之后的思路就很容易想到了,对这2000个大根堆进行两两合并,每2个大根堆,合并成一个大小为100的大根堆。这样第二轮就得到1000个大小为100的大根堆。继续这样两两合并下去,最终只剩下一个大小为100的大根堆。这个大根堆里的100个元素,就是最大的100个。