堆排序
堆排序(Heapsort) 是指利用堆这种数据结构所设计的一种排序算法。 堆积是一个近似完全二叉树的结构, 并同时满足堆积的性质: 即子结点的键值或索引总是小于(或者大于)它的父节点
算法描述
- 步骤1:将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 步骤2:将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 步骤3:由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
动图演示
代码实现
package algorithm.sort;
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] array = {1, 2, 9, 4, 6, 7, 8, 3, 0, 5};
System.out.println("原始数组:" + Arrays.toString(array));
System.out.println("排序后数组:" + Arrays.toString(HeapSort.heapSort(array)));
}
private static int[] heapSort(int[] array) {
int length = array.length;
if (length < 2) {
return array;
}
buildMaxHeap(array, length);
System.out.println("大顶堆:" + Arrays.toString(array));
while (length > 1) {
int temp = array[0];
array[0] = array[length - 1];
array[length - 1] = temp;
length--;
buildMaxHeap(array, length);
}
return array;
}
/**
* 建立最大堆
*
* @param array
* @param length
*/
private static void buildMaxHeap(int[] array, int length) {
for (int i = length / 2 - 1; i >= 0; i--) {
adjustHeap(array, i, length);
}
}
private static void adjustHeap(int[] array, int i, int length) {
int temp = array[i];
int parentIndex = i;
int chaildIndex = 2 * parentIndex + 1;
while (chaildIndex < length) {
if (chaildIndex + 1 < length && array[chaildIndex] < array[chaildIndex + 1]) {
//右叶子比较大
chaildIndex++;
}
if (temp < array[chaildIndex]) {
array[parentIndex] = array[chaildIndex];
parentIndex = chaildIndex;
chaildIndex = 2 * parentIndex + 1;
}else {
break;
}
}
array[parentIndex] = temp;
}
}
package algorithm.sort;
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] array = {1, 2, 9, 4, 6, 7, 8, 3, 0, 5};
System.out.println("原始数组:" + Arrays.toString(array));
System.out.println("排序后数组:" + Arrays.toString(HeapSort.heapSort(array)));
}
private static int[] heapSort(int[] array) {
int length = array.length;
if (length < 2) {
return array;
}
buildMaxHeap(array, length);
System.out.println("大顶堆:" + Arrays.toString(array));
while (length > 1) {
int temp = array[0];
array[0] = array[length - 1];
array[length - 1] = temp;
length--;
popBuildMaxHeap(array, length);
}
return array;
}
private static void popBuildMaxHeap(int[] array, int length) {
int parentIndex = 0;
int temp = array[parentIndex];
int childIndex = 2 * parentIndex + 1;
while (childIndex < length) {
if (childIndex + 1 < length && array[childIndex] < array[childIndex + 1]) {
childIndex++;
}
if (array[childIndex] > temp) {
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = 2 * parentIndex + 1;
} else {
break;
}
}
array[parentIndex] = temp;
}
/**
* 建立最大堆
*
* @param array
* @param length
*/
private static void buildMaxHeap(int[] array, int length) {
for (int i = 0; i < length / 2; i++) {
downAdjustHeap(array, i, length);
}
}
/**
* 通过下沉初始化一个无序数组为大顶堆
*
* @param array
* @param parentIndex
*/
private static void downAdjustHeap(int[] array, int parentIndex, int length) {
int childIndex = 2 * parentIndex + 1;
if (childIndex + 1 < length && array[childIndex] < array[childIndex + 1]) {
childIndex++;
}
if (array[childIndex] > array[parentIndex]) {
int temp = array[parentIndex];
array[parentIndex] = array[childIndex];
array[childIndex] = temp;
}
}
}
算法分析
- 最佳情况:T(n) = O(nlogn)
- 最差情况:T(n) = O(nlogn)
- 平均情况:T(n) = O(nlogn)
堆的应用
堆排序
其实就是用要排序的元素建一个堆(视情况而定是大根堆还是小根堆),然后依次弹出堆顶元素,最后得到的就是排序后的结果了
但是裸的并没有什么用,我们有sort而且sort还比堆排快,所以堆排一般都没有这种模板题,一般是利用堆排的思想,然后来搞一些奇奇怪怪的操作,第2个应用就有涉及到一点堆排的思想
用两个堆来维护一些查询第k小/大的操作
洛谷P1801 黑匣子
利用一个大根堆一个小根堆来维护第k小,并没有强制在线
不强制在线,所以我们直接读入所有元素,枚举询问,因为要询问第k小,所以把前面的第k个元素都放进大根堆里面,然后如果元素数量大于k,就把堆顶弹掉放到小根堆里面,使大根堆的元素严格等于k,这样这次询问的结果就是小根堆的堆顶了(前面k-1小的元素都在大根堆里面了)
记得在完成这次询问后重新把小根堆的堆顶放到大根堆里面就好
中位数
中位数也是这种操作可以解决的一种经典问题,但是实际应用不大(这种操作的复杂度为,然而求解中位数有做法)
Luogu中也有此类例题,题解内也讲的比较清楚了,此处不再赘述,读者可当做拓展练习进行食用
提示:设序列长度为,则中位数其实等价于序列中大的元素
例题:Luogu P1168 中位数
事实上堆在难度较高的题目方面更多的用于维护一些贪心操作,
以降低复杂度,很少会有题目是以堆为正解来出的了,更多的,堆在这些题目中处于“工具”的位置
利用堆来维护可以“反悔的贪心”
题目:Luogu P2949 [USACO09OPEN]工作调度Work Scheduling
这道题的话算是这种类型应用的经典题了
首先只要有贪心基础就不难想出一个解题思路:因为所有工作的花费时间都一样,我们只要尽量的选获得利润高的工作,以及对于每个所选的工作,我们尽量让它在更靠近它的结束时间的地方再来工作
但是两种条件我们并不好维护,这种两个限制条件的题目也是有一种挺经典的做法的:对一个限制条件进行排序,对于另一个限制条件使用某些数据结构来维护(如treap,线段树,树状数组之类),但是这并不在我们今天的讨论范畴QAQ
考虑怎么将这两个条件“有机统一”。
排序的思路是没有问题的,我们可以对每个工作按照它的结束时间进行排序,从而来维护我们的第二个贪心的想法。
那么对于这样做所带来的一个冲突:对于一个截止时间在d的工作,我们有可能把0~d秒全都安排满了(可能会有多个任务的截止时间相同)
怎么解决这种冲突并保证答案的最有性呢?
一个直观的想法就是把我们目前已选的工作全部都比较一下,然后选出一个创造的利润最低的工作(假设当前正在决策的这个工作价值很高),然后舍弃掉利润最低的工作,把这个工作放进去原来的那个位置。(因为我们已经按照结束时间排序了,所以舍弃的那个任务的截止完成时间一定在当前决策的工作的之前)
但是对于大小高达的n,的复杂度显然是无法接受的,结合上面的内容,读者们应该也不难想出,可以使用堆来优化这个操作
我们可以在选用了这个工作之后,将当前工作放入小根堆中,如果堆内元素大于等于当前工作的截止时间了(因为这道题中,一个工作的执行时间是一个单位时间),我们就可以把当前工作跟堆顶工作的价值比较,如果当前工作的价值较大,就可以将堆顶弹出,然后将新的工作放入堆中,给答案加上当前工作减去堆顶元素的价值(因为堆顶元素在放入堆中的时候价值已经累加进入答案了)。如果堆内元素小于截止时间那么直接放入堆中就好
至此,我们已经可以以的效率通过本题
而通过这道题我们也可以发现,只有在优化我们思考出来的贪心操作的时间复杂度时,我们才用到了堆。正如我们先前所说到的,在大部分有一定难度的题目里,堆都是以一个“工具”的身份出现,用于优化算法(大多时候是贪心)的时间复杂度等