二叉堆
完全二叉树:
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
二叉堆本质上就是完全二叉树,分为最大堆和最小堆。
什么是最大堆呢?最大堆任何一个父节点的值,都大于等于它左右孩子节点的值。
什么是最小堆呢?最小堆任何一个父节点的值,都小于等于它左右孩子节点的值。
那我们如何构建一个堆呢?
基于堆的自我调整
对于二叉堆,如下有几种操作:
- 插入节点
- 删除节点
- 构建二叉堆
这几种操作都是基于堆的自我调整。
二叉堆虽然是一颗完全二叉树,但它的存储方式并不是链式存储,而是顺序存储。换句话说,二叉堆的所有节点都存储在数组当中。
我们思考一个这样的问题,我们如何把一个数字插入到二叉堆中,插入之后还是二叉堆呢?以小堆为例
插入—O(logn):二叉堆的节点插入,插入位置是完全二叉树的最后一个位置。如果我插入的数字比父节点还小,我就让这个数字向上,父节点向下。不用真正交换,因为它的父节点可能一路下移。
public static void insertAdjust(int[] arr){
int childIndex=arr.length-1;
int parentIndex=(childIndex-1)/2;
int insertVal=arr[childIndex];
while(childIndex>0&&arr[parentIndex]>insertVal){
//无需真正交换,单向赋值即可
arr[childIndex]=arr[parentIndex];
childIndex=parentIndex;
parentIndex=(childIndex-1)/2;
}
arr[childIndex]=insertVal;
}
删除根节点用于堆排序。
注:无论插入还是删除,都是在已经是二叉堆的基础上,并要保证插入和删除完成还是二叉堆。
对于最大堆,删除根节点就是删除最大值;对于最小堆,是删除最小值。把堆存储的最后那个节点移到填在根节点处。再从上而下调整父节点与它的子节点。这样根节点就被删除了。这里的删除根节点和构建二叉堆是有关联的。
//删除根节点,就是对根节点做下沉
public static void deletAdjust(int[] arr){
arr[0]=arr[arr.length-1];
downAdjust(arr,0,arr.length-1);
}
/**
* arr 待调整的堆
* downIndex要下沉的节点索引值
* validLength:数组的有效长度
*/
public static void downAdjust(int[] arr,int downIndex,int validLength){
//把要下沉的值保存在变量downVal
int downVal=arr[downIndex];
int childIndex=downIndex*2+1;
while(childIndex<validLength){
//找到子节点值最小的下标
if(childIndex+1<validLength&&arr[childIndex+1]<arr[childIndex]){
childIndex++;
}
//特别注意,不是判断arr[downIndex]<arr[childIndex],而是和那个特定的值比较
if(downVal<arr[childIndex]){
break;
}
arr[downIndex]=arr[childIndex];
downIndex=childIndex;
childIndex=downIndex*2+1;
}
arr[downIndex]=downVal;
}
构建二叉堆—–O(n)
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质上就是让所有非叶子节点依次下沉。
public static void buildHeap(int[] arr){
for(int i=(arr.length-2)/2;i>=0;i--){
downAdjust(arr,i,arr.length);
}
}
特别说明:
如果仅从代码上直观观察,会得出构造二叉堆的时间复杂度为O(n㏒n)的结果,这个结果是错的,虽然该算法外层套一个n次循环,而内层套一个分治策略下的㏒n复杂度的循环,该思考方法犯了一个原则性错误,那就是构建二叉堆是自下而上的构建,每一层的最大纵深总是小于等于树的深度的,因此,该问题是叠加问题,而非递归问题。
参考:https://www.itcodemonkey.com/article/8660.html
二叉堆的使用—–堆排序
把无序数组构建成二叉堆。
堆顶元素和集合尾部交换后,删除堆顶元素。
public static void heapSort(int[] arr){
//1
buildHeap(arr);
for(int i=arr.length-1;i>=0;i--){
int temp=arr[i];
arr[i]=arr[0];
arr[0]=temp;
//调用downAdjust以确保剩下的i个元素仍然是最小堆。
downAdjust(arr,0,i);
}
}
小根堆每次把最小的元素交换到了最后面,用downAdjust对剩余的n-1个元素,生成一个小根堆。所以小根堆对应从大到小排序。
完整代码:
/**
*
* @author kaixin
*/
public class Heap {
/*
* arr的最后一个元素就是插入的元素,我们向上悬浮调整
*/
public static void insertAdjust(int[] arr){
int childIndex=arr.length-1;
int parentIndex=(childIndex-1)/2;
int insertVal=arr[childIndex];
while(childIndex>0&&arr[parentIndex]>insertVal){
arr[childIndex]=arr[parentIndex];
childIndex=parentIndex;
parentIndex=(childIndex-1)/2;
}
arr[childIndex]=insertVal;
}
/**
* arr 待调整的堆
* downIndex要下沉的节点索引值
* validLength:数组的有效长度
*/
public static void downAdjust(int[] arr,int downIndex,int validLength){
//把要下沉的值保存在变量downVal
int downVal=arr[downIndex];
int childIndex=downIndex*2+1;
while(childIndex<validLength){
//找到子节点值最小的下标
if(childIndex+1<validLength&&arr[childIndex+1]<arr[childIndex]){
childIndex++;
}
if(downVal<arr[childIndex]){
break;
}
arr[downIndex]=arr[childIndex];
downIndex=childIndex;
childIndex=downIndex*2+1;
}
arr[downIndex]=downVal;
}
/**
* 删除根节点元素的调整
* @param arr
*/
public static void deletAdjust(int[] arr){
arr[0]=arr[arr.length-1];
downAdjust(arr,0,arr.length-1);
}
public static void buildHeap(int[] arr){
for(int i=(arr.length-2)/2;i>=0;i--){
downAdjust(arr,i,arr.length);
}
}
public static void heapSort(int[] arr){
buildHeap(arr);
for(int i=arr.length-1;i>=0;i--){
int temp=arr[i];
arr[i]=arr[0];
arr[0]=temp;
downAdjust(arr,0,i);
}
}
public static void main(String[] args) {
int[] array = new int[] {1,3,2,6,5,7,8,9,10,0};
// insertAdjust(array);
// System.out.println(Arrays.toString(array));
// array = new int[] {0, 1, 2, 6, 3, 7, 8, 9, 10, 5};
// deletAdjust(array);
// System.out.println(Arrays.toString(array));
// array = new int[] {7,1,3,10,5,2,8,9,6};
// buildHeap(array);
array = new int[] {7,1,3,10,5,2,8,9,6};
heapSort(array);
System.out.println(Arrays.toString(array));
}
}