本文將詳細爲你們講解關於堆這種數據結構。學了本章之後咱們會發現,呃呵,原來...名字聽起來高大上的數據結構也就那麼回事。html
後面會持續更新數據結構相關的博文。java
數據結構專欄:https://www.cnblogs.com/hello-shf/category/1519192.htmlgit
git傳送門:https://github.com/hello-shf/data-structure.gitgithub
堆這種數據結構,有不少的實現,好比:最大堆,最小堆,斐波那鍥堆,左派堆,斜堆等。從孩子節點的個數上還能夠分爲二叉堆,N叉堆等。本文咱們從最大二叉堆堆入手看看堆到底是什麼高大上的東東。api
咱們先看看它的定義數組
1 堆是一種徹底二叉樹(不是平衡二叉樹,也不是二分搜索樹哦) 2 堆要求孩子節點要小於等於父親節點(若是是最小堆則大於等於其父親節點)
知足以上兩點性質便可成爲一棵合格的堆數據結構。咱們解讀一下上面的兩點性質數據結構
1,堆是一種徹底二叉樹,關於徹底二叉樹,在我另外一篇博客《二分搜索樹》中有詳細的介紹,要注意堆是一種創建在二叉樹上的數據結構,不一樣於AVL或者紅黑樹是創建在二分搜索樹上的數據結構。this
2,堆要求孩子節點要大於等於父親節點,該定義是針對的最大堆。對於最小堆,孩子節點小於或者等於其父親節點。spa
如上所示,只有圖1是合格的最大堆,圖2不知足父節點大於或者等於孩子節點的性質。圖3不知足徹底二叉樹的性質。3d
前面咱們說堆是一個徹底二叉樹,其中一種在合適不過的存儲方式就是數組。首先從下圖看一下用數組表示堆的可行性。
看了上圖,說明數組確實是能夠表示一個二叉堆的。使用數組來存儲堆的節點信息,有一種自然的優點那就是節省內存空間。由於數組佔用的是連續的內存空間,相對來講對於散列存儲的結構來講,數組能夠節省連續的內存空間,不會將內存打亂。
接下來看看數組到二叉堆的下標表示。將數組的索引設爲 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。
上面分析了數組做爲堆存儲結構的可行性分析。接下來咱們經過數組構建一下堆的基礎結構
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 }
向堆中插入元素意味着該堆的性質可能遭到破壞,因此這是如同向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 }
上面咱們介紹了插入和上浮操做,那刪除和下沉操做將再也不是什麼難題。通常的若是咱們取出堆頂元素,咱們選擇將該數組中的最後一個元素替換堆頂元素,返回堆頂元素,刪除最後一個元素。而後再對該元素作下沉操做 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 }
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 }
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