引言
堆,我們一般作爲二叉堆的一種總稱,它是建立在二叉樹之上的。在本篇博文中,會詳細介紹堆的結構和原理,以至於寫出堆的實現。在代碼實現中我們主要是針對於插入和刪除做一些操作,在刪除中我們只考慮刪除最小的,而不涉及更深一步的操作。筆者目前整理的一些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方法。
希望對你有所幫助。