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方法。

希望對你有所幫助。

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