Java堆的概念

目录

①什么是堆:

②堆的建立

分析时间复杂度:(建堆)

③堆的插入元素

时间复杂度分析:log(n)

④弹出元素(一般情况下面指的是弹出最顶端的元素)


①什么是堆:

如果对于一棵二叉树,它的每一个根节点的值都比它的左子节点大或者小,那么称这棵二叉树为堆,其中,如果每个节点都比它的子节点大,那么称这棵二叉树为大根堆,如果比它的每个节点都小,那么称之为小根堆。

需要注意的是:根节点的左孩子与右孩子之间没有大小关系。

其中:堆一定是一棵完全二叉树

如图所示为小根堆:

 二叉树的顺序存储:1,2,3,4,5,6,7,8,9,10,11,12为此二叉树的顺序存储,注意:顺序存储仅仅适用于完全二叉树。

      性质一:

      根据二叉树的性质可以得出:如果i为0,那么i表示的节点为根节点,否则,i节点的父亲节点的下标为(i-1)/2;

     性质二:

     如果 2*i+1小于总的节点个数,那么i节点的左孩子下标为2*i+1

②堆的建立

如图所示:

此时为一个刚刚加载的数组,并不构成大根堆的性质

 

思路:为了让每一棵树都成为大根堆,那么需要让每一颗子树都成为大根堆。

首先:需要获取到每一颗二叉树的父亲节点;

那么,先看一下最底下一棵二叉树:

 这一棵二叉树,满足大根堆的性质吗?答案肯定不满足,那么就需要把这一棵二叉树调整成为一个大根堆,即:交换孩子节点与父亲节点:

交换完成之后呢?prennt应当指向新的孩子节点的位置:child指向当前parent的左孩子的位置,也就是2*parent+1的位置。

 此时,这样才算调整完第一棵二叉树。

如何调整下一课二叉树呢?那肯定需要获取到最后一棵二叉树的根节点

 

 也就是索引为3的节点,这一棵二叉树的子节点有两个,分别为49,25,同样的,这一棵二叉树也不满足大根堆的,因为编号为49的这个节点比它的父亲节点的值大,因此也需要调整:调整的思路是这样的:

 此时我们已知parent的索引为3,其中一个child的索引为7,另外一个child的索引为8,此时需要获取值比较大的那一个节点,即:编号为49的节点:

接着,交换parent和child的值,并且,parent也要指向新的child的节点,因为要继续调整新的parent所指向的二叉树:但是由于此时child的索引已经超出了最大的索引,因此不再调整:

同理,继续调整父亲节点为2的二叉树......直到调整父亲节点为0的节点。

所以:

usedSize为原来加载进来的数组的大小。

建堆的思路是这样的:首先确定最后一棵子树的父亲节点的索引int parent=(usedSize-2)/2,以及它的叶子节点的索引:2*parent+1,接着,对这一个父亲节点进行向下调整的算法:

如果该叶子节点存在兄弟节点的时候,需要比较两个兄弟节点的大小,应当把child指向比较大的那一个兄弟节点。

此时,比较较大兄弟节点和父亲节点的大小,如果父亲节点的值比孩子节点的小,那么交换父亲节点和孩子节点的值,接着,parent指向孩子节点,child指向下一棵二叉树的父亲节点:

即:parent=child,child=2*parent+1。如果child的索引超过最大索引,就停止调整。

完成这样的操作之后,即:完成了最后一棵二叉树的调整;但是,整颗二叉树还没有调整完成,需要获取到倒数第二棵二叉树的父亲节点,即:再把此节点进行向下调整的算法...;直到父亲节点为0。

代码实现:

 public int[]elem;

    public int usedSize;

    public static int DEFAULT_SIZE=10;

    public TestHeap(){
        elem=new int[DEFAULT_SIZE];
    }

    /**
     * 初始化一个数组
     * 数组@param array
     */
    public void initElem(int [] array){
          for(int i=0;i<array.length;i++){
              elem[i]=array[i];
              usedSize++;
          }
        System.out.println(usedSize);
    }

    /**
     * 建堆的时间复杂度:O(n)
     */
    public void createHeap(){
        //已知孩子推导父亲
        for(int parent=(usedSize-1-1)/2;parent>=0;parent--){
            //统一的调整方案:
            shiftDown(parent,usedSize);
        }
    }

    public void disPlay(){
        for(int i=0;i<usedSize;i++){
            System.out.println(elem[i]);
        }
    }

    /**
     * 较小索引的值向较大的位置调整:结束的位置为最大的索引usedSize
     * 向下调整:把较小的元素往下面插入
     * 每棵子树的根节点下标@param parent
     * 每棵子树调整的结束位置,不能>len@param usedSize
     */
    private void shiftDown(int parent,int usedSize){
        int child=2*parent+1;
        while(child<usedSize){
            //child下标一定是最大值的下标
            if(child+1<usedSize&&elem[child]<elem[child+1]){
                child++;
            }
            //交换child和parent的值和下标
            if(elem[child]>elem[parent]){
                //先交换值
                int tmp=elem[child];
                elem[child]=elem[parent];
                elem[parent]=tmp;
                //孩子节点往下走
                parent=child;
                child=2*parent+1;
                System.out.println("....parent"+parent+"...child"+child);
            }else{
                //此二叉树不需要交换,跳出循环即可
                break;
            }
        }
    }

分析时间复杂度:(建堆)

 以此类推:假设一棵树的高度为h。第一层的节点,需要调整的次数为(h-1),共有2的0次方个节点,因此第一层的节点需要调整:2的0次方×(h-1);第二层每个节点需要调整的次数为(h-2)......,第二层一共拥有2²个节点......

 这不就是一个等差*等比数列的求和公式吗?

翻翻高中的教材,错位相减法可以得到答案:

因此:T(n)=2^h-1-h;

时间复杂度,通常考虑最坏的情况,因此,这是一棵满二叉树,结合满二叉树的知识可以得到,当一棵树的高度为h的时候,n=2的h次方-1。故:n=log以2为底n+1的对数。

但是,由于n的增加,n远大于 log以2为底(n+1)的对数,因此,建堆的时间复杂度为:O(n);

③堆的插入元素

把新的元素插入到数组的末尾(模拟二叉树的最后一棵子树的节点),接着,把此节点不断向上调整到对应的位置,直到child=0。

何为向上调整?首先:让堆的有效数量+1.

由于默认插入的位置是在队列的尾部。

如果需要向上调整,那么就首先需要获取到和新增节点组成的二叉树的父亲节点

此时 int child=usedSize-1; 

 int parent=(child-1)/2;

这时,需要进行比较如果子节点比父亲节点大,那么就需要交换子节点的值和父亲节点的值,

同时继续把child指向parent,parent指向上一棵二叉树的子节点......直到parent小于0

如图:红色的节点为新增的节点:

由于child比parent大,因此需要交换:

 

此时,child还是比parent大,因此,需要继续交换:

 

 

代码实现:

 /**
     * 插入元素
     * 插入的值@param val
     */
    public void offer(int val){
        if(isFull()){
            //扩容
            elem=Arrays.copyOf(elem,2*elem.length);
        }
        elem[usedSize]=val;
        usedSize++;
        //把此节点向上调整到合适的索引位置
        shiftUp(usedSize-1);
    }
    /**
     * 较大索引的值向较小的位置调整,结束的位置为最小的位置:0
     * 默认原来的二叉树已经是一棵构建好的大根堆了
     * 根节点(向上调整,需要把插入的值尽可能往上调整,找到合适的位置)
     * 插入的时候,
     * 尾插的索引:(usedSize-1)@param child
     */
    private void shiftUp(int child) {
        //找到父亲节点的下标
        int parent=(child-1)/2;
        while(child>0){
            if(elem[child]>elem[parent]){
                //交换元素的值
                int temp=elem[child];
                elem[child]=elem[parent];
                elem[parent]=temp;
                //孩子节点往上走
                child=parent;
                parent=(child-1)/2;
            }else {
                break;
            }
        }
    }

    private boolean isFull() {
        return usedSize==elem.length;
    }

时间复杂度分析:log(n)

 

原因:结合上述的图,可以发现,调整的最坏的情况,为从堆底调整到堆顶,也就是说,调整的次数,最多为这一棵二叉树的高度;结合前面二叉树的基本知识,可以得出,n个节点的高度最高为

h=log以2为底(n+1)的对数。

因此,根据时间复杂度的定义,时间复杂度为log(n);

④弹出元素(一般情况下面指的是弹出最顶端的元素)

思路:把顶部的元素与底部的元素的值进行交换,并且把usedSize--,接着把新的顶部的元素向下调整到合适的位置即可:

代码实现:

/**
     * 一定是删除堆顶的元素
     * 堆顶的元素@return
     */
    public int pop(){
        if(isEmpty()){
            return -1;
        }
        int tmp=elem[0];
        elem[0]=elem[usedSize-1];
        elem[usedSize-1]=tmp;
        usedSize--;
        //保证此堆依然为大根堆
        shiftDown(0,usedSize);
        return tmp;
    }

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

    /**
     * 较小索引的值向较大的位置调整:结束的位置为最大的索引usedSize
     * 向下调整:把较小的元素往下面插入
     * 每棵子树的根节点下标@param parent
     * 每棵子树调整的结束位置,不能>len@param usedSize
     */
    private void shiftDown(int parent,int usedSize){
        int child=2*parent+1;
        while(child<usedSize){
            //child下标一定是最大值的下标
            if(child+1<usedSize&&elem[child]<elem[child+1]){
                child++;
            }
            //交换child和parent的值和下标
            if(elem[child]>elem[parent]){
                //先交换值
                int tmp=elem[child];
                elem[child]=elem[parent];
                elem[parent]=tmp;
                //孩子节点往下走
                parent=child;
                child=2*parent+1;
                System.out.println("....parent"+parent+"...child"+child);
            }else{
                //此二叉树不需要交换,跳出循环即可
                break;
            }
        }
    }
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值