資料結構與演算法(十二):堆排序
一、什麼是堆排序
1.概述
堆排序是利用堆這種資料結構而設計的一種排序演算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均為O(nlogn),它也是不穩定排序。
其中,對於“堆”我們可以理解為具有以下性質的完全二叉樹:
- 每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆
- 每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆
在排序時,一般升序採用大頂堆,降序採用小頂堆。
2.大頂堆
我們可以看到,層數從小到大,節點的數字是越來越小的,對映到陣列有:{50,45,40,20,25,35,30,10,15}
特點是arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2]
3.小頂堆
跟大頂堆相反,層數從小到大,節點的數字是越來越大,對映到陣列:{10,20,15,25,50,30,40,35,45}
特點是:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2]
二、堆排序的思路分析
1.概述
- 將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。
- 將其與末尾元素進行交換,此時末尾就為最大值。
- 然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了。
- 遍歷構建大頂堆,在這過程中元素的個數逐漸減少,直到最後得到一個有序序列了.
2.舉個例子
對陣列{4,6,8,5,9}進行排序。
第一遍排序
-
我們從最後一個非葉子結點開始排序。第一個非葉子結點為
arr.length/2-1=5/2-1=1
,也就是元素6.,我們對他進行對比並調整位置; -
在{6,5,4}中,5比6小,而9比6大,所以9和6交換位置;
-
接著找到第二個非葉子節點4,由於9是{9,4,8}這個樹中最大的,故9與4交換位置
-
由於9與4交換位置打亂了原先{9,5,6}這棵樹順序,所以繼續對新樹{4,5,6}進行排序
-
由此得到了一個大頂堆,然後將堆頂元素9與末尾元素4進行交換,得到陣列{4,6,8,5,9}
至此,第一遍排序已經完成,我們確定了最大元素9的位置
第二遍排序
第二遍排序開始時,最大元素9的位置已經確定,實際上要排序的陣列變成了{4,6,8,5}
-
繼續從6開始比較,{6,5}排序正常,所以接著比較{4,6,8},8是最大的,所以與4交換位置
-
由此得到了一個大頂堆,然後將堆頂元素8與末尾元素5進行交換,得到陣列{8,6,4}
至此,第一遍排序已經完成,我們確定了最第二大元素8的位置
第三遍~第n遍排序
第二遍排序開始時,最大元素9和第二大元素8的位置已經確定,實際上要排序的陣列變成了{5,6,4}
重複比較-排序-交換堆頂和隊尾元素位置這一過程,直到最終獲得有序數列
三、程式碼實現
/**
* @Author:CreateSequence
* @Date:2020-07-16 16:53
* @Description:堆排序
*/
public class HeapSort {
/**
* 對陣列進行堆排序
* @param arr
* @return
*/
public static int[] sort(int[] arr) {
//將無序陣列構建成一個大/小頂堆
//有幾個非葉子節點就排序幾次
for (int i = arr.length / 2 - 1; i >= 0; i--) {
sortHeap(arr,i,arr.length);
}
int temp = 0;
//交換陣列頭尾元素,將最大的元素排沉到隊尾
for (int i = arr.length - 1; i > 0; i--) {
//交換頭尾元素
temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
//1.交換完後,此時最大的元素在arr[0],最小的元素在arr[i],即確定了本次排序範圍最大的數
//2.然後對0~i-1的範圍進行排序,重新獲得的陣列最小的元素在arr[0],最大的元素在arr[i-1]
sortHeap(arr, 0, i);
//3.接著進入下一次迴圈,重複步驟1,2,每次迴圈排序範圍都縮小一位
}
return arr;
}
/**
* 將以非葉子節點i為根節點的樹調整為一個大頂堆
* @param arr 要調整的陣列
* @param i 非葉子結點在陣列中的下標
* @param length 要調整的陣列長度
*/
public static int[] sortHeap(int[] arr, int i, int length) {
if (arr == null || arr.length == 0) {
throw new RuntimeException("數列必須至少有一個元素!");
}
//獲取根節點值
int temp = arr[i];
//從左節點開始遍歷
for (int j = i * 2 + 1; j < length; j = j * 2 + 1) {
//比較左右節點大小,將j指向值大的節點
if (j + 1 < length && arr[j + 1] > arr[j]) {
j = j + 1;
}
//比較將左右節點與父節點大小
if (arr[j] > temp) {
//如果子節點大於父節點,交換兩節點位置
arr[i] = arr[j];
//然後繼續從該子節點向下遍歷
i = j;
}else {
break;
}
}
//結束迴圈時,arr[i]已經存放了以原arr[i]為根節點的樹的最大值
arr[i] = temp;
return arr;
}
}