堆的原理和實現

1、前言

  本文將詳細爲你們講解關於堆這種數據結構。學了本章之後咱們會發現,呃呵,原來...名字聽起來高大上的數據結構也就那麼回事。html

 

  後面會持續更新數據結構相關的博文。java

  數據結構專欄:https://www.cnblogs.com/hello-shf/category/1519192.htmlgit

  git傳送門:https://github.com/hello-shf/data-structure.gitgithub

2、堆

  堆這種數據結構,有不少的實現,好比:最大堆,最小堆,斐波那鍥堆,左派堆,斜堆等。從孩子節點的個數上還能夠分爲二叉堆,N叉堆等。本文咱們從最大二叉堆堆入手看看堆到底是什麼高大上的東東。api

 

  2.一、什麼是堆

  咱們先看看它的定義數組

1 堆是一種徹底二叉樹(不是平衡二叉樹,也不是二分搜索樹哦)
2 堆要求孩子節點要小於等於父親節點(若是是最小堆則大於等於其父親節點)

 

  知足以上兩點性質便可成爲一棵合格的堆數據結構。咱們解讀一下上面的兩點性質數據結構

  1,堆是一種徹底二叉樹,關於徹底二叉樹,在我另外一篇博客《二分搜索樹》中有詳細的介紹,要注意堆是一種創建在二叉樹上的數據結構,不一樣於AVL或者紅黑樹是創建在二分搜索樹上的數據結構。this

  2,堆要求孩子節點要大於等於父親節點,該定義是針對的最大堆。對於最小堆,孩子節點小於或者等於其父親節點。spa

  如上所示,只有圖1是合格的最大堆,圖2不知足父節點大於或者等於孩子節點的性質。圖3不知足徹底二叉樹的性質。3d

 

   2.二、堆的存儲結構

  前面咱們說堆是一個徹底二叉樹,其中一種在合適不過的存儲方式就是數組。首先從下圖看一下用數組表示堆的可行性。

 

  看了上圖,說明數組確實是能夠表示一個二叉堆的。使用數組來存儲堆的節點信息,有一種自然的優點那就是節省內存空間。由於數組佔用的是連續的內存空間,相對來講對於散列存儲的結構來講,數組能夠節省連續的內存空間,不會將內存打亂。

  接下來看看數組到二叉堆的下標表示。將數組的索引設爲 i。則:

  左孩子找父節點:parent(i)= (i - 1)/2。好比2元素的索引爲5,其父親節點4的下標parent(2)= (5 - 1)/2 = 2;

  右孩子找父節點:parent(i)= (i-2)/ 2。好比0元素找父節點 (6-2)/2= 2;

  其實能夠將上面的兩種方法合併成一個,即parent(i)= (i - 1)/2;從java語法出發你們能夠發現,整數相除獲得的就是省略了小數位的。因此。。。你懂得。

  同理

  父節點找左孩子:leftChild(i)= parent(i)* 2 + 1。

  父節點找右孩子:rightChild(i) = parent(i)*2 + 2。

 

 3、最大二叉堆的實現

 

  3.一、構建基礎代碼

  上面分析了數組做爲堆存儲結構的可行性分析。接下來咱們經過數組構建一下堆的基礎結構

 1 /**
 2  * 描述:最大堆
 3  *
 4  * @Author shf
 5  * @Date 2019/7/29 10:13
 6  * @Version V1.0
 7  **/
 8 public class MaxHeap<E extends Comparable<E>> {
 9     //使用數組存儲
10     private Array<E> data;
11     public MaxHeap(){
12         data = new Array<>();
13     }
14     public MaxHeap(int capacity){
15         data = new Array<>(capacity);
16     }
17     public int size(){
18         return this.data.getSize();
19     }
20     public boolean isEmpty(){
21         return this.data.isEmpty();
22     }
23 
24     /**
25      * 根據當前節點索引 index 計算其父節點的 索引
26      * @param index
27      * @return
28      */
29     private int parent(int index) {
30         if(index ==0){
31             throw new IllegalArgumentException("該節點爲根節點");
32         }
33         return (index - 1) / 2;//這裏爲何不分左右?由於java中 / 運算符只保留整數位。
34     }
35 
36     /**
37      * 返回索引爲 index 節點的左孩子節點的索引
38      * @param index
39      * @return
40      */
41     private int leftChild(int index){
42         return index*2 + 1;
43     }
44 
45     /**
46      * 返回索引爲 index 節點的右孩子節點的索引
47      * @param index
48      * @return
49      */
50     private int rightChild(int index){
51         return index*2 + 2;
52     }
53 }

 

  3.二、插入和上浮 sift up

  向堆中插入元素意味着該堆的性質可能遭到破壞,因此這是如同向AVL中插入元素後須要再平衡是一個道理,須要調整堆中元素的位置,使之從新知足堆的性質。在最大二叉堆中,要堆化一個元素,須要向上查找,找到它的父節點,大於父節點則交換兩個元素,重複該過程直到每一個節點都知足堆的性質爲止。這個過程咱們稱之爲上浮操做。下面咱們用圖例描述一下這個過程:

  如上圖5所示,咱們向該堆中插入一個元素15。在數組中位於數組尾部。

  如圖6所示,向上查找,發現15大於它的父節點,因此進行交換。

  如圖7所示,繼續向上查找,發現仍大於其父節點14。繼續交換。

  而後還會繼續向上查找,發現小於其父節點19,中止上浮操做。整個二叉堆經過上浮操做維持了其性質。上浮操做的時間複雜度爲O(logn)

  插入和上浮操做的代碼實現很簡單,以下所示。

 1     /**
 2      * 向堆中添加元素
 3      * @param e
 4      */
 5     public void add(E e){
 6         // 向數組尾部添加元素
 7         this.data.addLast(e);
 8         siftUp(data.getSize() - 1);
 9     }
10 
11     /**
12      * 上浮操做
13      * @param k
14      */
15     private void siftUp(int k) {
16         // 上浮,若是大於父節點,進行交換
17         while(k > 0 && get(k).compareTo(get(parent(k))) > 0){
18             data.swap(k, parent(k));
19             k = parent(k);
20         }
21     }

 

   

   3.三、取出堆頂元素和下沉 sift down

  上面咱們介紹了插入和上浮操做,那刪除和下沉操做將再也不是什麼難題。通常的若是咱們取出堆頂元素,咱們選擇將該數組中的最後一個元素替換堆頂元素,返回堆頂元素,刪除最後一個元素。而後再對該元素作下沉操做 sift down。接下來咱們經過圖示看看一下過程。

  如上圖8所示,將堆頂元素取出,而後讓最後一個元素移動到堆頂位置。刪除最後一個元素,這時獲得圖9的結果。

 

  如圖10,堆頂的9元素會分別和其左右孩子節點進行比較,選出較大的孩子節點和其進行交換。很明顯右孩子17大於左孩子15。即和右孩子進行交換。

  如圖11,9節點繼續下沉最終和其左孩子12交換後,再沒有孩子節點。這次過程的下沉操做完成。下沉操做的時間複雜度爲O(logn)

  代碼實現仍然是很是簡單

 1     /**
 2      * 取出堆中最大元素
 3      * 時間複雜度 O(logn)
 4      * @return
 5      */
 6     public E extractMax(){
 7         E ret = findMax();
 8         this.data.swap(0, (data.getSize() - 1));
 9         data.removeLast();
10         siftDown(0);
11         return ret;
12     }
13 
14     /**
15      * 下沉操做
16      * 時間複雜度 O(logn)
17      * @param k
18      */
19     public void siftDown(int k){
20         while(leftChild(k) < data.getSize()){// 從左節點開始,若是左節點小於數組長度,就沒有右節點了
21             int j = leftChild(k);
22             if(j + 1 < data.getSize() && get(j + 1).compareTo(get(j)) > 0){// 選舉出左右節點最大的那個
23                 j ++;
24             }
25             if(get(k).compareTo(get(j)) >= 0){// 若是當前節點大於左右子節點,循環結束
26                 break;
27             }
28             data.swap(k, j);
29             k = j;
30         }
31     }

 

  3.四、Replace和Heapify

  Replace操做呢其實就是取出堆頂元素而後新插入一個元素。根據咱們上面的總結,你們很容易想到。返回堆頂元素後,直接將該元素置於堆頂,而後再進行下沉操做便可。

 1     /**
 2      * 取出最大的元素,並替換成元素 e
 3      * 時間複雜度 O(logn)
 4      * @param e
 5      * @return
 6      */
 7     public E replace(E e){
 8         E ret = findMax();
 9         data.set(0, e);
10         siftDown(0);
11         return ret;
12     }

 

   Heapify操做就比較有意思了。Heapify自己的意思爲「堆化」,那咱們將什麼進行堆化呢?根據其存儲結構,咱們能夠將任意一個數組進行堆化。將一個數組堆化?what?一個個向最大二叉堆中插入不就好了?呃,若是這樣的話,須要對每個元素進行一次上浮時間複雜度爲O(nlogn)。顯然這樣作的話,時間複雜度控制的不夠理想。有沒有更好的方法呢。既然這樣說了,確定是有的。思路就是將一個數組當成一個徹底二叉樹,而後從最後一個非葉子節點開始逐個對飛葉子節點進行下沉操做。如何找到最後一個非葉子節點呢?這也是二叉堆常問的一個問題。相信你們還記得前面咱們說過parent(i) = (child(i)-1)/2。這個公式是不分左右節點的哦,本身能夠用代碼驗證一下,在前面的parent()方法中也有註釋解釋了。那麼最後一個非葉子節點其實就是 (arr.size()-1)/2便可。

  接下來咱們經過圖示描述一下這個過程,假如咱們將以下數組進行堆化

  第一步:轉化爲一棵徹底二叉樹,如圖12所示。

  

  第二步:找到最後一個非葉子節點,如圖13所示。這裏咱們將還未調整的非葉子節點設爲黃色,將即將要調整的置爲綠色。調整完成的置爲綠邊圓。

  

  第三步:下沉,非葉子節點和左右孩子進行比較,選出最大的孩子節點進行交換。交換結果如圖14所示

 

  第四步:找到下一個非葉子節點。

 

  第五步:下沉。

  

  第六步:找到下一個非葉子節點。

 

  第七步:下沉。

 

  第八步:找到下一個非葉子節點。

  第九步:下沉。30節點下沉到56元素的位置,而後繼續下沉,可是發現大於23,下沉結束。

  第十步:找到下一個非葉子節點。

  第十一步:下沉。對17節點進行下沉操做,直到其直到適合本身的位置。

  Heapify的整個過程就完成了。時間複雜度控制在了O(n)。

   代碼實現很是的簡單。

 1     /**
 2      * Heapify
 3      * @param arr
 4      */
 5     public MaxHeap(E[] arr){
 6         data = new Array<>(arr);
 7         for(int i = parent(arr.length - 1); i >= 0; i --){
 8             siftDown(i);
 9         }
10     }

 

 4、完整代碼

  

  1 /**
  2  * 描述:最大堆
  3  *
  4  * @Author shf
  5  * @Date 2019/7/29 10:13
  6  * @Version V1.0
  7  **/
  8 public class MaxHeap<E extends Comparable<E>> {
  9     //使用數組存儲
 10     private Array<E> data;
 11     public MaxHeap(){
 12         data = new Array<>();
 13     }
 14     public MaxHeap(int capacity){
 15         data = new Array<>(capacity);
 16     }
 17 
 18     /**
 19      * Heapify
 20      * @param arr
 21      */
 22     public MaxHeap(E[] arr){
 23         data = new Array<>(arr);
 24         for(int i = parent(arr.length - 1); i >= 0; i --){
 25             siftDown(i);
 26         }
 27     }
 28     public int size(){
 29         return this.data.getSize();
 30     }
 31     public boolean isEmpty(){
 32         return this.data.isEmpty();
 33     }
 34 
 35     /**
 36      * 根據當前節點索引 index 計算其父節點的 索引
 37      * @param index
 38      * @return
 39      */
 40     private int parent(int index) {
 41         if(index ==0){
 42             throw new IllegalArgumentException("該節點爲根節點");
 43         }
 44         return (index - 1) / 2;//這裏爲何不分左右?由於java中 / 運算符只保留整數位。
 45     }
 46 
 47     /**
 48      * 返回索引爲 index 節點的左孩子節點的索引
 49      * @param index
 50      * @return
 51      */
 52     private int leftChild(int index){
 53         return index*2 + 1;
 54     }
 55 
 56     /**
 57      * 返回索引爲 index 節點的右孩子節點的索引
 58      * @param index
 59      * @return
 60      */
 61     private int rightChild(int index){
 62         return index*2 + 2;
 63     }
 64 
 65     /**
 66      * 向堆中添加元素
 67      * 時間複雜度 O(logn)
 68      * @param e
 69      */
 70     public void add(E e){
 71         // 向數組尾部添加元素
 72         this.data.addLast(e);
 73         siftUp(data.getSize() - 1);
 74     }
 75 
 76     /**
 77      * 上浮操做
 78      * 時間複雜度 O(logn)
 79      * @param k
 80      */
 81     private void siftUp(int k) {
 82         // 上浮,若是大於父節點,進行交換
 83         while(k > 0 && get(k).compareTo(get(parent(k))) > 0){
 84             data.swap(k, parent(k));
 85             k = parent(k);
 86         }
 87     }
 88 
 89     /**
 90      * 獲取 index 索引位置的元素
 91      * 時間複雜度 O(1)
 92      * @param index
 93      * @return
 94      */
 95     private E get(int index){
 96         return this.data.get(index);
 97     }
 98 
 99     /**
100      * 查找堆中的最大元素
101      * 時間複雜度 O(1)
102      * @return
103      */
104     public E findMax(){
105         if(this.data.getSize() == 0){
106             throw new IllegalArgumentException("當前heap爲空");
107         }
108         return this.data.get(0);
109     }
110 
111     /**
112      * 取出堆中最大元素
113      * 時間複雜度 O(logn)
114      * @return
115      */
116     public E extractMax(){
117         E ret = findMax();
118         this.data.swap(0, (data.getSize() - 1));
119         data.removeLast();
120         siftDown(0);
121         return ret;
122     }
123 
124     /**
125      * 下沉操做
126      * 時間複雜度 O(logn)
127      * @param k
128      */
129     public void siftDown(int k){
130         while(leftChild(k) < data.getSize()){// 從左節點開始,若是左節點小於數組長度,就沒有右節點了
131             int j = leftChild(k);
132             if(j + 1 < data.getSize() && get(j + 1).compareTo(get(j)) > 0){// 選舉出左右節點最大的那個
133                 j ++;
134             }
135             if(get(k).compareTo(get(j)) >= 0){// 若是當前節點大於左右子節點,循環結束
136                 break;
137             }
138             data.swap(k, j);
139             k = j;
140         }
141     }
142 
143     /**
144      * 取出最大的元素,並替換成元素 e
145      * 時間複雜度 O(logn)
146      * @param e
147      * @return
148      */
149     public E replace(E e){
150         E ret = findMax();
151         data.set(0, e);
152         siftDown(0);
153         return ret;
154     }
155 }

 

 

 

  

  若有錯誤的地方還請留言指正。

  原創不易,轉載請註明原文地址:https://www.cnblogs.com/hello-shf/p/11393655.html 

相關文章
相關標籤/搜索