java实现(3)-堆

引言

堆,我们一般作为二叉堆的一种总称,它是建立在二叉树之上的。在本篇博文中,会详细介绍堆的结构和原理,以至于写出堆的实现。在代码实现中我们主要是针对于插入和删除做一些操作,在删除中我们只考虑删除最小的,而不涉及更深一步的操作。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

场景引入

我们在考虑优先队列的时候会有这样的场景:比如说整个公司都用同一台打印机。一般说来会有队列实现,它遵循FIFO的规则,先提交打印任务的先打印,这无可厚非。但是在实际中,我们希望重要的文件先打印,比如说甲有50页不重要的文件和乙有2页重要文件,甲先提交,这种情况下,我们希望乙能够先打印。FIFO的规则显然不合适。
继续讨论这个问题,如果我们用自定义的链表实现呢?这里可以分为两种情况:
①如果链表是有序的,那么删除最小的元素的时间复杂度是O(1)(如果对时间复杂度不明白的可以查阅相关资料,或者查看我前几篇博文,在这里我详细写过计算方法:http://blog.csdn.net/u012403290/article/details/65631285),但是插入的时间复杂度就是O(N)。
②如果链表是无序的,那么插入定义为插入到最尾部,那么时间复杂度是O(1),但是删除最小的元素时间复杂度就是O(N)。
继续深究一下,如果我用二叉查找树呢?
按照二叉查找树的性质来说,我们插入和删除最小元素的时间复杂度都是O(Log N),相比于链表来说有一定的优化,但是我们要考虑一个问题,频繁的删除最小节点,会导致二叉查找树的退化,也就是说二叉查找树的右子树会比左子树大的多,也有可能会直接退化成链表。

完全二叉树

通俗来说,在除最后一层外,每一层上的节点数均达到最大值,在最后一层上只缺少右边的若干结点。大家可以看下面这张图理解:
这里写图片描述
再说明一下,只能缺少右边的若干节点,并不是可以缺少右子节点。

二叉堆

堆是一颗被完全填满的二叉树,如果没有填满,那么只能是在最后一层,而且在这层上,所有的元素从左到右填入,如果有空缺,只能空缺右边的元素。通俗来说它就是一颗完全二叉树。同时它分为两类描述:
①最小堆
意思就是最小的元素在堆顶,且每一个父节点的值都要小于子节点,下图就是一个最小堆:
这里写图片描述
②最大堆
意思就是最大的在堆顶,且每一个父节点的值都大于子节点,下图就是一个最大堆:
这里写图片描述
我们在代码实现过程中,已最小堆为例。

代码实现

1、描述方式
我们思考二叉堆,发现他不需要用链表来表述,直接用数组就可以表述了,我们尝试把二叉堆从上至下,一层一层平铺成数组。我们把上面的最小堆用数组表示就是:

这里写图片描述

我们对于其进行描述,对于一个二叉堆平铺之后的数组,我们可以发现,任意一个下标元素arr[i],他的左孩子的就是arr[2*i],他的右孩子就是arr[2*i+1],他的父节点就是arr[i/2]。

为什么可以用数组来表述二叉堆?
因为完全二叉树的性质,只能在最后一层的右侧允许缺少节点,而这些节点在数组中处于连续的末端,并不影响前面的所有元素。

2、插入
二叉堆的插入还是很有意思的,一般,我们采用上滤的方式来解决二叉堆的插人:①确认一个可以插入的预插入位置,如果最后一层不满的话,那就插入到最后一层最靠左的那个位置,如果最后一层已满的话,那就新开一层,插入到最左边;②判断把当前数据放入到这个位置是否会破坏二叉堆的性质,如果不破坏,那么插入结束;③如果不符合,那么就需要把这个预插入位置和它的父节点进行兑换;重复②③步骤,直至插入结束。
下面这张图描述了这种插入过程:
这里写图片描述

3、删除
理解了插入的过程,删除其实也不难的。想对应的,我们称这种方法为下滤。在最小堆中,我们知道如果要删除最小的,那么其实就是删除堆顶就可以了。可想而知,那我们删除之后,有必要把整个二叉堆恢复到满足的条件。也就是说:①移除堆顶元素。并指定当前位置为预插入位置,并尝试把最后一个元素(最后一个元素在二叉堆的最后一层的最后一个位置)放到这个②如果不能顺利插入,那么就比较它的孩子,把较小的孩子放入这个预插入位置。③继续处理这个预插入位置,循环②步骤,直至又形成一个完整的二叉堆位置。
下面这张图描述了这种删除最小的过程:
这里写图片描述

代码实现

以下是用代码实现的二叉堆,包含了初始化,插入和删除:

package com.brickworkers;

public class Heap<T extends Comparable<? super T>> {

    private static final int DEFAULT_CAPACITY = 10; //默认容量

    private T[] table; //用数组存储二叉堆

    private int size; //表示当前二叉堆中有多少数据

    public Heap(int capactiy){
        this.size = 0;//初始化二叉堆数据量
        table = (T[]) new Comparable[capactiy + 1];//+1是因为我们要空出下标为0的元素不存储
    }

    public Heap() {//显得专业些,你就要定义好构造器
        this(DEFAULT_CAPACITY);
    }


    //插入
    public void insert(T t){
        //先判断是否需要扩容
        if(size == table.length - 1){
            resize();
        }
        //开始插入
        //定义一个预插入位置下标
        int target = ++size;
        //循环比较父节点进行位置交换
        for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){
            table[target] = table[target/2];//如果满足条件,那么两者交换,知道找到合适位置(上滤)
        }
        //插入数据
        table[target] = t;
        print();

    }


    //删除最小
    //删除过程中,需要重新调整二叉堆(下滤)
    public void deleteMin(){
        if(size == 0){
            throw new IllegalAccessError("二叉堆为空");
        }

        //删除元素
        table[1] = table[size--];

        int target  = 1;//从顶部开始重新调整二叉堆

        int child;//要处理的节点下标
        T tmp = table[ target ];

        for( ; target * 2 <= size; target = child )
        {
            child = target * 2;
            if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小
                child++;
            }
            if( table[ child ].compareTo( tmp ) < 0 ){
                table[ target ] = table[ child ];
                table[child] = null;
            }
            else{
                 break;
            }
        }
        table[ target ] = tmp;

        print();
    }





    //如果插入数据导致达到数组上限,那么就需要扩容
    private void resize(){
          T [] old = table;
          table = (T [])  new Comparable[old.length*2 + 1];//把原来的数组扩大两倍
          for( int i = 0; i < old.length; i++ )
              table[ i ] = old[ i ];        //数组进行拷贝
    }


    //打印数组
    private void print(){
        System.out.println();
        for (int i = 1; i <= size; i++) {
            System.out.print(table[i] + " ");
        }
        System.out.println("二叉堆大小:"+size);
    }


    public static void main(String[] args) {
        Heap<Integer> heap = new Heap<>();

        //循环插入0~9的数据
        for (int i = 0; i < 10; i++) {
            heap.insert(i);
        }

        //循环删除3次,理论上是删除0,1,2 
        for (int i = 0; i < 3; i++) {
            heap.deleteMin();
        }
    }

}


//输出结果:
//
//0 二叉堆大小:1
//
//0 1 二叉堆大小:2
//
//0 1 2 二叉堆大小:3
//
//0 1 2 3 二叉堆大小:4
//
//0 1 2 3 4 二叉堆大小:5
//
//0 1 2 3 4 5 二叉堆大小:6
//
//0 1 2 3 4 5 6 二叉堆大小:7
//
//0 1 2 3 4 5 6 7 二叉堆大小:8
//
//0 1 2 3 4 5 6 7 8 二叉堆大小:9
//
//0 1 2 3 4 5 6 7 8 9 二叉堆大小:10
//
//1 3 2 7 4 5 6 9 8 二叉堆大小:9
//
//2 3 5 7 4 8 6 9 二叉堆大小:8
//
//3 4 5 7 9 8 6 二叉堆大小:7

尾记

这里,对于新手来说有一个小小的规则。对于一个类,代码模块存放顺序一般都是:静态成员变量/常量,构造方法,public方法,private方法。我主要说的是要把你封装起来的private方法放到最后面,因为别人查看你的代码的时候,别人希望最先看到的是你暴露出来的public方法,而不是对他来说无关紧要的private方法。

希望对你有所帮助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章