淺談堆-Heap(一)

應用場景和前置知識複習

  • 堆排序html

排序咱們都很熟悉,如冒泡排序、選擇排序、希爾排序、歸併排序、快速排序等,其實堆也能夠用來排序,嚴格來講這裏所說的堆是一種數據結構,排序只是它的應用場景之一數組

  • Top N的求解數據結構

  • 優先隊列ide

堆得另外一個重要的應用場景就是優先隊列測試

咱們知道普通隊列是:先進先出優化

而 優先隊列:出隊順序和入隊順序無關;和優先級相關this

實際生活中有不少優先隊列的場景,如醫院看病,急診病人是最優先的,雖然這一類病人可能比普通病人到的晚,可是他們可能隨時有生命危險,須要及時進行治療. 再好比 操做系統要"同時"執行多個任務,實際上現代操做系統都會將CPU的執行週期劃分紅很是小的時間片斷,每一個時間片斷只能執行一個任務,究竟要執行哪一個任務,是有每一個任務的優先級決定的.每一個任務都有一個優先級.操做系統動態的每一次選擇一個優先級最高的任務執行.要讓操做系統動態的選擇優先級最高的任務去執行,就須要維護一個優先隊列,也就是說全部任務都會進入這個優先隊列.spa

 

基本實現

首先堆是一顆二叉樹,這個二叉樹必須知足兩個兩條件操作系統

  1. 這個二叉樹必須是一顆徹底二叉樹,所謂徹底二叉樹就是除了最後一層外,其餘層的節點的個數必須是最大值,且最後一層的節點都必須集中在左側.即最後一層從左往右數節點必須是緊挨着的,不能是中間空出一個,右邊還有兄弟節點.3d

  2. 這個二叉樹必須知足 左右子樹的節點值必須小於或等於自身的值(大頂堆) 或者 左右子樹的節點值必須大於或等於自身的值(小頂堆)

下圖分別是一個大頂堆和小頂堆的示例

 

看到這兩顆二叉樹,咱們首先就能定義出樹節點的結構:

 1 Class Node {  2     //節點自己的值
 3     private Object value;  4     
 5     private Node left;  6     
 7     private Node right;  8     
 9  ....getter and setter 10     
11 }

可是這裏咱們利用徹底二叉樹的性質用數組來構建這棵樹.先從上到下,自左至右的來給樹的每個節點編上號.

以大頂堆爲例

標上編號後,咱們發現每一個節點的左子節點(若是存在)的序號都是其自身的2倍,右子節點(若是存在)的序號是其自身的2倍加1. 相反,若是已知某個節點的序號,父節點的序號是其自身的二分之一(計算機中整型相除,捨棄餘數)下面來用代碼構建一個堆的骨骼

public class MaxHeap { /* * 堆中有多少元素 */
    private int count; /* * 存放堆數據的數組 */
    private Object[] data; public MaxHeap(int capacity) { /* * 由於序號是從1 開始的,咱們不用下標是0的這個位置的數 */
        this.data = new Object[capacity + 1]; } /** * 返回堆中有多少數據 * @return
     */
    public int size() { return count; } /** * 堆是否還有元素 * @return
     */
    public boolean isEmpty() { return count == 0; } }
View Code

骨骼是構建好了,乍一看堆中存放的數據是一個object類型的數據, 父子節點按節點值 沒法比較,這裏再調整一下

 1 public class MaxHeap<T extends Comparable<T>> {  2 
 3     /*
 4  * 堆中有多少元素  5      */
 6     private int count;  7 
 8     /*
 9  * 存放堆數據的數組 10      */
11     private Object[] data; 12     
13     /**
14  * 堆的容量 15      */
16     private int capacity; 17 
18     /**
19  * @param clazz 堆裏放的元素的類型 20  * @param capacity 堆的容量 21      */
22     public MaxHeap(int capacity) { 23         /*
24  * 由於序號是從1 開始的,咱們不用下標是0的這個位置的數 25          */
26         this.data = new Object[capacity + 1]; 27         this.capacity = capacity; 28  } 29 
30     /**
31  * 返回堆中有多少數據 32  * 33  * @return
34      */
35     public int size() { 36         return count; 37  } 38 
39     /**
40  * 堆是否還有元素 41  * 42  * @return
43      */
44     public boolean isEmpty() { 45         return count == 0; 46  } 47 
48     public Object[] getData() { 49         return data; 50  } 51 }

這樣骨架算是相對無缺了,下面實現向堆中添加數據的過程,首先咱們先把上面的二叉樹的形式按標號映射成數組的形式如圖對比(已經說了0號下標暫時不用)

如今這個大頂堆被映射成數組,因此向堆中插入元素,至關於給數組添加元素,這裏咱們規定每新插入一個元素就插在當前數組最後面,也即數組最大標 + 1的位置處.對於一顆徹底二叉樹來講就是插在最後一層的靠左處,若是當前二叉樹是一顆滿二叉樹,則新開闢一層,插在最後一層最左側.可是這樣插入有可能破壞堆的性質. 如插入節點45

 

插入新節點後已經破壞了大頂堆的性質,由於45比父節點17大, 這裏咱們只要把新插入的節點45和父節點17 交換,相似依次比較與父節點的大小作交換便可

第一次交換:

第二次交換:

這裏咱們發現通過兩次交換,已經知足了堆的性質,這樣咱們就完成了一次插入,這個過程,咱們發現待插入的元素至底向頂依次向樹根上升,咱們給這個過程起個名叫shiftUp,用代碼實現即是:

 1 /**
 2  * 插入元素t到堆中  3  * @param t  4      */
 5     public void insert(T t) {  6         //把這個元素插入到數組的尾部,這時堆的性質可能被破壞
 7         data[count + 1] = t;  8         //插入一個元素,元素的個數增長1
 9         count++; 10         //移動數據,進行shiftUp操做,修正堆
11  shiftUp(count); 12 
13  } 14 
15     private void shiftUp(int index) { 16         while (index > 1 && ((((T) data[index]). 17                 compareTo((T) data[index >> 1]) > 0))) { 18             swap(index, index >>> 1); 19             index >>>= 1; 20  } 21  } 22 
23     /**
24  * 這裏使用引用交換,防止基本類型值傳遞 25  * @param index1 26  * @param index2 27      */
28     private void swap(int index1, int index2) { 29         T tmp = (T) data[index1]; 30         data[index1] = data[index2]; 31         data[index2] = tmp; 32     }

這裏有一個隱藏的問題,初始化咱們指定了存放數據數組的大小,隨着數據不斷的添加,總會有數組越界的這一天.具體體如今以上代碼 data[count + 1] = t 這一行

 1    /**
 2  * 插入元素t到堆中  3  * @param t  4      */
 5     public void insert(T t) {  6         //把這個元素插入到數組的尾部,這時堆的性質可能被破壞
 7         data[count + 1] = t;   //這一行會引起數組越界異常  8         //插入一個元素,元素的個數增長1
 9         count++; 10         //移動數據,進行shiftUp操做,修正堆
11  shiftUp(count); 12 
13     }

 

咱們能夠考慮在插入以前判斷一下容量

 1     /**
 2  * 插入元素t到堆中  3  * @param t  4      */
 5     public void insert(T t) {  6         //插入的方法加入容量限制判斷
 7         if(count + 1 > capacity)  8             throw new IndexOutOfBoundsException("can't insert a new element...");  9         //把這個元素插入到數組的尾部,這時堆的性質可能被破壞
10         data[count + 1] = t;   //這一行會引起數組越界異常 11         //插入一個元素,元素的個數增長1
12         count++; 13         //移動數據,進行shiftUp操做,修正堆
14  shiftUp(count); 15 
16     }

 

至此,整個大頂堆的插入已經還算完美了,來一波兒數據測試一下,應該不是問題

可能上面插入時咱們看到有shiftUp這個操做,可能會想到從堆中刪除元素是否是shiftDown這個操做. 沒錯就是shiftDown,只不過是刪除堆中元素只能刪除根節點元素,對於大頂堆也就是剔除最大的元素.下面咱們用圖說明一下.

 

刪除掉根節點,那根節點的元素由誰來補呢. 簡單,直接剁掉原來數組中最後一個元素,也就是大頂堆中最後一層最後一個元素,摘了補給根節點便可,相應的堆中元素的個數要減一

 

最終咱們刪除了大頂堆中最大的元素,也就是根節點,堆中序號最大的元素變成了根節點.

 

此時整個堆不知足大頂堆的性質,由於根節點17比其子節點小,這時,shiftDown就管用了,只須要把自身與子節點交換便可,但是子節點有兩個,與哪一個交換呢,若是和右子節點30交換,30變成父節點,比左子節點45小,仍是不知足大頂堆的性質.因此應該依次與左子節點最大的那個交換,直至父節點比子節點大才可.因此剔除後新被替換的根節點依次下沉,因此這個過程被稱爲shiftDown,最終變成

因此移除最大元素的方法實現:

 1 /**
 2  * 彈出最大的元素並返回  3  *  4  * @return
 5      */
 6     public T popMax() {  7         if (count <= 0)  8             throw new IndexOutOfBoundsException("empty heap");  9         T max = (T) data[1]; 10         //把最後一個元素補給根節點
11         swap(1, count); 12         //補完後元素個數減一
13         count--; 14         //下沉操做
15         shiftDown(1); 16         return max; 17  } 18 
19     /**
20  * 下沉 21  * 22  * @param index 23      */
24     private void shiftDown(int index) { 25         //只要這個index對應的節點有左子節點(徹底二叉樹中不存在 一個節點只有 右子節點沒有左子節點)
26         while (count >= (index << 1)) { 27             //比較左右節點誰大,當前節點跟誰換位置 28             //左子節點的inedx
29             int left = index << 1; 30             //右子節點則是
31             int right = left + 1; 32             //若是右子節點存在,且右子節點比左子節點大,則當前節點與右子節點交換
33             if (right <= count) { 34                 //有右子節點
35                 if ((((T)data[left]).compareTo((T)data[right]) < 0)) { 36                     //左子節點比右子節點小,且節點值比右子節點小
37                     if (((T)data[index]).compareTo((T)data[right]) < 0) { 38  swap(index, right); 39                         index = right; 40                     } else
41                         break; 42 
43                 } else { 44                     //左子節點比右子節點大
45                     if (((T)data[index]).compareTo((T)data[left]) < 0) { 46  swap(index, left); 47                         index = left; 48                     } else
49                         break; 50  } 51             } else { 52                 //右子節點不存在,只有左子節點
53                 if (((T)data[index]).compareTo((T)data[left]) < 0) { 54  swap(index, left); 55                     index = left; 56                 } else
57                     //index 的值大於左子節點,終止循環
58                     break; 59  } 60  } 61     }

 

至此,大頂堆的插入和刪除最大元素就都實現完了.來寫個測試

 1 public static void main(String[] args) {  2         MaxHeap<Integer> mh = new MaxHeap<Integer>(Integer.class, 12);  3         mh.insert(66);  4         mh.insert(44);  5         mh.insert(30);  6         mh.insert(27);  7         mh.insert(17);  8         mh.insert(25);  9         mh.insert(13); 10         mh.insert(19); 11         mh.insert(11); 12         mh.insert(8); 13         mh.insert(45); 14         Integer[] data = mh.getData(); 15         for (int i = 1 ; i <= mh.count ; i++ ) { 16             System.err.print(data[i] + " "); 17  } 18  mh.popMax(); 19         for (int i = 1 ; i <= mh.count ; i++ ) { 20             System.err.print(data[i] + " "); 21  } 22 }
View Code

 

嗯,還不錯,結果跟上面圖上對應的數組同樣.結果卻是指望的同樣,但總感受上面的shiftDown的代碼比shiftUp的代碼要多幾倍,並且看着不少相似同樣的重複的代碼, 看着難受.因而乎想個辦法優化一下. 對我這種強迫症來講,不幹這件事,晚上總是睡不着覺.

思路: 上面咱們不斷的循環條件是這個index對應的節點有子節點.若是節點堆的性質破壞,最終是要用這個值與其左子節點或者右子節點的值交換,因此咱們計算出了左子節點和右子節點的序號.其實否則,咱們定義一個抽象的最終要和父節點交換的變量,這個變量多是左子節點,也多是右子節點,初始化成左子節點的序號,只有在其左子節點的值小於右子節點,且父節點的值也左子節點,父節點纔可能與右子節點,這時讓其這個交換的變量加1變成右子節點的序號便可,其餘狀況則要麼和左子節點交換,要麼不做交換,跳出循環,因此shiftDown簡化成:

 1    /**
 2  * 下沉  3  *  4  * @param index  5      */
 6     private void shiftDown(int index) {  7         //只要這個index對應的節點有左子節點(徹底二叉樹中不存在 一個節點只有 右子節點沒有左子節點)
 8         while (count >= (index << 1)) {  9             //比較左右節點誰大,當前節點跟誰換位置 10             //左子節點的inedx
11             int left = index << 1; 12             //data[index]預交換的index的序號
13             int t = left; 14             //若是右子節點存在,且右子節點比左子節點大,則當前節點可能與右子節點交換
15             if (((t + 1) <= count) && (((T) data[t]).compareTo((T) data[t + 1]) < 0)) 16                 t += 1; 17             //若是index序號節點比t序號的節點小,才交換,不然什麼也不做, 退出循環
18             if (((T) data[index]).compareTo((T) data[t]) >= 0) 19                 break; 20  swap(index, t); 21             index = t; 22  } 23     }

 

嗯,還不錯,這下完美了.簡單多了.其餘還有待優化的地方留在下篇討論

總結

  • 首先複習了堆的應用場景,具體的應用場景代碼實現留在下一篇.

  • 引入堆的概念,性質和大頂堆,小頂堆的概念,實現了大頂堆的元素添加和彈出

  • 根據堆的性質和彈出時下沉的規律,優化下沉方法代碼.

  • 下一篇優化堆的構建,用代碼實現其應用場景,如排序, topN問題,優先隊列等

 

 

原文出處:https://www.cnblogs.com/blentle/p/10941119.html

相關文章
相關標籤/搜索