流暢閱讀本文的前置條件是:瞭解樹,二叉樹這兩種數據結構。
堆與堆排序
首先要明確:堆排序是指利用大頂堆(或者小頂堆)來實現排序,達到使數據有序的目的。而非是將排序的結果存儲在堆這樣一個數據結構中。
堆
二叉堆:用數組存儲的完全二叉樹,也稱堆。
堆中的元素的存儲特性:下標爲i的元素,其左孩子的下標爲2*i+1,右孩子的下標爲2*i+2。
如下圖所示的存儲在數組中的堆,實質是一個完全二叉樹。
其下標爲0的元素,左孩子(lc)存儲在下標爲1的位置, 右孩子(rc)存儲在下標爲2的位置。
如上圖數組中存儲的完全二叉樹可以用以下樹圖表示:(數節點中存儲的數是數組的下標)
“堆”這個概念很形象的表述了其數據結構。如果覺得不容易理解,可以試着想象一下,在現實生活中,一堆蘋果,肯定是由上至下逐層增多,且中間並無空隙的,因此存儲上用的是空間連續的數組,而非鏈式。而一顆蘋果樹,是有長長的枝杈的,因此,樹的節點間的關係是用引用即指針來實現,可以將指針想象成枝杈,節點想象成果實。
“堆”的概念正是源於堆排序。
“堆”代表的另一個常見概念:垃圾收集存儲機制。
例如在java語言的JVM中,堆內存空間(JavaHeapSpace)就是垃圾回收的主要對象,JVM設計了堆來存儲需要被回收的內存對象數據。
堆頂
即完全二叉樹的根,也即存儲堆數據的數組A的第一個元素A[0]。
小頂堆
小頂堆:完全二叉樹中的每個結點的值都小於或等於者其左右孩子結點的值。因此,根是此樹中的值最小的結點。即A[0]最小。
大頂堆則是每個節點都大於其左右孩子節點,因此根是最大節點。
堆排序
堆排序,就是利用小頂堆或者大頂堆來實現數據的排序。
實現方法(正序-利用小頂堆):
- 將所有數據分爲兩部分,一部分是已經有序的數據,存儲在數組B中;另一部分是還未排好序的無序數據,存儲在數組A中;
- 在堆排序過程中,對於存儲堆元素的數組A有兩個size,一個是數組本身的size,另一個是有效堆數據個數heap_size。因爲,在排序過程中,有序數組B中元素不斷擴充,而構成堆的無序數據的個數則在不斷減少。也就是說,雖然數組A中,A[0…A.length]可能一直都存儲有數據,但是排序過程中,只有A[0…A.heap_size]中存儲的是堆的有效元素。
- 在未排序時,A.size=A.heap_size; 排序過程中,A.size=A.heap_size + B.size;
- 排序前,所有元素都是無序的,全都存儲在數組A中,這時需要將所有元素構建成小頂堆。
- 取數組A中的堆頂元素(最小值),追加到有序數據即數組B的尾部。
- 每輪取出堆頂元素後,將堆的最後一個有效元素(即完全二叉樹的最後一個節點)放置到堆頂位置(即完全二叉樹的根,也即A[0]),這時堆必然已經不是小頂堆了,那就繼續調整堆,使之成爲一個小頂堆
- 即每輪都從無序數據中取出一個最小值,那麼第一輪取得的是所有元素的最小值,放到有序部分中去;第二輪取得的是無序數據中的最小值,也是所有元素中的次小值,再追加到有序數據尾部,即將次小值追加到了最小值之後,這樣就形成了正序排序。
- 直到剩餘無序元素個數爲1
import static java.lang.System.out;
import java.util.*;
public class HeapSort {
public static void main(String[] args){
// 存儲無序數據的數組,堆元素數組
int [] heapArr = new int[10];
// 隨機生成一些待排序整數
Random r = new Random();
for(int i=0;i<heapArr.length;i++){
heapArr[i] = r.nextInt(100);
}
out.println(Arrays.toString(heapArr));
// 有效堆元素在數組中的的最大下標,即剩餘待排序數據的個數
int heap_size=heapArr.length;
// 將待排序元素構成成小頂堆
createMinTopHeap(heapArr);
out.println(new StringBuffer("構建出的小頂堆:").append(Arrays.toString(heapArr)));
// 存儲有序數據的數組
int [] sortedData = new int[heapArr.length];
for(int i=0;i<heapArr.length-1;i++){
//取堆頂元素,即無序數據中的最小值,追加到有序元素數組中
sortedData[heapArr.length-heap_size]=heapArr[0];
out.println(new StringBuffer("sortedData:").append(Arrays.toString(sortedData)));
//將有效堆元素中的最後一個放到剛被取走的堆頂位置
heapArr[0]=heapArr[heap_size-1];
heap_size--;
//堆頂節點有變化,已經不再是小頂堆,需要再次將其調整爲小頂堆
adjust(heapArr,0,heap_size-1);
}
sortedData[heapArr.length-heap_size]=heapArr[0];
out.println(new StringBuffer("sortedData:").append(Arrays.toString(sortedData)));
for(int i=1;i<sortedData.length;i++){
if(sortedData[i]<sortedData[i-1]) {
out.println("不是有序的數據");
}
}
}
/**
構建小頂堆
*/
static void createMinTopHeap(int [] a){
Objects.requireNonNull(a);
// 從最後一個非葉子節點開始,由下層至上層將所有非葉子節點的子樹調整爲小頂堆
// 最後一個元素(下標爲a.length-1)的父節點,即最後一個非葉子節點
// 根據堆的存儲特性:下標爲i的元素,其左孩子下標爲2*i+1,其右孩子下標爲2*i+2。
// 則有,下標爲n的元素,如果n不能能整除2,則說明其是父節點的左孩子,父節點的下標爲(n-1)/2;否則父節點下標爲(n-2)/2
int lastNodeIdx = a.length-1;
int parentOfLastNode = (lastNodeIdx|1)==0 ? ((lastNodeIdx-2)>>1) : ((lastNodeIdx-1)>>1) ;
for(int i=parentOfLastNode; i>=0; i--){
// 將以此節點爲根的的子樹調整爲小頂堆
adjust(a,i,a.length-1);
}
}
/**
調整堆,使之成爲小頂堆
a 存儲堆元素的數組
r 要調整的子樹的根節點的下標
heap_size 數組中實際有效堆元素的最大下標
*/
static void adjust(int [] a,int r,int heap_size){
if(r==heap_size) return;
// 比較 父、左子、右子 三個節點的大小,找出值最小的的節點下標
int left = 2*r+1;
int right = left+1;
int min = r;
if(left <= heap_size){
min = a[left]<a[r]?left:r;
if(right<=heap_size){
min = a[right]<a[min]?right:min;
}
}
if(min!=r){
// 如果父節點不是最小節點,則交換,使父節點最小
swap(a,min,r);
// 交換後,被交換的子節點的位置上的數據有更新,需要將此節點的子樹重新調整成小頂堆
adjust(a,min,heap_size);
}
}
/**
交換i,j兩個位置中的值
*/
public static void swap(int [] a,int i,int j){
if(i!=j){
int tmp = a[i];
a[i]=a[j];
a[j]=tmp;
}
}
}
優先隊列
堆排序的效率相比插入排序仍然略差,但是堆有另外的高效的用途——優先隊列。
在java中,PriorityQueue類實現了優先隊列。(PriorityQueue實現了Queue接口,有add / remove / element操作,)
優先隊列並不要求在某一時間點,隊列中的所有元素都是有序的。只要求在remove或者element操作時,獲得的是所有元素中的最小值或者最大值。
因此,只需在每次更新隊列時(插入、刪除、修改元素的值),將所有元素構建或者調整爲小頂堆、或者大頂堆,那麼接着進行element / remove時,返回的是數組的第一個元素,即爲最小值或者最大值。