概念:
堆排序是指堆積樹(二叉樹)這種數據結構所設計的一種排序算法,它是選擇排序的一種。可以利用數組的特點快速定位指定索引的元素。堆分為大根堆和小根堆,是完全二叉樹。大根堆的要求是每個節點的值都不大於其父節點的值。
即:
A[PARENT[i]] >= A[i]。在數組的非降序排序中,需要使用的就是大根堆,因為根據大根堆的要求可知,最大的值一定在堆頂。
定義:
n個關鍵字序列 K1,K2,...Kn 稱為(Heap),當且僅當該序列滿足如下性質(簡稱為堆性質):
Ki <= K(2i)且Ki <= K(2i+1)(1 <= i <= n/2),當然,這是小根堆,大根堆則換成>=號。//K(i)相當於二叉樹的非葉子節點,K(2i)則是左子節點,K(2i+1)是右子節點
若將此序列所存儲的向量R[1..n]看作是一棵完全二叉樹的存儲結構,則堆實質上是滿足如下性質的完全二叉樹:
樹中任一非葉子節點的關鍵字均不大於(或不小於)其左右孩子(若存在)節點的關鍵字。
堆分為大根堆和小根堆:根節點(亦稱為“堆頂”)的關鍵字是堆里所有節點關鍵字中最小者的堆稱為“小根堆”,又稱“最小堆”。根節點(亦稱為“堆頂”)的關鍵字是堆里所有節點關鍵字中的最大者的,稱為“大根堆”,又稱“最大堆”。
注意:a堆中任一子樹亦是堆。b以上討論的堆實際上是二叉堆(Binary Heap),類似的可定義K叉堆。
高度:
堆可以被看成是一棵樹,節點在堆中的高度可以被定義為從本節點到葉子節點的最長簡單下降路徑上邊的數目;定義堆的高度為樹根的高度。我們將看到,堆結構上的一些基本操作的運行時間至多是與樹的高度成正比,為O(lgn)。
排序過程:
堆排序利用了大根堆(或小根堆)堆頂記錄的關鍵字最大(或最小)這一特征,使得在當前無序區中選取最大(或最小)關鍵字的記錄變得簡單。
(1)用大根堆排序的基本思想:
①先將初始文件R[1..n]建成一個大根堆,此堆為初始的無序區
②再將關鍵字最大的記錄R[1](即堆頂)和無序區的最后一個記錄R[n]交換,由此得到新的無序區R[1..n-1]和有序區R[n],且滿足R[1..n-1].keys <= R[n].keys
③由於交換后新的根R[1]可能違反堆性值,故應將當前無序區R[1..n-1]調整為堆。然后再次將R[1..n-1]中關鍵字最大的記錄R[1]和該區間的最后一個記錄R[n-1]交換,由此得到新的無序區R[1..n-2]和有序區R[n-1..n],且仍然滿足關系R[1..n-2].keys <= R[n-1..n].keys,同樣要將R[1..n-2]調整為堆。
......
直到無序區只有一個元素為止。
(2)大根堆排序算法的基本操作:
①建堆:建堆是不斷調整堆的過程,從len/2處開始調整,一直到第一個節點,此處len是堆中元素的個數。建堆的過程是線性的過程,從len/2到0處一直調用調整堆的過程,相當於o(h1)+o(h2)+...+o(hlen/2),其中h表示節點的深度,len/2表示節點的個數,這是一個求和的過程,結果是線性的O(n)。
②調整堆:調整堆在構建堆的過程中會用到,而且在堆排序過程中也會用到。利用的思想是比較節點i和它的孩子節點left(i)、right(i),選出三者中最大(或最小)者,如果最大(或最小)者的值不是節點i而是它的一個孩子節點,那便交換節點i和該節點,然后再調用調整堆的過程,這是一個遞歸的過程。調整堆的過程時間復雜度與堆的深度有關系,是lgn的操作,因為是沿着深度方向進行調整的。
③堆排序:堆排序是利用上面的兩個過程來進行的。首先是根據元素構建堆,然后將堆的根節點取出(一般是與最后一個節點進行交換),將前面len-1個節點繼續進行堆調整的過程,然后再將根節點取出,這樣一直到所有節點都取出。堆排序過程的時間復雜度是O(nlgn)。因為建堆的時間復雜度是O(n)(調用一次);調整堆的時間復雜度是lgn,調用了n-1次,所以堆排序的時間復雜度是O(nlgn)[2]。
(3)注意:
①只需做n-1趟排序,選出較大的n-1個關鍵字即可以使得文件遞增有序。
②用小根堆排序與利用大根堆類似,只不過其排序結果是遞減有序的。堆排序和直接選擇排序相反:在任何時刻堆排序中無序區總是在有序區之前,且有序區是在原向量的尾部由后往前逐步擴大至整個向量為止。
(4)特點:
堆排序(Heap Sort)是一樹形選擇排序。堆排序的特點是:在排序過程中,將R[1..n]看成是一棵完全二叉樹的順序存儲結構,利用完全二叉樹中雙親節點和孩子節點之間的內在關系(參見:二叉樹的順序存儲結構),在當前無序區中選擇關鍵字最大(或最小)的記錄。
示例:
package com.cnblogs.lxj;
/**
* @author liuxiaojiang
* @packageName:com.cnblogs.lxj
* @ClassName:HeapSort
* @Description:測試堆排序
* @date 2020/11/26
*/
public class HeapSort {
/**
* 主方法
* @param args
*/
public static void main(String[] args) {
int[] a = {6,5,3,1,8,7,2,4};
printArray(a);
heapSort(a);
printArray(a);
}
/**
* 堆排序方法
* @param a
*/
public static void heapSort(int[] a){
buildHeap(a);
printArray(a);
for(int i = a.length-1;i > 0;i--){
swapArray(i,0,a);
adjustTopToDown(0,a,i);
}
}
/**
* 建堆方法
* @param a
*/
public static void buildHeap(int[] a){
for(int i = (a.length-1)/2;i >= 0;i--){
adjustTopToDown(i,a,a.length);
}
}
/**
* 調整堆方法
* @param k
* @param a
* @param length
*/
public static void adjustTopToDown(int k,int[] a,int length){
for(int i = 2 * k + 1;i < length;i = 2 * i + 1){
if(i < length - 1){
if(a[i] < a[i + 1]){
i++;
}
}
if(a[k] < a[i]){
swapArray(k,i,a);
k = i;
}else {
break;
}
}
}
/**
* 交換方法
* @param i
* @param j
* @param a
*/
public static void swapArray(int i,int j,int[] a){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
/**
* 輸出方法
* @param array
*/
public static void printArray(int[] array){
for(int i : array){
System.out.print(i + " ");
}
System.out.println();
}
}
運行結果:
6 5 3 1 8 7 2 4 //數組初始化
8 6 7 4 5 3 2 1 //建堆及調整堆
1 2 3 4 5 6 7 8 //堆排序
原理:
算法分析:
堆排序的時間,主要由建立初始堆和反復重建堆這兩部分的時間開銷構成,它們均是通過調用Heapify實現的。
平均性能
O(N*logN)
其它性能
由於建初始堆所需的比較次數較多,所以堆排序不適宜於記錄數較少的文件。
堆排序是就地排序,輔助空間為O(1)
堆排序是不穩定的排序方法(排序的穩定性是指:如果在排序的序列中,存在前后相同的兩個元素的話,排序前和排序后他們的相對位置不發生變化)