文章目錄
前言
一、堆排序基本介紹
-
堆排序是利用堆這種數據結構而設計的一種排序算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均爲O(nlogn),它也是不穩定排序。
-
堆是具有以下性質的完全二叉樹
:每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆
, 注意 : 沒有要求結點的左孩子的值和右孩子的值的大小關係。 -
每個結點的值都小於或等於其左右孩子結點的值,稱爲
小頂堆
-
大頂堆舉例說明
我們對堆中的結點按層進行編號,映射到數組中就是下面這個樣子: (這裏其實就是順序儲存二叉樹
)
大頂堆特點:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 對應第幾個節點,i從0開始編號 -
小頂堆舉例說明
小頂堆特點:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 對應第幾個節點,i從0開始編號 -
一般從下往上:
升序採用大頂堆,降序採用小頂堆
二、堆排序基本思想
- 堆排序的基本思想是:
- 將待排序序列構造成一個
大頂堆
- 此時,整個序列的
最大值就是堆頂的根節點
。 - 將其與末尾元素進行交換,此時末尾就爲最大值。
- 然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了。
- 可以看到在構建大頂堆的過程中,元素的個數逐漸減少,最後就得到一個有序序列了.
三、思路圖解
要求:給你一個數組 {4,6,8,5,9} , 要求使用堆排序法,將數組升序排序。
3.1 步驟一 構造初始大頂堆。
將給定無序序列構造成一個大頂堆(一般升序採用大頂堆,降序採用小頂堆)。
- .假設給定無序序列結構如下
- .此時我們從最後一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。
- .找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換。
- 這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。
此時,我們就將一個無序序列構造成了一個大頂堆。
3.2 步驟二 將堆頂元素與末尾元素進行交換
將堆頂元素與末尾元素進行交換,使末尾元素最大。然後繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反覆進行交換、重建、交換。
- .將堆頂元素9和末尾元素4進行交換
- .重新調整結構,使其繼續滿足堆定義
- .再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.
- 後續過程,繼續進行調整,交換,如此反覆進行,最終使得整個序列有序
3.3 再簡單總結下堆排序的基本思路
- 將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
- 將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
- 重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
四、堆排序代碼實現
要求:給你一個數組 {4,6,8,5,9} , 要求使用堆排序法,將數組升序排序。
代碼實現:
說明:
- 堆排序不是很好理解,通過Debug 幫助大家理解堆排序
- 堆排序的速度非常快,在我的機器上 8百萬數據 3 秒左右。O(nlogn)
4.1 代碼實現
package com.feng.ch12_tree;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
/*
* 堆排序
* 堆 是具有以下性質的 完全二叉樹:
* 1、每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆(注意 : 沒有要求結點的左孩子的值和右孩子的值的大小關係。)
* 2、每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆
*
* 完全二叉樹:
* 如果該二叉樹的所有葉子節點都在最後一層或者倒數第二層,而且最後一層的葉子節點在左邊連續,倒數第二層的葉子節點在右邊連續,我們稱爲完全二叉樹。
*
* 對排序分爲三步:
* 1、將無序序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
* 2、將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
* 3、重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
* 說明:
* 第一步直接將二叉樹對應的數組 調整成 大頂堆或者 小頂堆
* 然後對第二步、第三步進行循環操作即可。
* */
public class T4_HeapSort {
public static void main(String[] args) {
// 默認升序排序
// int[] array = {4, 6, 8, 5, 9};
int[] array = {4, 6, 8, 5, 9, -1, 90, 89, 56, -999};
System.out.println("原始數組:");
System.out.println(Arrays.toString(array));
heapSort(array);
System.out.println("測試堆排序速度:");
testTime(); // 8000數據:88ms; 8萬數據: 122ms; 80萬數據:380ms; 800萬數據:4s
}
/*
* 測試一下 堆排序的速度, 給 80000 個數據,測試一下
* */
public static void testTime() {
// 創建一個 80000個的隨機的數組
int array2[] = new int[8000000];
for (int i = 0; i < 8000000; i++) {
array2[i] = (int) (Math.random() * 8000000); // 生成一個[ 0, 8000000] 數
}
// System.out.println(Arrays.toString(array2)); // 不在打印,耗費時間太長
long start = System.currentTimeMillis(); //返回以毫秒爲單位的當前時間
System.out.println("long start:" + start);
Date date = new Date(start); // 上面的也可以不要,但是我想測試
System.out.println("date:" + date);
SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
System.out.println("排序前的時間是=" + format.format(date));
heapSort(array2);
System.out.println();
long end = System.currentTimeMillis();
Date date2 = new Date(end); // 上面的也可以不要,但是我想測試
System.out.println("排序後的時間是=" + format.format(date2));
System.out.println("共耗時" + (end - start) + "毫秒");
System.out.println("毫秒轉成秒爲:" + ((end - start) / 1000) + "秒");
}
/*
* 編寫一個堆排序的方法
* 核心:將樹排成 大頂堆或者小頂堆。
* 1、將一個數組(對應二叉樹), 調整成一個大頂堆
* 2、將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
* 3、重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
* 循環 2、3 步
* */
public static void heapSort(int[] array) {
int temp = 0;
//分步調整
// System.out.println("調整成大頂堆:");
// adjustHeap(array, 1, array.length);
// System.out.println("第 1 次:"+Arrays.toString(array)); // [4, 9, 8, 5, 6]
//
// adjustHeap(array, 0, array.length);
// System.out.println("第 2 次:"+Arrays.toString(array)); // [9, 6, 8, 5, 4]
/*
* 完成我們最終代碼 , 對上面的 兩步規律 進行整合,使用for 循環,使用 array.length / 2 - 1 找到第一個非葉子結點。
* i = array.length / 2 - 1 : 從左到右,從下到上的第一個非葉子節點的 索引
* */
for (int i = array.length / 2 - 1; i >= 0; i--) {
adjustHeap(array, i, array.length);
}
// System.out.println("調整成的大頂堆:"+Arrays.toString(array)); // [9, 6, 8, 5, 4]
/*
* 2).將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
* 3).重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。
* */
for (int j = array.length - 1; j > 0; j--) {
// 交換: 將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
temp = array[j];
array[j] = array[0];
array[0] = temp;
// 每互換一次,都要對根結點 進行調整爲大頂堆。
adjustHeap(array, 0, j);
}
// System.out.println("排序後:"+Arrays.toString(array)); // [9, 6, 8, 5, 4]
}
/*
* 將一個數組(對應二叉樹), 調整成一個大頂堆
* 功能: 完成將 以 i 對應的非葉子結點的樹,調整成大頂堆
* 舉例: int[] array = {4, 6, 8, 5, 9}; => i = 1 => adjustHeap => {4, 9, 8, 5, 6}
* 再次調用adjustHeap 傳入的是 i = 0 => {9, 4, 8, 5, 6}
* 再次調用adjustHeap 傳入的是 i = 0 => {9, 4, 8, 5, 6} 進行調整 => {9, 6, 8, 5, 4}
*
* @param array 待調整的數組
* @param i 表示 非葉子節點 的在數組中的索引,就是當前結點
* @param length 表示對多少個元素進行調整,length是在逐漸減少
* */
public static void adjustHeap(int[] array, int i, int length) {
int temp = array[i]; // 先取出 當前 i結點 的值,保存在臨時變量
/*
* 開始調整
* 1、k = i * 2 + 1: k 是以 i 爲非葉子結點的 左子結點
* */
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && array[k] < array[k + 1]) { // 說明左子結點 小於 右子結點的值
k++; // 讓 k 指向 右子結點,這時 k 爲最大值的索引
}
if (array[k] > temp) { // 如果右(左)子結點 大於 父結點,說明這裏要對右(左)子結點(k)和父結點(i)進行 互換
array[i] = array[k]; // 把較大的值賦給當前結點
array[k] = temp;
i = k; // !!! i指向 k ,改變父結點 ,繼續循環比較
} else {
break; //!!! 敢break 是因爲 這裏的i 是從左到右,從下到上,第一個的非葉子節點
}
}
/*
* 老師是寫在這兒,我寫在了上面的判斷中 :array[k] = temp;
* 當代碼走到這兒,for循環結束後,已經將以 i 爲父結點的樹的最大值,放在了最頂(局部)
* */
// array[i] = temp;// 將 temp 值 放到調整後的位置。
}
}
4.2 測試結果
800萬數據 僅用 4S ,可見速度之快。