參考左程雲的視頻
1.完全二叉樹的概念
在瞭解堆排序的最開始,需要明白什麼是完全二叉樹
對於這樣一棵用編號代表節點的樹,若這棵樹的節點嚴格按照圖中的順序填充(不必填滿),即稱爲完全二叉樹
也就是說,除了最後一層之外的每一層都被完全填滿,而最後一層的所有節點,都需要保持從左到右的順序
以上面這個圖舉例:若去掉節點12 13 14 15,該樹滿足完全二叉樹
但是若去掉節點8 其他不動,則不滿足
簡單的說就是從左到右的排列中,不允許出現 “插隊”的節點
2.大根堆的概念
首先我們要有一棵完全二叉樹,這棵完全二叉樹需要滿足以下情況:
在該完全二叉樹的任意一棵子樹中,父節點都是這棵子樹的最大值
仍用上圖舉例(圖中的數字代表序號 而不代表實際值)
在1-15號節點的樹中 1號節點應是1-15號所有節點的最大值
在這棵子樹中 2號節點的值也應該是最大值
在中,4號節點的值也應該是最大值
其他節點間的大小關係不作任何約束
這樣一棵 最大值爲父節點的完全二叉樹 稱爲大根堆
相反爲小根堆
3.由數組來“建立”大根堆
現在我們有數組 5 7 0 6 8
我們要將它搭成一個大根堆,那麼首先要將它變換成一棵完全二叉樹
這裏有一個非常重要的概念:
這棵完全二叉樹並不是真實存在的,它只是我們腦海中的排列方式
我們常見的二叉樹是這種形式來表現的:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
而此時我們需要搭建的完全二叉樹並不是這樣,它的本體仍然是數組,也就是說,樹中不同節點需要用數組下標來表示
我們仍然按照下標從0到4的順序,來填滿這棵完全二叉樹
假設父節點的下標爲 i
那麼它的左子節點爲 2*i+1
它的右子節點爲 2*i+2
對於某個節點,它的父節點的下標即爲 (i-1)/2
節點間的對應關係清楚之後 我們發現這樣一棵完全二叉樹,並不符合大根堆的概念,是因爲我們並沒有按照規則來填滿完全二叉樹
正確的步驟是:
第一步:填入第一個數字 5
第二步:填入第二個數字7
此時7>5 不符合大根堆性質 我們將5與7的位置交換
注意:交換實際是發生在數組中的
之後數組變爲 7 5 0 6 8
樹變爲
第三步:填入第三個數字0
不需要交換
第四步:填入第四個數字6
需要將5和6交換 交換前數組爲:7 5 0 6 8 交換後數組即爲 7 6 0 5 8
最後一步:將8填入
8填入之後需要將6與8交換 交換前數組爲:7 6 0 5 8 交換後數組爲:7 8 0 5 6
但是交換後發現 7和8也需要交換 其實每一次交換完之後 都需要將換上去的父節點 與這個換上去的父節點的父節點進行一次對比
交換前數組:7 8 0 5 6
交換後數組: 8 7 0 5 6
這樣 由數組建立大根堆的步驟就完成了
用代碼來實現也非常簡單:
傳入數組arr 對他的每個元素進行“填入”
for(int i=0;i<arr.length;i++)
{
heapInsert(arr,i);
}
public static void heapInsert(int[] arr,int index)
{
//直到當前節點不再大於它的父節點
while(arr[index]>arr[(index-1)/2])
{
swap(arr,index,(index-1)/2);
index=(index-1)/2;
}
}
4.由大根堆來完成排序
建完大根堆之後的數組爲 8 7 0 5 6
樹中的形式爲:
由於大根堆中父節點最大的性質,此時8一定是數組中最大的元素
由於整棵樹的父節點,在數組中的下標一定是0 所以下標爲0的這個元素即爲數組中的最大值
接下來將這個最大值,與最後一個元素交換
在這個例子中,即將8與6交換
交換後的數組爲 6 7 0 5 8
交換後:數組的最後一個數即爲數組中的最大元素,此時這個數不再參與接下來的排序
那麼如何讓最後的元素不再參與接下來的排序?
在排除之前
在生成大根堆時,是如何判斷X位置沒有元素的?
是因爲X位置 在數組中的下標爲5 而我們數組實際最大下標只到4 數組越界 所以X位置沒有值
那麼我們在將8排除後,同樣可以設置一個指針,這個指針初始指向數組實際的最大下標 即爲4
排除8後,這個指針前移一位 指向3 這個指針就是是否越界的標誌 指向3後,相當於8被排除了
此時數爲上圖
在每一次排除一個元素後,樹都應該做一次檢查,檢查是否符合大根堆的性質,若不符合則進行變換
在上圖中,元素6換到下標0位置後,顯然小於7 ,不符合大根堆性質
此時應該進行交換,通俗的做法是:
從下標0的節點開始,即從整棵樹的父節點開始
若這個父節點有左孩子,那麼去看他是否有右孩子,在父節點/左孩子/右孩子 三者中(可能不存在孩子)選擇出最大的元素的下標
若這個最大元素的下標就是父節點的下標 說明父節點是最大值 不需要調整
其他情況則交換
交換後繼續向下判斷是否符合大根堆性質 直到某個元素沒有左孩子(沒有左孩子就沒有右孩子)
本次調整完成
此時7爲最大值 與數組最後一個元素5交換 並排除 指針前移一位 指向2 數組爲 5 6 0 7 8
樹爲:
之後將5 6 0進行調整 調整後爲 6 5 0
調整完成 交換6與0 排除6 指針前移一位 指向1
此時數組爲0 5 6 7 8 此時看似排序已經完成 但是指針下標並沒有指向0 所以繼續
樹爲
調整後爲
交換5 與 0 排除 5 此時指針下標指向0 排序完成
完整代碼
import java.util.List;
public class Main
{
public static void main(String[] args)
{
int[] arr={5,7,0,6,8};
heapSort(arr);
for(int i=0;i<arr.length;i++)
{
System.out.println(arr[i]);
}
}
public static void heapSort(int[] arr)
{
if(arr==null||arr.length<2)
{
return;
}
for(int i=0;i<arr.length;i++)
{
heapInsert(arr,i);
}
int size=arr.length;
swap(arr,0,--size);
while(size>0)
{
heapify(arr,0,size);
swap(arr,0,--size);
}
}
public static void heapInsert(int[] arr,int index)
{
//建立大根堆 每個元素往上走
while(arr[index]>arr[(index-1)/2])
{
swap(arr,index,(index-1)/2);
index=(index-1)/2;
}
}
public static void heapify(int[] arr,int index,int size)
{
int left=index*2+1;
//有左孩子時
while(left<size)
{
//在左孩子和右孩子中選出較大者
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
//返回當前三個節點中最大的下標
largest=arr[largest]>arr[index] ? largest : index;
//若最大者是自己 則不用調整
if(largest==index)
break;
//某個孩子大於我 所以交換
swap(arr,largest,index);
//每個元素都往下走
index=largest;
left=index*2+1;
}
}
public static void swap(int[] nums,int i,int j)
{
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}