堆排序演算法學習小記
1.完全二叉樹的概念
若設二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。
完全二叉樹是由滿二叉樹而引出來的。對於深度為K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度為K的滿二叉樹中編號從1至n的結點一一對應時稱之為完全二叉樹。 (1)所有的葉結點都出現在第k層或k-l層(層次最大的兩層) (2)對任一結點,如果其右子樹的最大層次為L,則其左子樹的最大層次為L或L+l。 一棵二叉樹至多隻有最下面的兩層上的結點的度數可以小於2,並且最下層上的結點都集中在該層最左邊的若干位置上,則此二叉樹成為完全二叉樹,並且最下層上的結點都集中在該層最左邊的若干位置上,而在最後一層上,右邊的若干結點缺失的二叉樹,則此二叉 樹稱為完全二叉樹。
2.堆的概念
堆(英語:heap)是電腦科學中一類特殊的資料結構的統稱。堆通常是一個可以被看做一棵樹的陣列物件。堆總是滿足下列性質:
a.堆中某個節點的值總是不大於或不小於其父節點的值;
b.堆總是一棵完全二叉樹。
如果某個節點的值總是小於其父節點的值,稱為大根堆(a),如果總是大於其父節點的值,稱為小根堆(b)。
3.堆排序原理
a.將排序元素構建成堆(升序採用大頂堆,降序採用小頂堆)
b.取出堆頂元素
c.將剩下元素繼續構造成堆
重複b c 步驟即可實現堆排序,下面以array=[4,6,8,5,9]陣列的升序排序具體說明過程
第一步:將陣列元素對映成完全二叉樹,如上圖
第二步:找出完全二叉樹中序號最大的非葉子節點(這裡比較難理解),這個數+1也是這棵數的非葉子節點數量,下面給出推導過程:
可以分兩種情形考慮:
①樹的最後一個非葉子節點若只有左孩子
②樹的最後一個非葉子節點有左右兩個孩子
完全二叉樹的性質之一是:如果節點序號為i,在它的左孩子序號為2*i+1,右孩子序號為2*i+2。
對於①左孩子的序號為n-1,則n-1=2*i-1,推出i=n/2-1;
對於②左孩子的序號為n-2,在n-2=2*i-1,推出i=(n-1)/2-1;右孩子的序號為n-1,則n-1=2*i+2,推出i=(n-1)/2-1;
很顯然,當完全二叉樹最後一個節點是其父節點的左孩子時,樹的節點數為偶數;當完全二叉樹最後一個節點是其父節點的右孩子時,樹的節點數為奇數。
根據java語法的特徵,整數除不盡時向下取整,則若n為奇數時(n-1)/2-1=n/2-1。
因此對於②最後一個非葉子節點的序號也是n/2-1。
對於上面給出的完全二叉樹結構,根據上面的推導,可以計算出最後一個非葉子節點的 序號 lastNoChildNodeIndex = arr.lenth/2-1=5/2-1=1,即這棵樹中序號為0和1的節點均為非葉子節點。
第三步:開始將這棵完全二叉樹調整為最大堆,從最後一個非葉子節點開始調整,依次往序號較低的葉子節點進行,上面的樹有0,1兩個葉子節點,首先調整序號為1的葉子節點
調整思路:序號為1的葉子節點值為6,它的兩個子節點分別為5和9,明顯,三個值中,最大的值為9,要滿足最大堆,9和6交換。按理說,交換值後,需要考慮序號4即6的位置是否滿足最大堆要求,但是由於序號4處是葉子節點,所以滿足要求。
第四步:調整序號為0的非葉子節點
調整思路:
0號節點的值以及兩個子節點三個值中,明顯9最大,交換4和9的值,此時,被交換過的節點1是非葉子節點,且不滿足最大堆要求,下面繼續調整節點1使其滿足最大堆的要求。
第五步:節點1的三個值中,明顯6的值最大,交換6和4的值。
到這裡一個最大堆就建立起來了。堆頂元素9即為整個陣列的最大值。
第六步:將9和4進行交換,其實就是把9放在樹的末尾節點,9後面就不參與排序了。
第七步:將剩下的節點用上述方式重新調整為最大堆
第八步:將堆頂元素8和最後的子節點元素5交換,此時,第二大元素8產生,交換後不再參與排序。
重複上面上述的構建最大堆,將最大元素交換到樹節點尾,最後得到的結果如下:
到此,完成了所有排序。
4.堆排序java程式碼實現
public class heapSorted { public static void main(String[] args) { int[] arr = {4, 6, 8, 5, 9}; for(int i = 0;i< arr.length;i++){ System.out.println(arr[i]); } arr = sortHeap(arr); for(int i = 0;i< arr.length;i++){ System.out.println(arr[i]); } } private static int[] sortHeap(int[] arr) { int len = arr.length; //構建最大堆 buildMaxHeap(arr, len); for (int i = len - 1; i > 0; i--) { //堆頂元素(序號為0)和當前堆的最後一個節點(序號i)交換 swap(arr, 0, i); //堆元素總數減一,即將當前樹結構最後一個節點排除,不參與下一輪堆結構構建 len--; //重新構建大頂堆 reBuildHeap(arr, 0, len); } return arr; } /** * 構建大頂堆 * @param arr * @param len */ private static void buildMaxHeap(int[] arr, int len) { //(int) Math.floor(len >> 1) = len/2-1 即最大非葉子節點的序號 for (int i = (int) Math.floor(len >> 1); i >= 0; i--) { //從最大非葉子節點開始,依次調整每個非葉子節點,使其滿足最大堆要求(比如說最大非葉子節點序號為3,那麼一次要調整的非葉子節點為3、2、1、0) //調整完畢,最大堆就構建完成 reBuildHeap(arr, i, len); } } /** * 堆調整 * * @param arr * @param i * @param len */ private static void reBuildHeap(int[] arr, int i, int len) { //1. i為傳進來的非子樹節點的序號 //2. len為排序陣列的長度 //3. left為i節點的左子樹節點序號 int left = 2 * i + 1; //4. right為i節點的右子樹節點序號 int right = 2 * i + 2; //5. largest為i、left、right三個節點中值最大的節點序號,暫時假定i節點值最大 int largest = i; //6.如果左節點存在並且左節點的值大於最大值節點(目前為i)的值,則將左節點序號賦給值最大節點序號 if (left < len && arr[left] > arr[largest]) { largest = left; } //7.如果右節點存在並且右節點的值大於最大值節點的值,則將右節點序號賦給值最大節點序號 if (right < len && arr[right] > arr[largest]) { largest = right; } //8.上述兩個判斷,其實就是為了找到三個節點中,值最大的序號的值largest; if (largest != i) { //9.i和largest不相等,說明i,right,left三個節點中,其中一個子節點(left或者right)比父節點i大,因為是大頂堆,此時要交換父節點和最大子節點的值 swap(arr, i, largest); //10.由於交換了父親節點和其中一個子的位置,所以被交換成父親值的子節點可能不滿足最大堆的要求,因此重建該子節點的堆結構 //注意:此時的largest序號處變成了父親節點的值,也就是說,他已經不再是最大值!! reBuildHeap(arr, largest, len); } } /** * 值交換函式 * * @param arr * @param i * @param j */ private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
5.總結
要完全理解堆排序需要深刻搞懂一下幾點
a.什麼是堆
b.堆排序的思想
c.序號最大的非葉子節點的計算原理
d.怎麼將堆頂元素提取出來
f.提取出堆頂元素後,剩下的元素怎麼再構建成堆
備註:文章中的圖都是從網上找的,程式碼也是參考敲了一遍,理解了再敲一遍更容易理解~