漫画算法-小灰的算法之旅(11)
1. 什么是二叉堆
二叉堆:本质上是一种完全二叉树,它分两个类型。
- 最大堆
- 最小堆
什么是最大堆?
最大堆:最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值。
什么是最小堆?
最小堆:最小堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值。
二叉堆的根节点叫作堆顶。最大堆和最小堆堆特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素。
2. 二叉堆堆自我调整
对于二叉堆,有如下几种操作:
- 插入节点
- 删除节点
- 构建二叉堆
这几种操作都是基于堆的自我调整。所谓堆的自我调整,就是把一个不符合堆性质的完全二叉树,调整为一个堆。以最小堆为例,讲解二叉堆事如何进行自我调整的。
插入节点
- 当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。例如插入一个新节点,值为0.
- 此时,新节点的父节点5比0大,显然不符合最小堆的性质。于是让新节点**“上浮”**,和父节点交换位置。
- 继续用节点0和父节点3做比较,因为0小于3,则让新节点继续**“上浮”**。
- 继续比较,最终新节点0**“上浮”**,到了堆顶位置。
删除节点
二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处于堆顶的节点。例如删除最小堆的堆顶节点1.
此时,为了继续维持完全二叉树的结构,我们把堆堆最后一个节点10临时不到原本堆顶顶的位置。
接下来,让暂处堆顶位置的节点10和它的左、右孩子进行比较,如果左、右孩子节点中最小的一个(显然是节点2)比节点10小,那么让节点10**“下沉”**。
继续让节点10和它的左、右孩子做比较,左、右孩子中最小的是节点7,由于10大于7,让节点10继续**“下沉”**。
这样一来,二叉堆重新得到了调整。
构建二叉堆
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质上就是让所有非叶子节点依次“下沉”。
下面举一个无序完全二叉树的例子,如下图所示。
首先,从最后一个非叶子节点开始,也就是从节点10开始。如果节点10大于它左、右孩子节点中最小的一个,则节点10”下沉“。
接下来轮到节点3,如果节点3大于它左、右孩子节点中最小的一个,则节点3“下沉”。
然后轮到节点1,如果节点1大于它左、右孩子节点中最小的一个,则节点1“下沉”。事实上节点1小于它的左、右孩子,因此不用改变。
接下来轮到节点7,如果节点7大于它左、右孩子节点中最小的一个,则节点7“下沉”。
节点7继续比较,继续“下沉”。
经过上述几轮比较和“下沉”操作,最终每一个节点都小于它的左、右孩子节点,一个无序的完全二叉树就被构建成了一个最小堆。
时间复杂度
插入节点:时间复杂度是O(logn);是单一节点的"上浮",平均交换次数都是堆高度的一半。 空间复杂度O(n)
删除节点:时间复杂度是O(logn);删除操作是针对单节点的"下沉",平均交换次数都是堆高度的一半。空间复杂度O(n)
3. 二叉堆的代码实现
二叉堆虽然是一个完全二叉树,但它的存储方式并不是链式存储,而是顺序存储。
因此,二叉堆的所有节点都存储在数组中。
在数组中,在没有左、右指针的情况下,如何定位一个父节点的左孩子和右孩子呢?
如上图所示,采用数组下标来计算。
假设父节点的下标是parent,那么它的左孩子下标就是2xparent+1;右孩子下标就是2xparent+2.
示例中,节点6包含9和10两个孩子节点,节点6在数组中的下标是3,节点9在数组中的下标是7,节点10在数组的下标是8.
7=3x2+1;
8=3x2+2;
代码实现
/**
* “上浮”调整
* param array. 待调整的堆
*/
public static void upAdjust(int[] array){
int childIndex=array.length-1;
int parentIndex=(childIndex-1)/2;
// temp保存插入的叶子节点值,用于最后的赋值
int temp=array[childIndex];
while(childIndex >0 && temp< array[parentIndex]){
// 无须真正交换,单向赋值即可
array[childIndex]=array[parentIndex];
childIndex=parentIndex;
parentIndex=(parentIndex-1)/2;
}
array[childIndex]=temp;
}
/**
* “下沉”调整
* param array 待调整的堆
* param parentIndex 要“下沉”的父节点
* param length 堆的有效大小
*/
public static void downAdjust(int[] array, int parentIndex,int length){
// temp保存父节点值,用于最后的赋值
int temp =array[parentIndex];
int childIndex=2*parentIndex+1;
while(childIndex<length){
//如果有右孩子,且右孩子小于左孩子的值,则定位到右孩子
if(childIndex+1<length && array[childIndex+1]<array[childIndex]){
childIndex++;
}
//如果父节点小于任何一个孩子的值,则直接跳出
if(temp<=array[childIndex]){
break;
}
//无须真正交换,单向赋值即可
array[parentIndex]=array[childIndex];
parentIndex=childIndex;
childIndex=2*childIndex+1;
}
array[parentIndex]=temp;
}
/**
* 构建堆
* param array 待调整的堆
*/
public static void buildHeap(int[] array){
// 从最后一个非叶子节点开始,依次做"下沉"调整
for(int i=(array.length-2)/2;i>=0;i--){
downAdjust(array,i,array.length);
}
}
public static void main(String[] args){
int[] array=new int[]{1,3,2,6,5,7,8,9,10,0};
upAdjust(array);
System.out.println(Arrays.toString(array));
array=new int[]{7,1,3,10,5,2,8,9,6};
buildHeap(array);
System.out.println(Arrays.toString(array));
}
二叉堆的用处?
二叉堆是实现堆排序以及优先队列的基础。