数据结构--关于堆的构造

这一篇博客主要是关于堆的相关问题。

一丶相关概念

因为二叉树有顺序存储和链式存储,所以所谓堆,通常我们可以看做是一个完全二叉树的数组对象。
其具体定义如下:

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

讲的通俗些,大根堆就是每一个结点的值都大于其左右子树的值,小根堆就是每一个结点的值都小于其左右子树的值。
最后再提点一句:
堆 就 是 一 棵 完 全 二 叉 树 \color{red}{堆就是一棵完全二叉树}

二丶关于堆的构造

关于堆的构造,还是上面讲的那样:

所谓堆,通常我们可以看做是一个完全二叉树的数组对象。

这就意味着,我们完全可以把底层设置为数组并且初始化。

public class MyPriorityQueue {
    Integer[] array;//构造新数组
    int size;//记录有效元素个数

    public MyPriorityQueue(){
        array = new Integer[11];//初始容量大小设置为11
        size = 0;
    }
    //支持手动设置初始数组大小
    public MyPriorityQueue(int k){
        if(k < 0){
            throw new IllegalArgumentException("初始容量异常");
        }
        array = new Integer[k];
        size = 0;
    }
}

有了初始的规模之后。我们接下来就要构造几个最基本的方法,以此来完成对堆的一些操作。
请 注 意 , 我 下 面 所 有 的 堆 都 是 以 小 根 堆 为 例 \color{red}{请注意,我下面所有的堆都是以小根堆为例}

关于堆的向上和向下调整

以下图为例,我这里说的向上和向下调整如下。

在这里插入图片描述
向上调整和向下调整是所有操作的基础,所以在这里先对这两个方法进行书写。

关于向上调整

所谓向上调整,就是从一个数组下标开始,然后不断与大于其值的双亲节点交换位置。

这两个操作都是属于基础操作,所以这里直接给出代码:

	//关于向上调整
    public void shiftUp(int child){
        int parent = (child - 1) / 2;//定义双亲节点
        while(child != 0){
            if(array[child] < array[parent]){//小根堆,如果父母节点值比自己大,那么双方交换
                swap(child,parent);
                child = parent;//交换成功之后,child节点变为原来的parent节点
                parent = (child - 1) / 2;
            }else{
                return;
            }
        }
    }
    //关于交换的方法
    public  void swap(int i,int j){
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

关于向下调整

向下调整和向下调整的基本思路一样,但是是双亲节点和子节点进行比较,如果双亲节点的值大于子节点的值,那么就进行交换。
下面给出代码:

 public void shiftDown(int parent){
        int child = parent * 2+1;//这里直接定义做孩子
        //向下调整首先它肯定有左孩子,并且左孩子不能越界
        while(child < size){
            //如果它有右孩子的值更小,那么就和右孩子进行交换
            if(child + 1 < size && array[child + 1] < array[child]){
                child = child + 1;
            }
            //这是交换的具体过程
            if(array[child] < array[parent]){
                //注意这里的swap方法就是上面的swap方法
                swap(child,parent);
                parent = child;
                child = parent * 2 + 1;
            }
        }
    }

关于插入和删除

其实有了上面的两个方法做支撑,接下来的一切操作都很简单了。

关于删除操作

对于删除操作主要可以分为以下几步

1.数组首元素和末尾元素进行交换
2.数组有效元素减一
3.把首元素向下调整

那么接下来给出代码

//关于删除操作
    public Integer poll(){
        if(isEmpty()){
            return null;
        }
        //保存首元素
        Integer key = array[0];
        //交换首位元素位置
        swap(0,size - 1);
        //有效元素个数size--
        size--;
        //向下调整
        shiftDown(0);
        return key;
    }

    public boolean isEmpty(){
        return size == 0;
    }

关于插入操作

对于插入操作,具体过程如下

1.先扩容
2.数组末尾进行插入
3.有效元素个数加一
4.向上调整

具体代码如下:

 //关于插入操作
    public boolean offer(Integer e){
        //先保证传入参数正确
        if(e == null){
            throw new NullPointerException("传入参数有误");
        }
        //扩容
        ensureCapcity();
        //末尾进行插入
        array[size] = e;
        //有效元素个数加一
        size++;
        //向上调整
        shiftUp(size - 1);
        return true;
    }
    //关于扩容
    public void ensureCapcity(){
        if(array.length == size){
            int newCaptity = size * 2;
            array = Arrays.copyOf(array,newCaptity);
        }
    }

构造一个小根堆

我们要怎样构造一个堆呢?
首先我们要明白,小根堆的性质是根节点比左右子树的值都要小。
那么从这一点开始想,我们要怎样才能让在遍历一遍的情况下就让小根堆形成呢?

那么就要从倒数第一个非叶子结点开始,然后把包含它在内以及之前的所有结点全部向下调整一遍。然后这样就可以保证,从基层开始到顶层,全部都是小根堆的形式。

PS:堆的表现形式可以用完全二叉树来理解。关于完全二叉树的性质可以看我下面这篇博客
关于二叉树的性质

然后接下来给出对应代码:

 public MyPriorityQueue(Integer[] arr){
        // 1. 将arr中的元素拷贝到数组中
        array = new Integer[arr.length];
        for(int i = 0; i < arr.length; ++i){
            array[i] = arr[i];
        }
        size = arr.length;

        // 2. 找当前完全二叉树中倒数第一个叶子节点
        //    注意:倒数第一个叶子节点刚好是最后一个节点的双亲
        //    最后一个节点的编号size-1  倒数第一个非叶子节点的下标为(size-1-1)/2
        int lastLeafParent = (size-2)/2;

        // 3. 从倒数第一个叶子节点位置开始,一直到根节点的位置,使用向下调整
        for(int root = lastLeafParent; root >= 0; root--){
            shiftDown(root);
        }
    }

如果说接下来还要继续往堆里面插元素构建小堆,那么就用上面的插入方法就好了。

三丶补充

关于堆部分操作的时间复杂度

堆向下调整

堆元素向下调整,那么最坏情况是什么?
就是一个堆顶元素最后到了最底层,那么在这个过程中,过程如下:

1.选出左右子树较小的哪一个
2.和双亲节点比较值大小然后交换
3.选出左右子树较小的哪一个
4.和双亲节点比较值大小然后交换


最后一直到最底层

那么在这个过程中,比较次数多少呢?
假设高度是h,那么时间复杂度就是 ㏒ ₂ n \color{red}{㏒₂n} n

建堆的时间复杂度

因为建堆就是一个不断向下调整的过程,如果说有N个元素,时间复杂度就是 N ∗ ㏒ ₂ n \color{red}{N*㏒₂n} Nn?是嘛?
很明显不是,因为不是每一个元素都从堆顶往下调整

第一层,0个节点往下走h -1层
第二层,两个节点往下走h - 2层
第三层,三个节点往下走h - 3层


一直到最底层
PS:n是对应层数,h是总高度
所以可以发现,每层节点数遵循公式

2的n - 1次方

层数遵循

h - n

那么,如果把每层相加得到结果T(n),给它乘2,等到T2(n),用后者减去前者然后化成大O阶可得到结果:

建堆的时间复杂度为O(N)

关于堆算法

堆算法主要是利用堆的性质进行排序,什么意思?
就是堆顶元素,如果是大根堆那么就是整个数组的最小值,如果是大根堆那么就是整个数组的最大值。

交换堆顶元素和最后一位元素,然后把堆顶元素向下调整,同时向下调整的时候假设最后一位元素已经抛弃。不断重复这个过程

此处给出结论:
如 果 是 大 根 堆 , 那 么 就 是 升 序 。 如 果 是 小 根 堆 , 那 么 就 是 降 序 \color{red}{如果是大根堆,那么就是升序。如果是小根堆,那么就是降序}

此处我们就单单针对于升序的大根堆来进行书写。
(注意,因为这里是大根堆,所以上面的代码这里都不能用了)

 //这里是大根堆的向下调整
 public static void shiftDown(int[] array, int size, int parent){
        int child = parent*2+1;

        while(child < size){
            // 找左右孩子中较大的孩子
            if(child+1 < size && array[child+1] > array[child]){
                child += 1;
            }

            // 双亲小于交大的孩子
            if(array[parent] < array[child]){
                swap(array, parent, child);
                parent = child;
                child = parent*2+1;
            }else{
                return;
            }
        }
    }
    
	 public static void heapSort(int[] array){
        // 1. 建堆----升序:大堆    降序:小堆---向下调整
        for(int root = (array.length-2)/2; root >= 0; root--){
            shiftDown(array, array.length, root);
        }


        // 2. 利用堆删除的思想来排序---向下调整
        int end = array.length-1;   // end标记最后一个元素
        while(end != 0){
            swap(array,0,end);
            shiftDown(array, end,0);
            end--;
        }
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值