什麼是堆
堆首先是一個完全二叉樹,堆分爲大頂堆和小頂堆;
大頂堆 :
每個節點的值大於或等於其左右孩子節點的值,稱爲大頂堆。
小頂堆同理就是每個節點的值小於或等於其左右孩子節點的值。
注意:
每個節點的左右孩子節點的大小關係並沒有限定。
大頂堆舉例
如圖:
首先其爲一個完全二叉樹,且其每個節點的值都大於或者等於其左右孩子節點的值。
完全二叉樹從上到下,從左到右依次編號,就可以將其進行順序存儲,我們從根節點開始,從0開始編號,存入數組如下:
堆特點
由大頂堆定義知道,如果我們從上到下,從左到右,根節點開始從0編號進行順序存儲的話,並將數組記爲arr;
我們可以得到如下式子:
arr[i] >= arr[ 2i + 1] && arr[ i ] >= arr[ 2i + 2];
其中 2i + 1爲第 i 個節點的左孩子節點的編號。2i + 2爲第 i 個節點的右孩子節點的編號;
同理得小頂堆的特點:
arr[i] <= arr[ 2i + 1] && arr[ i ] <= arr[ 2i + 2];
堆排序基本思想
本文以大頂堆爲例,進行講解。
算法步驟如下:
1、首先將待排序序列構建成一個大頂堆(存入數組中),那麼這時,整個序列的最大值就是堆頂的根節點;
2、將堆頂元素與最後一個元素交換,那麼末尾元素就存入了最大值;
3、將剩餘的 n - 1個元素重新構建成一個大頂堆,重複上面的操作;
反覆執行,就能得到一個有序序列了。
舉例
給定一個待排序序列數組 arr = [ 0 , 2, 4, 1 , 5 ];
先構建成一個完全二叉樹如下;
構建堆
我們從最後一個非葉子節點開始,從左至右,從下到上,開始調整;
最後一個非葉子節點的索引即 arr.length / 2向下取整 - 1 ,對於此例就是 5 / 2向下取整 - 1 = 2 - 1 = 1;
即值爲2的節點;
我們用左右孩子節點的最大值與該節點進行比較;
此時我們發現它的左右孩子節點的最大值爲5,大於2,進行交換;
然後處理下一個非葉子節點,即剛纔的索引減去1; 1 - 1 = 0;
即:
左右孩子節點爲5和4,5最大,且大於該節點的值,發生交換;
這時我們發現了一個問題:
值爲0的節點的左右節點又比該節點大了,又不滿足大頂堆的定義了
繼續進行調整:
對非葉子節點調整完畢,構建大頂堆完成。
交換
將堆頂元素與末尾元素進行交換,使得末尾元素最大。
當交換完畢後最大的元素已經到達數組末尾;
對數組中其他元素進行排序即可。
進行交換:
剩下的元素調整並交換後:
剩下的元素調整並交換後:
此時也意味着排序完成了。
代碼
先說下調整的代碼;
我們需要三個參數,待排序的數組,數組的長度,還有一個就是調整的哪一個非葉子節點;
/**
* author:微信公衆號:code隨筆
* @param arr 待排序的數組
* @param i 表示等待調整的哪個非葉子節點的索引
* @param length 待調整長度
*/
public static void adjustHeap(int arr[],int i,int length){
//非葉子節點的值
int notLeafNodeVal = arr[i];
//k的初始值爲當前非葉子節點的左孩子節點的索引
//k = 2 * k + 1表示再往左子節點找
for(int k = i * 2 + 1;k<length;k=2 *k + 1){
//如果k + 1還在待調整的長度內,且右子樹的值大於等於左子樹的值
//將k++,此時爲當前節點的右孩子節點的索引
if(k+1<length && arr[k] < arr[k+1]){
k++;
}
//如果孩子節點大於當前非葉子節點
if(arr[k] > notLeafNodeVal){
arr[i] = arr[k];//將當前節點賦值爲孩子節點的值
i = k;//將i賦值爲孩子節點的值,再看其孩子節點是否有比它大的
}else{
break;//能夠break的保證是,我們是從左至右,從下到上進行調整的
//只要上面的不大於,下面的必不大於
}
}
//循環結束後,將i索引處的節點賦值爲之前存的那個非葉子節點的值
arr[i] = notLeafNodeVal;
}
再說下堆排序代碼,看好註釋;
//堆排序方法
public static void heapSort(int arr[]){
//進行第一次調整
for(int i=arr.length/2 - 1;i>=0;i--){
adjustHeap(arr,i,arr.length);
}
for(int j=arr.length - 1;j>0;j--){
//進行交換
int temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
//調整長度爲j的那些
//這裏爲什麼填0呢
//因爲我們第一次調整的時候從左到右,從下到上調整的;
//交換時只是變動了堆頂元素和末尾元素
//末尾元素我們不用去管,因爲已經是之前長度最大的了
//只需要把當前堆頂元素找到合適的位置即可
adjustHeap(arr,0,j);
}
}
完整代碼
import java.util.Arrays;
public class Solution {
public static void main(String[] args) {
int [] arr = new int[]{0 , 2, 4, 1 , 5};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
//堆排序方法
public static void heapSort(int arr[]){
//進行第一次調整
for(int i=arr.length/2 - 1;i>=0;i--){
adjustHeap(arr,i,arr.length);
}
for(int j=arr.length - 1;j>0;j--){
//進行交換
int temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
//調整長度爲j的那些
//這裏爲什麼填0呢
//因爲我們第一次調整的時候從左到右,從下到上調整的;
//交換時只是變動了堆頂元素和末尾元素
//末尾元素我們不用去管,因爲已經是之前長度最大的了
//只需要把當前堆頂元素找到合適的位置即可
adjustHeap(arr,0,j);
}
}
/**
* author:微信公衆號:code隨筆
* @param arr 待排序的數組
* @param i 表示等待調整的哪個非葉子節點的索引
* @param length 待調整長度
*/
public static void adjustHeap(int arr[],int i,int length){
//非葉子節點的值
int notLeafNodeVal = arr[i];
//k的初始值爲當前非葉子節點的左孩子節點的索引
//k = 2 * k + 1表示再往左子節點找
for(int k = i * 2 + 1;k<length;k=2 *k + 1){
//如果k + 1還在待調整的長度內,且右子樹的值大於等於左子樹的值
//將k++,此時爲當前節點的右孩子節點的索引
if(k+1<length && arr[k] < arr[k+1]){
k++;
}
//如果孩子節點大於當前非葉子節點
if(arr[k] > notLeafNodeVal){
arr[i] = arr[k];//將當前節點賦值爲孩子節點的值
i = k;//將i賦值爲孩子節點的值,再看其孩子節點是否有比它大的
}else{
break;//能夠break的保證是,我們是從左至右,從下到上進行調整的
//只要上面的不大於,下面的必不大於
}
}
//循環結束後,將i索引處的節點賦值爲之前存的那個非葉子節點的值
arr[i] = notLeafNodeVal;
}
}
時間複雜度
在建初始堆時,其複雜度爲;
交換操作需 n-1 次;
重建堆的過程中近似爲;
堆排序時間複雜度爲。
穩定性
堆排序是不穩定的:
比如:10,9,6,9;
如圖:
當堆頂元素10和末尾元素交換後,兩個9的相對位置發生改變。
歡迎關注
歡迎大家的關注
掃描下方的二維碼或者微信搜一搜即可關注我的微信公衆號:code隨筆