先提個問題,完全二叉樹/滿二叉樹,區別?前者是指每一層都是緊湊靠左排列,最後一層可能未排滿,後者是一種特殊的完全二叉樹,
每層都是滿的,即節點總數和深度滿足N=(2^n) -1。堆Heap,一堆蘋果,為了賣相好,越好看的越往上放,就是大頂堆;為了蘋果堆
的穩定,質量越小越往上放,就是小頂堆;堆首先是完全二叉樹,但只確保父節點和子節點大小邏輯,不關心左右子節點的大小關係,
通常是一個可以被看做一棵樹的陣列物件,是個很常見的結構,比如BST物件,都與堆有關係,今天就說下這個重要的資料結構和應用。
作者原創文章,謝絕一切轉載,違者必究!
本文只發表在"公眾號"和"部落格園",其他均屬複製貼上!如果覺得排版不清晰,請檢視公眾號文章。
準備:
Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4
難度: 新手--戰士--老兵--大師
目標:
1.堆的構建和調整演算法
1 優先順序佇列
為理解堆的原理,先看優先順序佇列,它是一種資料結構,插入或者刪除元素的時候,元素會自動排序,(優先順序不是狹義的數值大小,
但為了通俗理解,這裡以字母序為例),通常使用陣列儲存,我們可以按照下圖進行轉換,序號 0 不用:
優先順序佇列的實現(Java版):
public class PriorityQueue<Key extends Character> { /** 儲存元素的陣列 */ private Key[] keys; private int N = 0; public PriorityQueue(int capacity){ // 下標0不用,多分配一個單位 keys = (Key[]) new Character[capacity + 1]; } public Key max(){ return keys[1]; } public void insert(Key e){ N ++; keys[N] = e; swim(N); } public Key delMax(){ Key max = keys[1]; swap(1,N); keys[N] = null; N --; // 讓第一個元素下沉到合適的位置 sink(1); return max; } /** 上浮第k個元素*/ private void swim(int k){ // 比父節點小,即進行交換,直到根 while (k > 1 && less(parent(k),k)){ swap(k,parent(k)); k = parent(k); } } /** 下沉第 k 個元素*/ private void sink(int k){ while(k < N){ int small = left(k); if (right(k) < N && less(right(k),left(k))){ small = right(k); } if (less(k,small)){ swap(k,small); k = small; } } } private void swap(int i,int j){ Key temp = keys[i]; keys[i] = keys[j]; keys[j] = temp; } /** 元素i和j大小比較*/ private boolean less(int i,int j){ // 'a' - 'b' = -1 ; return keys[i].compareTo(keys[j]) > 0; } /** 元素i的父節點*/ private int parent(int i){ return i/2; } /** 元素i的左子節點*/ private int left(int i){ return i * 2; } /** 元素i的右子節點*/ private int right(int i){ return i * 2 + 1; } }
以上程式碼解析:
1 swim 上浮,對於元素k,是否需要上浮,僅需與其父節點比較,大於父節點則交換,迭代直到根節點;
2 sink 下沉,對於元素k,是否需要下沉,需先比較其左右子節點,找出左右子節點中較小者,較小者若比父節點大,則交換,迭代直到末尾元素;
3 insert 插入,先將元素放到陣列末尾位置,再對其進行上浮操作,直到合適位置;
4 delMax 刪除最大值,大根堆,故第一個元素最大,先將首末元素交換,再刪除末尾元素,再對首元素下沉操作,直到合適位置;
總結:以上只是Java簡化版,java.util.PriorityQueue 是JDK原版,客官可自行研究。但設計還是非常有技巧的,值得思考一番,假設 insert 插入
到首位,會導致陣列大量元素移動。delMax 若直接刪除首位最大值,則需要進一步判斷左右子節點大小,並進行先子節點上浮再首元素下沉操作。
有了這個堆結構,就可以進行堆排序了,將待排數全部加入此堆結構,然後依次取出,即成有序序列了!
2 堆排序
如要求不使用上述堆資料結構。思路(升序為例):將陣列構建為一個大頂堆,首元素即為陣列最大值,首尾元素交換;排除末尾元素後調整大頂堆,
則新的首元素即為次最大值,交換首尾並再排除末尾元素;如此迴圈,最後的陣列即為升序排列。
public class HeapSort02 { public static void main(String []args){ int []arr = {2,1,8,6,4,7,3,0,9,5}; sort(arr); System.out.println(Arrays.toString(arr)); } public static void sort(int []arr){ int len = arr.length; // 建立一個大頂堆 for(int i = (int) Math.ceil(len/2 - 1); i >= 0; i--){ //從第一個非葉子結點從下至上,從右至左調整結構 adjustHeap(arr,i,len); } // 交換首尾元素,並重新調整大頂堆 for(int j = len-1;j > 0;j--){ swap(arr,0,j); adjustHeap(arr,0,j); } } /** 迭代寫法*/ public static void adjustHeap(int []arr,int i,int length){ int temp = arr[i]; for (int k = 2*i + 1; k < length; k=k*2 + 1) { // 注意這裡的k + 1 < length // 如果右子節點大於左子節點,則比較物件為右子節點 if (k + 1 < length && arr[k] < arr[k+1]){ k++; } if (arr[k] > temp){ // 不進行值交換 arr[i] = arr[k]; i = k; } else{ break; } } arr[i] = temp; } /** 遞迴寫法*/ private static void adjustHeap2(int[] arr, int i, int len){ int left = 2 * i + 1; int right = 2 * i + 2; int maxIndex = i; // 注意這裡的 left < len if (left < len && arr[left] > arr[maxIndex]){ maxIndex = left; } if (right < len && arr[right] > arr[maxIndex]){ maxIndex = right; } if (maxIndex != i){ swap(arr,i,maxIndex); adjustHeap2(arr,maxIndex,len); } } /** 交換元素 */ public static void swap(int []arr,int a ,int b){ int temp=arr[a]; arr[a] = arr[b]; arr[b] = temp; } }
以上程式碼解析:
1完全二叉樹結構中,如果根節點順序號為 0,總節點數為 N,則最末節點的父節點即為最後一個非葉子節點,順序號為 ceil(N/2 -1),
2 adjustHeap2 為啥使用三個引數,不用中間的引數可以?使用三個引數,是為了進行遞迴呼叫,因為遞迴肯定是縮小計算規模,而這裡的形參arr和len是固定不變的;
3 adjustHeap是非遞迴寫法,不用中間的引數可以?呼叫一在“構建大頂堆”處,可寫為函式體內初始化 i,並形成雙重 for 迴圈;呼叫二在“重新調整大頂堆”處,
可見中間引數為 0,可直接去掉。故回答是可以!但需要調整寫法,且影響該方法複用,這裡直接寫為三個形參的函式更為優雅而已。
4非遞迴寫法理解:類似插入排序思想(依次移動並找到合適的位置再插入),先將 arr[i] 取出,然後此節點和左右子樹進行比較,如子樹更大則子節點上升一層,使
用for迴圈迭代到最終位置,並進行賦值;
以 i=0 為例:
5遞迴方式理解:定位目標元素的左右子樹,若子樹值更大,則進行值交換,且因為子樹發生了變化,故需要對子樹進行遞迴處理;
3 前K個最大的數
在N個數中找出前K個最大的數: 思路:從N個數中取出前K個數,形成一個陣列[K],將該陣列調整為一個小頂堆,則可知堆頂為K個數中最小值,
然後依次將剩餘 N-K 個數與堆頂比較,若大於,則替換掉並調整堆,直到所有元素加入完畢,堆中元素即為目標集合。
public class HeapSort { public static void main(String[] args) { int[] arr = new int[100]; for (int i = 0; i < 100; i++) { arr[i] = i + 1; } // 前10個最大的數 int k = 10; // 構造小頂堆 for (int i = (int) Math.ceil(k/2 - 1); i >= 0; i--) { adjustHeap(arr,i,k); } // 依次比較剩餘元素 for (int i = 10; i < arr.length; i++) { if (arr[i] > arr[0]){ swap(arr,0,i); adjustHeap(arr,0,k); } } // 輸出結果 for (int i = 0; i < 10; i++) { System.out.print(arr[i]+"-"); } } /** 非迭代寫法 ,對arr[i]進行調整 */ private static void adjustHeap(int[] arr,int i,int length){ int temp = arr[i]; for (int k = i * 2 + 1; k < length; k = k * 2 + 1) { // 因第一次迴圈中可能越界,故需要 k+1 < length if (k + 1 < length && arr[k] > arr[k + 1]){ k++; } if (arr[k] < temp){ arr[i] = arr[k]; i = k; } else { break; } } arr[i] = temp; } /** 遞迴寫法 */ private static void adjustHeap2(int[] arr,int i,int length){ int left = i * 2 + 1; int right = i * 2 + 2; int samller = i; if (left < length && arr[left] > arr[samller]){ samller = right; } if (right < length && arr[right] > arr[samller]){ samller = right; } if (samller != i){ swap(arr,i,samller); adjustHeap2(arr,samller,length); } } /** 交換元素 */ public static void swap(int []arr,int a ,int b){ int temp=arr[a]; arr[a] = arr[b]; arr[b] = temp; } }
以上程式碼解析:按照"初始化—構建小頂堆—比較調整—輸出結果"執行。注意for迴圈中,因第一次迴圈中未使用for語句條件判斷,可能越界,故需要 k+1 < length
。
輸出結果如下:
請看官思考,如果需求變為找出N個數中找出前K個最小的數,該如何實現? 建議動腦且動手的寫一遍!因為魔鬼在細節!
全文完!
我近期其他文章:
- 1 Dubbo學習系列之十九(Apollo配置中心)
- 2 聊聊演算法——二分查詢演算法深度分析
- 3 DevOps系列——Jenkins/Gitlab自動打包部署
- 4 DevOps系列——Jenkins私服
- 5 DevOps系列——Gitlab私服
只寫原創,敬請關注