堆
堆(heap)是一类特殊的数据结构,通常可以被看做一棵树的数组对象,也就是顺序存储的树
不清楚顺序存储的可以看:数据结构 - 解析二叉树的顺序存储
将树结构用数组存储:
堆是具有以下性质的完全二叉树:
- 每个结点的值都大于或等于左右子结点的值称为大顶堆,根据完全二叉树的性质,可以推出
arr[i]>=arr[i*2+1],arr[i]>=arr[i*2+2]
- 每个结点的值都小于或等于左右子结点的值称为小顶堆,根据完全二叉树的性质,可以推出
arr[i]<=arr[i*2+1],arr[i]<=arr[i*2+2]
注意:堆并没有限制左右子结点的大小关系,是简化版的二次查找树
Java解析实现大顶堆
这样一个堆的树结构:
对应的数组对象(层次遍历):
如何实现大顶堆:
- 找到最后一个非叶子结点
arr.length/2-1
,当前为3,即元素8,从左至右,从下至上进行调整,也就是调整该非叶子结点为父结点的子树
- 先比较左右子结点,找到最大的结点,再比较该结点与父结点的值的大小,如果该结点大,交换位置
- 指针前移,调整结点6所在子树
明显,也是需要交换值的:
- 指针前移,调整结点3所在子树,因为我们是从下到上调整,实际上也只是比较[3,66,10]的值
明显,也需要调整:将66与3交换位置,我们会发现交换完位置后,结点3所在子树是不符合大顶堆要求的
还需要交换结点3与13,这在我们的代码中同样需要考虑到
- 指针前移,调整结点1所在子树
这已经是最后的非叶子结点,最终大顶堆为:其中同样遇到了上面的情况,需要多次调整
Java实现:
/**
* @param arr 待排序的数组
* @param length 数组长度
* @param i 表示非叶子节点在数组中的索引
* 完成将对应的非叶子结点调整成大顶堆,递归完成大顶堆
*/
public static void adjustHeap(int[] arr,int length,int i){
//取出当前元素值,保存
int temp = arr[i];
//k = i * 2 +1说明k是i结点的左子结点
for (int k = i * 2 +1;k < length;k = k * 2 +1){
if (k + 1 < length && arr[k] < arr[k + 1]){
//左子结点小于右子结点的值,指向右子结点
k++;
}
if (arr[k] > temp){
//子结点大于父结点,将值赋值给父结点
arr[i] = arr[k];
//将k赋值给i,用于后续交换值
i = k;
}else {
break;
}
}
//当for循环结束,以i为父结点的子树的最大值已经在i点
//如果运行了for循环内的赋值,将父结点值赋值给对应的子结点
arr[i] = temp;
}
其中在for循环内,将子结点值赋值给父结点,并移动指针i,直到for循环结束才将父结点的值赋值给arr[i]
,这样就可以处理步骤4、5的多次调整
接下来就需要设计堆排序
堆排序
具体的思路: 对于已经调整好的大顶堆、小顶堆,我们可以知道根结点是最大值(最小值),且在数组的首位,将堆顶元素与末尾元素交换,使末尾元素最大,然后重新调整堆,再将堆顶元素与末尾元素交换,得到第二大的数据,循环直到数组完成
完整堆排序Java代码
堆排序的实现:每次对于调整好后的大顶堆,交换堆顶与末尾元素,然后继续调整堆
package com.company.sort;
import java.util.Arrays;
import java.util.Date;
/**
* @author zfk
* 堆排序
*/
public class HeapSort{
public static void main(String[] args) {
//假设数组升序排序:大顶堆
int[] arr = {1,3,6,8,10,14,88,66,13};
System.out.println("初始数组:"+Arrays.toString(arr));
heapSort(arr);
}
/**
* @param arr 要排序的数组
* 堆排序方法
*/
public static void heapSort(int[] arr){
System.out.println("=== 堆排序 ===");
int length = arr.length;
int temp;
//调整得到大顶堆
for (int i = length / 2 - 1;i >= 0;i--){
adjustHeap(arr,length,i);
}
for (int j = length - 1;j > 0;j--){
//交换大顶堆最大值与最后的结点
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
//进行调整大顶堆
//因为是取走最大值,并将数组末尾值放到首位,再次调整大顶堆从首位开始
adjustHeap(arr,j,0);
}
System.out.println("排序后数组:" + Arrays.toString(arr));
}
/**
* @param arr 待排序的数组
* @param length 数组长度
* @param i 表示非叶子节点在数组中的索引
* 完成将对应的非叶子结点调整成大顶堆,递归完成大顶堆
*/
public static void adjustHeap(int[] arr,int length,int i){
//取出当前元素值,保存
int temp = arr[i];
//k = i * 2 +1说明k是i结点的左子结点
for (int k = i * 2 +1;k < length;k = k * 2 +1){
if (k + 1 < length && arr[k] < arr[k + 1]){
//左子结点小于右子结点的值,指向右子结点
k++;
}
if (arr[k] > temp){
//子结点大于父结点,将值赋值给父结点
arr[i] = arr[k];
//将k赋值给i,用于后续交换值
i = k;
}else {
break;
}
}
//当for循环结束,以i为父结点的子树的最大值已经在i点
//如果运行了for循环内的赋值,将父结点值赋值给对应的子结点
arr[i] = temp;
}
}
这里完成了大顶堆排序,小顶堆大致相同
速度测试
将80000个随机0~80000大小的数据进行堆排序
public static void main(String[] args) {
int[] arr2 = new int[80000];
for (int i = 0; i < arr2.length; i++) {
//随机生成80000内的整数
arr2[i] = (int) (Math.random() * 80000);
}
Date dataBefore = new Date();
heapSort(arr2);
Date dateAfter = new Date();
System.out.println("消耗了:"+(dateAfter.getTime()-dataBefore.getTime())+"ms");
}
可以看到很快:
时间复杂度
堆排序的时间复杂度,主要在初始化堆与交换最大值后重建堆
- 初始化堆的时间复杂度为O(n)
初始化堆也就是heapSort方法中的for循环adjustHeap方法:从倒数第一个非叶子结点,从下而上建堆,倒数第二层的结点在调整的过程中只需要比较一次,倒数第三层需要比较两次。。。
n为堆结点数,k为堆层数,堆是完全二叉树
只有根结点需要比较 logn 次(logn 向下取整),层数k的满二叉树总结点数为
2^k -1,近似计算
堆排序中建堆过程时间复杂度O(n)怎么来的? - 吴献策的回答 - 知乎
https://www.zhihu.com/question/20729324/answer/509924802
- 交换最大值后重建堆
循环n-1次建堆,每次都是从根结点开始,且每次堆大小-1,最多交换k(层数)-1次,即每次调整时间为logn
总时间:log n*(n-1) = nlogn - logn 约为 nlogn
堆排序:O(n)+O(nlogn) 约为 O(nlogn)