簡單認識
堆排序(Heapsort)是指利用堆積樹(堆)這種數據結構所設計的一種排序算法,它是選擇排序的一種。可以利用數組的特點快速定位指定索引的元素。堆分爲大根堆和小根堆,是完全二叉樹。完全二叉樹:除最後一層外,每一層上的節點數均達到最大值;在最後一層上只缺少右邊的若干結點。
理解
如圖所示,我們要排序的數據時存在數組中的,只是,我們像訪問二叉樹一樣訪問”節點“。因爲完全二叉樹的父節點與左右孩子節點的座標是可以相互計算出來的。下面是按照從0開始計數的計算公式。從1開始計數的公式會有所不同,但也不難得到,畫個簡單的完全二叉樹試一下就可以了。
計算當前節點的左孩子節點:(current * 2) + 1
;
很明顯右孩子節點:(current * 2) + 2
;
計算當前節點的父節點:(current - 1) / 2;
基本思想
堆排序利用了大根堆(或小根堆)堆頂記錄的關鍵字最大(或最小)這一特徵,使得在當前無序區中選取最大(或最小)關鍵字的記錄變得簡單。(1)用大根堆排序的基本思想:
① 先將初始數組R[0..n]建成一個大根堆,此堆爲初始的無序區
② 再將關鍵字最大的記錄R[0](即堆頂)和無序區的最後一個記錄R[n]交換,由此得到新的無序區R[0..n-1]和有序區R[n],且滿足R[0..n-1].keys≤R[n].key
③由於交換後新的根R[0]可能違反堆性質,故應將當前無序區R[0..n-1]調整爲堆。然後再次將R[0..n-1]中關鍵字最大的記錄R[0]和該區間的最後一個記錄R[n-1]交換,由此得到新的無序區R[0..n-2]和有序區R[n-1..n],且仍滿足關係R[0..n-2].keys≤R[n-1..n].keys,同樣要將R[0..n-2]調整爲堆。
……
直到無序區只有一個元素爲止。
(2)大根堆排序算法的基本操作:
①建堆,建堆是不斷調整堆的過程,從(len-1)/2處開始調整,一直到第一個節點,此處len是堆中元素的個數。建堆的過程是線性的過程,從(len-1)/2到0處一直調用調整堆的過程,相當於o(h1)+o(h2)…+o(h(len-1)/2) 其中h表示節點的深度,(len-1)/2表示節點的個數,這是一個求和的過程,結果是線性的O(n)。
②調整堆:調整堆在構建堆的過程中會用到,而且在堆排序過程中也會用到。利用的思想是比較節點i和它的孩子節點left(i),right(i),選出三者最大(或者最小)者,如果最大(小)值不是節點i而是它的一個孩子節點,那邊交互節點i和該節點,然後再調用調整堆過程,這是一個遞歸的過程。調整堆的過程時間複雜度與堆的深度有關係,是lgn的操作,因爲是沿着深度方向進行調整的。
③堆排序:堆排序是利用上面的兩個過程來進行的。首先是根據元素構建堆。然後將堆的根節點取出(一般是與最後一個節點進行交換),將前面len-1個節點繼續進行堆調整的過程,然後再將根節點取出,這樣一直到所有節點都取出。堆排序過程的時間複雜度是O(nlgn)。因爲建堆的時間複雜度是O(n)(調用一次);調整堆的時間複雜度是lgn,調用了n-1次,所以堆排序的時間複雜度是O(nlgn)。- 示例(來自堆排序原理及算法實現(最大堆))
給定一個整形數組a[]={16,7,3,20,17,8},對其進行堆排序。
首先根據該數組元素構建一個完全二叉樹,得到
然後需要構造初始堆,則從最後一個非葉節點開始調整:
20和16交換後導致16不滿足堆的性質,因此需重新調整這樣就得到了初始堆。
即每次調整都是從父節點、左孩子節點、右孩子節點三者中選擇最大者跟父節點進行交換(交換之後可能造成被交換的孩子節點不滿足堆的性質,因此每次交換之後要重新對被交換的孩子節點進行調整)。有了初始堆之後就可以進行排序了。
此時3位於堆頂不滿堆的性質,則需調整繼續調整。
這樣整個區間便已經有序了。
從上述過程可知,堆排序其實也是一種選擇排序,是一種樹形選擇排序。只不過直接選擇排序中,爲了從R[1…n]中選擇最大記錄,需比較n-1次,然後從R[1…n-2]中選擇最大記錄需比較n-2次。事實上這n-2次比較中有很多已經在前面的n-1次比較中已經做過,而樹形選擇排序恰好利用樹形的特點保存了部分前面的比較結果,因此可以減少比較次數。對於n個關鍵字序列,最壞情況下每個節點需比較log2(n)次,因此其最壞情況下時間複雜度爲nlogn。堆排序爲不穩定排序,不適合記錄較少的排序。 - 代碼(來自百度百科:堆排序)
public class HeapSort {
private static int[] sort = new int[] { 1, 0, 10, 20, 3, 5, 6, 4, 9, 8, 12,
17, 34, 11 };
public static void main(String[] args) {
// 建初始最大堆
buildMaxHeapify(sort);
// 最大堆排序
heapSort(sort);
// 打印結果
print(sort);
}
/**
*
* @param data建堆的數組
*/
private static void buildMaxHeapify(int[] data) {
// 沒有子節點的才需要創建最大堆,從最後一個的父節點開始
int startIndex = getParentIndex(data.length - 1);
// 從尾端開始創建最大堆
for (int i = startIndex; i >= 0; i--) {
maxHeapify(data, data.length, i);
}
}
/**
* 創建最大堆
*
* @param data
* @param heapSize需要創建最大堆的大小
* ,一般在sort的時候用到,因爲最大值放在末尾,末尾就不再歸入最大堆了
* @param index當前需要創建最大堆的位置
* ,即父節點位置
*/
private static void maxHeapify(int[] data, int heapSize, int index) {
// 當前點與左右子節點比較
int left = getChildLeftIndex(index);
int right = getChildRightIndex(index);
int largest = index;
// >heapSize的節點是已經排好序的,不能再動了
if (left < heapSize && data[index] < data[left]) {
largest = left;
}
if (right < heapSize && data[largest] < data[right]) {
largest = right;
}
// 得到最大值後可能需要交換,如果交換了,其子節點可能就不是最大堆了,需要重新調整
if (largest != index) {
swap(data, index, largest);
// 不能忽略這一步
maxHeapify(data, heapSize, largest);
}
}
/**
* 排序,最大值放在末尾
*
* @param data
*/
private static void heapSort(int[] data) {
// 末尾與頭交換,交換後調整最大堆
for (int i = data.length - 1; i > 0; i--) {
swap(data, 0, i);
// 由於節點0變動了,所以需要調整最大堆,注意這裏的i是指需要調整的最大堆的大小,是從1開始計算的
maxHeapify(data, i, 0);
}
}
private static void swap(int[] data, int i, int j) {
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
/**
* 父節點位置
*
* @param current
* @return
*/
private static int getParentIndex(int current) {
// return (current - 1) / 2;
// 移位運算,等同於上面
return (current - 1) >> 1;
}
/**
* 左子節點position注意括號,加法優先級更高
*
* @param current
* @return
*/
private static int getChildLeftIndex(int current) {
// return (current * 2) + 1;
return (current << 1) + 1;
}
/**
* 右子節點position
*
* @param current
* @return
*/
private static int getChildRightIndex(int current) {
// return (current * 2) + 2;
return (current << 1) + 2;
}
private static void print(int[] data) {
int pre = -2;
for (int i = 0; i < data.length; i++) {
if (pre < (int) getLog(i + 1)) {
pre = (int) getLog(i + 1);
System.out.println();
}
System.out.print(data[i] + "|");
}
}
/**
* 計算以2爲底的對數
*
* @param param
* @return
*/
private static double getLog(double param) {
return Math.log(param) / Math.log(2);
}
}
輸出結果:
0|
1|3|
4|5|6|8|
9|10|11|12|17|20|34|