Java中的堆排序詳解
1:堆
毫無疑問,排序兩個字沒必要去死磕,這里的重點,在于排序的方式,堆排序,就是以堆的形式去排序,毫無疑問,了解堆很重要。
那么,什么是堆呢?
這里,必須引入一個完全二叉樹的概念,然后過渡到堆的概念。

上圖,就是一個完全二叉樹,其特點在于:
從作為第一層的根開始,除了最后一層之外,第N層的元素個數(shù)都必須是2的N次方;
第一層2個元素,第二層4個,第三層8個,以此類推。
而最后一行的元素,都要緊貼在左邊,換句話說,每一行的元素都從最左邊開始安放,兩個元素之間不能有空閑,具備了這兩個特點的樹,就是一棵完全二叉樹。
那么,完全二叉樹與堆有什么關系呢?
我們假設有一棵完全二叉樹,在滿足作為完全二叉樹的基礎上,對于任意一個擁有父節(jié)點的子節(jié)點,其數(shù)值均不小于父節(jié)點的值;
這樣層層遞推,就是根節(jié)點的值最小,這樣的樹,稱為小根堆。
同理,又有一棵完全二叉樹,對于任意一個子節(jié)點來說,均不大于其父節(jié)點的值,如此遞推,就是根節(jié)點的值是最大的,這樣的數(shù),稱為大根堆。

如上圖,左邊就是大根堆;右邊則是小根堆,這里必須要注意一點,只要求子節(jié)點與父節(jié)點的關系,兩個節(jié)點的大小關系與其左右位置沒有任何關系。
明確下大根堆,小根堆的概念,繼續(xù)說堆排序。
現(xiàn)在對于堆排序來說,我們先要做的是,把待排序的一堆無序的數(shù),整理成一個大根堆,或者小根堆,下面討論以大根堆為例子。
給定一個列表array=[16,7,3,20,17,8],對其進行堆排序(使用大根堆)。
接下來內容是轉載部分,自己繪圖功底太差:其中綠色部分為自己的注解。
步驟一
構造初始堆。將給定無序序列構造成一個大頂堆(一般升序采用大頂堆,降序采用小頂堆)。
假設給定無序序列結構如下

此時我們從最后一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。
此處必須注意,我們把6和9比較交換之后,必須考量9這個節(jié)點對于其子節(jié)點會不會產生任何影響?
因為其是葉子節(jié)點,所以不加考慮;但是,一定要熟練這種思維,寫代碼的時候就比較容易理解為什么會出現(xiàn)一次非常重要的交換了。

找到第二個非葉節(jié)點4,由于[4,9,8]中9元素最大,4和9交換。
在真正代碼的實現(xiàn)中,這時候4和9交換過后,必須考慮9所在的這個節(jié)點位置,因為其上的值變了,必須判斷對其的兩個子節(jié)點是否造成了影響
這么說不合適,實際上就是判斷其作為根節(jié)點的那棵子樹,是否還滿足大根堆的原則,每一次交換,都必須要循環(huán)把子樹部分判別清楚。

這時,交換導致了子根[4,5,6]結構混亂,繼續(xù)調整,[4,5,6]中6最大,交換4和6。
牢記上面說的規(guī)則,每次交換都要把改變了的那個節(jié)點所在的樹重新判定一下,這里就用上了,4和9交換了,變動了的那棵子樹就必須重新調整,一直調整到符合大根堆的規(guī)則為截。

此時,我們就將一個無序序列構造成了一個大頂堆。
步驟二
- 將堆頂元素與末尾元素進行交換,使末尾元素最大。然后繼續(xù)調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。
如此反復進行交換、重建、交換。
將堆頂元素9和末尾元素4進行交換
這里,必須說明一下,所謂的交換,實際上就是把最大值從樹里面拿掉了,剩下參與到排序的樹,其實只有總結點的個數(shù)減去拿掉的節(jié)點個數(shù)了。所以圖中用的是虛線。

- 重新調整結構,使其繼續(xù)滿足堆定義

- 再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.

后續(xù)過程,繼續(xù)進行調整,交換,如此反復進行,最終使得整個序列有序

下面,附上我的代碼,也是從文末鏈接中模仿過來的,但是親自敲過一遍,印象深刻。
public class HeapSort {
public static void main(String[] args) {
int[] array = new int[] { 2, 1, 4, 3, 6, 5, 8, 7 };
// 接下來就是排序的主體邏輯
sort(array);
System.out.println(Arrays.toString(array));
}
/**
*
* @description 本方法只有一個參數(shù),那就是待排序的array
* @author
* @param
* @return
* @time 2018年3月9日 下午2:24:45
*/
public static void sort(int[] array) {
// 按照完全二叉樹的特點,從最后一個非葉子節(jié)點開始,對于整棵樹進行大根堆的調整
// 也就是說,是按照自下而上,每一層都是自右向左來進行調整的
// 注意,這里元素的索引是從0開始的
// 另一件需要注意的事情,這里的建堆,是用堆調整的方式來做的
// 堆調整的邏輯在建堆和后續(xù)排序過程中復用的
for (int i = array.length / 2 - 1; i >= 0; i--) {
adjustHeap(array, i, array.length);
}
// 上述邏輯,建堆結束
// 下面,開始排序邏輯
for (int j = array.length - 1; j > 0; j--) {
// 元素交換
// 說是交換,其實質就是把大頂堆的根元素,放到數(shù)組的最后;換句話說,就是每一次的堆調整之后,都會有一個元素到達自己的最終位置
swap(array, 0, j);
// 元素交換之后,毫無疑問,最后一個元素無需再考慮排序問題了。
// 接下來我們需要排序的,就是已經(jīng)去掉了部分元素的堆了,這也是為什么此方法放在循環(huán)里的原因
// 而這里,實質上是自上而下,自左向右進行調整的
adjustHeap(array, 0, j);
}
}
/**
*
* @description 這里,是整個堆排序最關鍵的地方,正是因為把這個方法抽取出來,才更好理解了堆排序的精髓,會盡可能仔細講解
* @author
* @param
* @return
* @time 2018年3月9日 下午2:54:38
*/
public static void adjustHeap(int[] array, int i, int length) {
// 先把當前元素取出來,因為當前元素可能要一直移動
int temp = array[i];
// 可以參照sort中的調用邏輯,在堆建成,且完成第一次交換之后,實質上i=0;也就是說,是從根所在的最小子樹開始調整的
// 接下來的講解,都是按照i的初始值為0來講述的
// 這一段很好理解,如果i=0;則k=1;k+1=2
// 實質上,就是根節(jié)點和其左右子節(jié)點記性比較,讓k指向這個不超過三個節(jié)點的子樹中最大的值
// 這里,必須要說下為什么k值是跳躍性的。
// 首先,舉個例子,如果a[0] > a[1]&&a[0]>a[2],說明0,1,2這棵樹不需要調整,那么,下一步該到哪個節(jié)點了呢?肯定是a[1]所在的子樹了,
// 也就是說,是以本節(jié)點的左子節(jié)點為根的那棵小的子樹
// 而如果a[0}<a[2]呢,那就調整a[0]和a[2]的位置,然后繼續(xù)調整以a[2]為根節(jié)點的那棵子樹,而且肯定是從左子樹開始調整的
// 所以,這里面的用意就在于,自上而下,自左向右一點點調整整棵樹的部分,直到每一顆小子樹都滿足大根堆的規(guī)律為止
for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
// 讓k先指向子節(jié)點中最大的節(jié)點
if (k + 1 < length && array[k] < array[k + 1]) {
k++;
}
// 如果發(fā)現(xiàn)子節(jié)點更大,則進行值的交換
if (array[k] > temp) {
swap(array, i, k);
// 下面就是非常關鍵的一步了
// 如果子節(jié)點更換了,那么,以子節(jié)點為根的子樹會不會受到影響呢?
// 所以,循環(huán)對子節(jié)點所在的樹繼續(xù)進行判斷
i = k;
// 如果不用交換,那么,就直接終止循環(huán)了
} else {
break;
}
}
}
/**
* 交換元素
*
* @param arr
* @param a
* 元素的下標
* @param b
* 元素的下標
*/
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}小頂推
/**
* @Author: 白雄雄
* @Date: 2019/9/10 13:48
*/
public class SmailHeap {
public static void main(String[] args) {
int[] array = new int[]{1,3,2,7,4,0,5,10};
heapSort(array);
System.out.println(Arrays.toString(array));
}
private static void heapSort(int[] array) {
for (int i = array.length / 2 - 1; i >= 0; i--) {
adjustHeap(array,i,array.length);
}
for (int j = array.length - 1; j >= 0; j--) {
int temp = array[0];
array[0] = array[j];
array[j] = temp;
adjustHeap(array,0,j);
}
}
private static void adjustHeap(int[] array, int i, int len) {
int temp = array[i];
for (int k = i * 2 + 1; i < len; k = 2 * k + 1) {
if (k + 1 < len && array[k] > array[k + 1]) {
k++;
}
if (k<len && array[k] < temp) {
array[i] = array[k];
i = k;
} else {
break;
}
}
array[i] = temp;
}
}到此這篇關于Java中的堆排序詳解的文章就介紹到這了,更多相關Java堆排序內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Mybatis和orcale update語句中接收參數(shù)為對象的實例代碼
Mybatis的 mapper.xml 中 update 語句使用 if 標簽判斷對像屬性是否為空值。本文重點給大家介紹Mybatis和orcale update語句中接收參數(shù)為對象的實例代碼,需要的朋友參考下吧2017-09-09
SpringAMQP消息隊列(SpringBoot集成RabbitMQ方式)
這篇文章主要介紹了SpringAMQP消息隊列(SpringBoot集成RabbitMQ方式),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-04-04
Netty分布式ByteBuf使用的底層實現(xiàn)方式源碼解析
這篇文章主要為大家介紹了Netty分布式ByteBuf使用底層實現(xiàn)方式源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-03-03
SpringBoot同時支持HTTPS與HTTP的實現(xiàn)示例
本文主要介紹了SpringBoot同時支持HTTPS與HTTP的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-07-07
Spring AOP結合注解實現(xiàn)接口層操作日志記錄
在項目開發(fā)中我們需要記錄接口的操作日志:包含請求參數(shù)、響應參數(shù)、接口所屬模塊、接口功能描述、請求地址、ip地址等信息;實現(xiàn)思路很簡單就是基于注解和aop的方式去記錄日志,主要的難點在于日志表結構、注解的設計已經(jīng)aop實現(xiàn)的一些比較好的實現(xiàn)方式的借鑒2022-08-08
Java實現(xiàn)添加,讀取和刪除Excel圖片的方法詳解
本文介紹在Java程序中如何添加圖片到excel表格,以及如何讀取、刪除excel表格中已有的圖片。文中的示例代碼講解詳細,感興趣的可以學習一下2022-05-05

