二叉堆
1.什么是二叉堆?
二叉堆本质上是一种完全二叉树,它分为两个类型。
- 最大堆—最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值。
- 最小堆—最小堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值。
二叉堆的根节点叫做堆顶。
最大堆和最小堆的特点决定了:最大堆堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素。
2.二叉堆的构建
二叉堆的构建需要依靠二叉堆的自我调整
2.1 二叉堆的自我调整
对于二叉堆,有如下几种操作:
-
-
- 插入节点
- 删除节点
- 构建二叉堆
-
这几种操作都是基于堆的自我调整。所谓堆的自我调整,就是把一个不符合堆性质的完全二叉树,调整成一个堆。
2.1.1 插入节点(以最小堆为例)
当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。新节点与父节点比较大小,小于父节点,则新节点上浮,与父节点交换位置。依次循环比较,知道最小值到达堆顶位置,并且所有父节点都小于或等于它左、右孩子节点。
2.1.2 删除节点
二叉堆删除节点的过程与插入节点的过程正好相反,所删除的是处于堆顶的节点。
需要注意的是,当删除堆顶的节点后,为了继续维持完全二叉树的结构,我们把堆的最后一个节点临时补到原本堆顶的位置,接下来,让堆顶位置的节点与它的左右孩子节点进行比较,来满足最小堆或者最大堆的特性。
2.1.3 构建二叉堆
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质就是让所有非叶子节点依次下沉。
首先,需要从最后一个非叶子节点开始,与它的左右孩子节点进行比较,并按照最大堆或者最小堆的特性依次完成节点的下沉。
2.2 二叉堆的代码实现
首先,我们要明确一点,二叉堆虽然是一个完全二叉树,但是它的存储方式并不是链式存储,二是顺序存储。换句话说,二叉堆的所有节点都存储在数组中。
在数组中,在没有左右指针的情况下,如何定位一个父节点的左孩子和右孩子呢?
假设父节点的下标是parent,那么它左孩子的下标就是2 * parent + 1;右孩子的下标就是2 * parent +2。
简单的代码实现如下:
1 package com.algorithm.test; 2 3 import java.util.Arrays; 4 5 /** 6 * @Author Jack丶WeTa 7 * @Date 2020/7/30 11:13 8 * @Description 二叉堆的代码实现 9 */ 10 public class HeapTest { 11 12 /** 13 * "上浮"调整 14 * @param array 待调整的堆 15 */ 16 public static void upAdjust(int[] array){ 17 int childIndex = array.length - 1; 18 int parentIndex = (array.length - 1) / 2; 19 //temp保存插入的叶子节点的值,用于最后的赋值 20 int temp = array[childIndex]; 21 while (childIndex > 0 && temp < array[parentIndex]) { 22 //无须真正交换,单向赋值即可 23 array[childIndex] = array[parentIndex]; 24 childIndex = parentIndex; 25 parentIndex = (parentIndex - 1) / 2; 26 } 27 array[childIndex] = temp; 28 } 29 30 /** 31 * "下称调整" 32 * @param array 待调整的堆 33 * @param parentIndex 要“下称”的父节点 34 * @param length 堆的有效大小 35 */ 36 public static void downAdjust(int[] array, int parentIndex, int length){ 37 //temp保存父节点的值,用于最后赋值 38 int temp = array[parentIndex]; 39 int childIndex = 2 * parentIndex + 1; 40 while (childIndex < length) { 41 //如果有右孩子,且右孩子小于左孩子的值则定位到右孩子 42 if (childIndex + 1 < length && array[childIndex+1] < array[childIndex]) 43 childIndex++; 44 //如果父节点小于任何一个孩子的值,则直接跳出 45 if (temp < array[childIndex]) 46 break; 47 48 //无须真正交换,单向赋值即可 49 array[parentIndex] = array[childIndex]; 50 parentIndex = childIndex; 51 childIndex = 2 * childIndex + 1; 52 } 53 array[parentIndex] = temp; 54 } 55 56 /** 57 * 构建堆 58 * @param array 待调整的堆 59 */ 60 public static void buildHeap(int[] array){ 61 //从最后一个非叶子节点开始,依次做“下沉”调整 62 for (int i = (array.length - 2) / 2; i >= 0; i--){ 63 downAdjust(array, i, array.length); 64 } 65 } 66 67 public static void main(String[] args) { 68 int[] array = new int[]{1,3,2,6,5,7,8,9,10,0}; 69 upAdjust(array); 70 System.out.println(Arrays.toString(array)); 71 72 array = new int[]{7,1,3,10,5,2,8,9,6}; 73 buildHeap(array); 74 System.out.println(Arrays.toString(array)); 75 } 76 }
堆的插入操作是单一节点的“上浮”,堆的删除操作是单一节点的“下沉”,这两个操作的平均交换次数都是堆高度的一半,所以时间复杂度是0(logn),至于堆的构建,则是O(n)。