【跟着小蛋刷算法】 什么是堆?如何实现堆排序

概念

堆可以理解为是一种特殊的树,只要满足以下两个条件就可以称之为堆:

  • 是一个完全二叉树
  • 堆中每个节点值都大于等于或者小于等于其子树中每个节点的值

完全二叉树的概念可能有的人有点忘了,这里做下简单描述:

二叉树除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。

每个节点都大于等于子树中每个节点的值的堆,我们叫作“大顶堆”。每个节点值都小于等于子树中每个节点值的堆,我们叫作“小顶堆”。

定义很简单,我来考考大家,下面这几个二叉树是不是堆?

image-20211114201017994

1和2是大顶堆,3是小顶堆,4不是堆,因为它不满足完全二叉树的结构。

怎么去实现一个堆?

这里我用数组来实现堆的结构,用数组的话,如下图所示: image-20211114201420688

通过数组完成了大顶堆的构造,这里面有三个名词需要先解释下,可以辅助我们理解代码。

假设我们的节点存储都是从数组第二个位置开始存储,定义每个节点在数组的位置为i,

其左子节点为:2i, 右子节点为:2i+1。

这个规律通过图不难推导出来,这也是需要重点掌握的,接下来我们就来一步一步用代码实现堆的各种功能。

往堆中插入一个元素

下面的示例我都以大顶堆进行举例讲解

堆化: 往堆中插入一个元素,我们还需要满足堆的两个特性。如果插入后不满足堆的特性,我们就需要进行调整,这个调整的过程我们称之为堆化。

怎么去进行堆化呢,其实就是让新插入的节点和它的父节点进行比较,如果发现不满足子节点小于等于父节点,就互换两个节点值,一直重复这个过程,直到整个堆都满足父子大小关系。

image-20211114211141204

代码实现

public class NewMaxHeap {
    //存储数据,从下标1开始存储
    private int[] datas;
    //堆目前已经存储堆数据个数
    private int count;
    //堆可以存储堆最大数据个数
    private int capacity;
​
    public NewMaxHeap(int capacity) {
        datas = new int[capacity + 1];
        this.capacity = capacity;
    }
    
    public void insert(int data) {
        if (count >= capacity) {
            //堆已满
            return;
        }
        ++count;
        datas[count] = data;
        int i = count;
        /* 不断循环比较父子节点大小,
         *
         * 直到整个堆父子节点大小满足堆的要求
         */
        while (datas[i] > getParent(i)) {
            swap(data, i);
            i = getParent(i);
        }
    }
​
    /**
     * 两个节点的值互换
     *
     * @param index
     * @param parent
     */
    private void swap(int index, int parent) {
        int tmp = datas[index];
        datas[index] = parent;
        datas[parent] = tmp;
    }
​
    /**
     * 根据节点索引位获取父节点索引位
     *
     * @param index 节点索引位
     * @return
     */
    private int getParent(int index) {
        if (index <= 0) {
            throw new IllegalArgumentException("no parent");
        }
        return index / 2;
    }
​
}
复制代码

删除堆顶元素

删除堆顶元素其实就是删除堆最大的元素。如何进行删除呢?

我们可以将堆顶元素值和最后一个节点值进行互换,互换完成后,再利用父子节点对比方法,堆不满足父子节点大小关系的进行互换,不断重复这个过程,直到满足堆堆条件。这个过程其实就是从上往下的堆化方法

image-20211114215606255

代码示例:

 /**
     * 删除堆顶元素
     */
    public void deleteMaxData() {
        if (count == 0) {
            return;
        }
        datas[1] = datas[count];
        --count;
        //自上而下进行堆化
        heapify(datas, count, 1);
    }
    
      /**
     * 自上而下堆化
     *
     * @param i
     */
    private void heapify(int[] datas, int count, int i) {
        while (true) {
            int maxPos = i;
            //比较父节点和左子节点大小
            if (2 * i <= count && datas[i] < datas[2 * i]) {
                maxPos = 2 * i;
            }
            //比较父节点和右子节点大小
            if (i * 2 + 1 <= count && datas[maxPos] < datas[i * 2 + 1]) {
                maxPos = 2 * i + 1;
            }
            //没有变化说明已经堆化结束,跳出循环
            if (maxPos == i) {
                break;
            }
            //节点值互换
            swap(datas, maxPos, i);
            i = maxPos;
        }
    }
复制代码

时间复杂度分析

堆化的过程就是顺着节点所在的路径进行比较交换,因此堆化的时间复杂度跟树的高度是成正比的,也就是 O( log n )。

插入数据和删除堆顶数据主要的核心逻辑就是堆化,所以两者时间复杂度都是 O( log n )。

堆排序

堆排序就是利用堆(都以大顶堆举例)进行排序堆方法。首先将待排序的序列构造成一个大顶堆,那么最大值就是堆顶的根节点。然后将这个根节点和堆的末尾元素进行互换,那么此时末尾元素就是最大值,将剩余的n-1个元素重新构造成一个堆,这样就得到了n个元素的次大值,反复执行,就能得到一个有序序列了。

建堆

实现堆排序的第一步就是将一个带排序的序列构造成一个大顶堆。这里我们采用从上往下的堆化方式,代码如下:

 /**
     * 将一个无序数组构建成堆
     *
     * @param datas 无序数组
     * @param n     元素个数
     */
    private void bulidHeap(int[] datas, int n) {
        for (int i = n / 2; i >= 1; --i) {
          //堆化
            heapify(datas, n, i);
        }
    }
复制代码

假设我们要排序的序列是{0,7,5,19,8,4,1,20,13,16},堆元素从数组下标1开始,因此总共有9个元素,

这个for循环可能大家很疑惑,这个for循环i是从 9/2=4 开始,4->3->2->1的变量变化。为啥不是从1到9或者从9到1,而是从4到1呢?其实通过下面的图我们就可以明白了,它们都是有孩子的节点。

image-20211115225108985

排序

建堆结束后,就要按照大顶堆堆特性来组织完成最终的排序,排序就是不断将堆顶元素和末尾元素进行交换,交换完后对n-1个元素进行重新堆化,接着继续交换,不断重复该过程,最终就完成了排序。

image-20211115225404863

排序代码

 /**
     * 堆排序
     *
     * @param datas 源数据
     * @param n     数据长度
     */
    public void sort(int[] datas, int n) {
        //构建堆
        bulidHeap(datas, n);
        //排序
        int k = n;
        while (k > 1) {
            swap(datas, 1, k);
            --k;
            heapify(datas, k, 1);
        }
    }
复制代码

堆排序复杂度分析

堆排序包括建堆和排序两个操作,建堆的时间复杂度是 O(n) ,排序的时间复杂度是 O(n\log n),所以堆排序整体的时间复杂度就是 O(n\log n)。

总结

本篇文章我们讲解了堆的概念,堆是一种完全二叉树,并且堆每个节点的值都大于等于或小于等于子节点的值。

接着我们讲解了堆中重要的两个操作插入数据和删除堆顶元素,两者其实都用到了堆化的操作。插入数据时我们是从下往上进行对话,删除堆顶元素我们是通过把数组的最后一个元素移到堆顶,然后从上往下堆化。

最后我们讲到了堆的应用,就是进行堆排序,堆排序包含建堆和排序两个过程。建堆就是将一个无序序列通过从上往下堆化的过程完成堆的构建。排序就是不断的迭代将堆顶元素放到堆的末尾,并将堆的元素减1,然后再堆化,不断重复这个过程,最终就形成了有序序列。

堆的知识点就大致讲完了,但是刷算法的路还很长,那就继续坚持,关注小蛋我,一起交流,一起进步,算法的干货会源源不断的带给大家。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员蛋蛋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值