数据结构-二叉堆(大顶堆&小顶堆)

 

文章目录

 

简介

二叉堆就是一颗二叉树,是一颗完全二叉树,最直观表现一个二叉树左边最多比右边深 1 层,二叉堆我们常常讨论的就是大顶堆和小顶堆,其中大顶堆根结点最大,左右节点依次递归,小顶堆类似

二叉堆算是一种比较重要的数据结构,实际中我们的堆排序就涉及到二叉堆,它也是优先级队列的基础

所以这里我可以说下二叉堆的性质

  • 完全二叉树
    完全二叉树比平衡二叉树条件更严格,平衡二叉树可能左边深一点,也可能右边深一点,但是完全二叉树只能左边深。而且只要一谈到完全二叉树,就应该要直接想到一颗完全二叉树完全可以用数组去实现,而且用数组存储每一层后有这样的规律:(若当前二叉堆结点下标是 currentIndex)

    // 当前结点下标
    int currentIndex;
    
    /* 对应着删除的下沉操作 */
    // 当前结点的左孩子结点下标
    int leftChildIndex = 2 * currentIndex + 1;
    // 当前结点的右孩子结点下标
    int rightChildIndex = 2 * currentIndex + 2;
    
    /* 对应着新增的上浮操作 */
    // 当前结点双亲结点的下标(当前结点不论是双签的左孩子还是右孩子都是满足的)
    int parentsIndex = (currentIndex - 1) / 2;
    

    这个规律几乎成了二叉堆变换的关键

  • 遵循一定的大小规律

    这里讨论的大顶堆和小顶堆遵循一定的大小规律

  • 自行调整的特性

    一般我们讨论二叉堆,都是默认二叉堆的自动调整功能,比如二叉堆的插入后自动调整,删除结点自动调整等。需要注意的是对于插入操作,我们默认从二叉堆的最后面插入,也就是数组的最后插入,对于数组而言这也不叫插入了,其实就是新增;删除操作对于二叉堆我们默认直接删除对顶元素,对于数组而言其实就是移除头部元素,这两种操作有可能使得二叉堆不具有大顶堆或者小顶堆的特性,因此还需要进行一定的二叉堆调整操作

Java 实现

逻辑思路

首先声明二叉堆是一颗完全二叉树,完全二叉树是可以存储在一个数组中,因为存储在数组中的完全二叉树找到其左右孩子结点甚至双亲节点很容易,这得要通过下标去寻找

  • 插入操作

    插入默认从完全二叉树最后面插入,其实就是新增结点,通过上浮操作,最后上浮到不能上浮为止,新的完全二叉堆就成功构建

  • 删除操作

    删除默认删除完全二叉树的根结点,然后我们还需要将最后一个结点直接放在根结点出,然后进行下沉,下沉到不能在变动的时候新的二叉堆就成功构建了

  • 构建二叉堆

    构建二叉堆默认就是把一个无序的完全二叉树通过自我调整,变成一个二叉堆的过程。具体怎么做呢?可以从完全二叉树最后一个非叶子结点开始进行下沉操作,然后依次遍历到所有的非叶子结点,你要是从根结点这个打头的非叶子结点开始也行

由于二叉堆用数组存储,因此遵循的规律如下:(这里重复写一遍)

// 当前结点下标
int currentIndex;

/* 对应着删除的下沉操作 */
// 当前结点的左孩子结点下标
int leftChildIndex = 2 * currentIndex + 1;
// 当前结点的右孩子结点下标
int rightChildIndex = 2 * currentIndex + 2;

/* 对应着新增的上浮操作 */
// 当前结点双亲结点的下标(当前结点不论是双签的左孩子还是右孩子都是满足的)
int parentsIndex = (currentIndex - 1) / 2;

结构图解

堆排序涉及到构建堆,下图就是构建一个大顶堆,从最后一个非叶子结点开始进行下沉操作,循环到根结点也进行下沉操作,最后结束

数据结构-二叉堆(大顶堆&小顶堆)

代码实现

下面是针对于大顶堆的代码

下面代码中,对于插入的结点的代码中,数组已经将结点插入到尾部,代码只需要进行上浮操作

删除结点的代码中,数组已经将指定位置的结点删除,然后将最后一个结点放置于被删除的结点处,然后代码只需要进行下沉操作

// 插入结点(需要将插入到尾部的结点上浮操作,该数组中已经将插入的结点放置于数组尾部)
public void upAdjust(int[] arr) {
    int currentIndex = arr.length - 1;
    int parentsIndex = (currentIndex - 1) / 2;
    // 临时保存插入的结点值
    int tmp = arr[currentIndex];
    // 上浮操作循环
    while (parentsIndex >= 0 && arr[parentsIndex] < tmp) {
        // 单向赋值,无交换
        arr[currentIndex] = arr[parentsIndex];
        currentIndex = parentsIndex;
        parentsIndex = (currentIndex - 1) / 2;
    }
    // 最后将临时储存的结点值赋给 currentIndex 指向的结点值
    arr[currentIndex] = tmp;
}


// 删除结点(需要将指定的结点删除,将尾部结点放在被删除结点处,数组中已放,需要进行下沉操作)
public void downAdjust(int[] arr, int currentIndex) {
    int tmp = arr[currentIndex];
    int childIndex = 2 * currentIndex + 1;
    while (childIndex < arr.length) {
        // 判断是左孩子还是右孩子
        if (childIndex + 1 < arr.length && arr[childIndex] < arr[childIndex+1]) {
            childIndex++;
        }
        // 若发现不满足循环条件即可跳出
        if (tmp >= arr[childIndex]) {
            break;
        }
        // 单向赋值,无交换
        arr[currentIndex] = arr[childIndex];
        currentIndex = childIndex;
        childIndex = 2 * currentIndex + 1;
    }
    arr[currentIndex] = tmp;
}


// 创建二叉堆(创建一个大顶堆,一致有一个完全二叉树的数组)
public void buildHeap(int[] arr) {
    // 实际是 ((arr.length - 1) - 1) / 2
    for (int i = (arr.length - 2) / 2; i >= 0; i++) {
        // 每个非叶子结点都需要做下沉操作,从最后一个开始往前遍历
        downAdjust(arr, i);
    }
}
时间复杂度

满二叉树的结点数是 n,那么其深度应该是 log 以 2 为底 n 的对数

  • 插入操作时间复杂度

    二叉堆是一个完全二叉树,其插入操作是上浮操作,最差情况下每层值进行一次,所以其时间复杂度为 O(logn),最差情况下就是 O(logn),最好情况 O(1),平均时间复杂度表示为 O(logn)

  • 删除操作时间复杂度

    删除是下沉操作,最差情况每层也只用进行一次,所以时间复杂度为 O(logn),最差时间复杂度为 O(logn),最好情况就是 1 次 O(1),平均时间复杂度表示为 O(logn)

  • 构建二叉堆的时间复杂度

    有兴趣的筒子们可以用数学知识证明一下,时间复杂度为 O(n)

一些疑问
  1. 为什么删除操作不能是删除根结点,然后不把最后一个元素移到头部,直接去做筛选元素上浮呢?

    答:因为如果过这么去做,会发现最终结果可能无法构成一个完全二叉树!打个比方如下:

            1
        3		2
            
    4	5        
    

    现在删除 1,不把 5 移动到头部,那么 2 直接上浮占据了 1 的位置,此时 2 的位置置空,那么左边深度要比右边大 2 了,这也就不是一颗完全二叉树了,自然也不能存储数组里去了!

 

上一篇:Flutter 中 IndexedStack 的使用详解 _ Flutter Widgets


下一篇:【PM产品】读点淘(淘宝直播)产品分析有感