詳解堆排序算法

什麼是堆

首先是一個完全二叉樹,分爲大頂堆小頂堆
大頂堆 :
每個節點的值大於或等於其左右孩子節點的值,稱爲大頂堆。
小頂堆同理就是每個節點的值小於或等於其左右孩子節點的值。
注意:
每個節點的左右孩子節點的大小關係並沒有限定。

大頂堆舉例

如圖:

大頂堆舉例

首先其爲一個完全二叉樹,且其每個節點的值都大於或者等於其左右孩子節點的值。
完全二叉樹從上到下,從左到右依次編號,就可以將其進行順序存儲,我們從根節點開始,從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的節點;

構建堆1

我們用左右孩子節點的最大值與該節點進行比較;
此時我們發現它的左右孩子節點的最大值爲5,大於2,進行交換;

構建堆2

然後處理下一個非葉子節點,即剛纔的索引減去1; 1 - 1 = 0;
即:

構建堆3

左右孩子節點爲5和4,5最大,且大於該節點的值,發生交換;

構建堆4

這時我們發現了一個問題:
值爲0的節點的左右節點又比該節點大了,又不滿足大頂堆的定義了

繼續進行調整:

構建堆5

對非葉子節點調整完畢,構建大頂堆完成。

交換

將堆頂元素與末尾元素進行交換,使得末尾元素最大。

堆頂元素與末尾元素交換

當交換完畢後最大的元素已經到達數組末尾;

第一次交換後

對數組中其他元素進行排序即可。

剩下的四個元素進行調整

進行交換:

第二大元素歸位

剩下的元素調整並交換後:

第三大元素歸位

剩下的元素調整並交換後:

第三大元素歸位

第四大元素歸位置

此時也意味着排序完成了。

代碼

先說下調整的代碼;
我們需要三個參數,待排序的數組,數組的長度,還有一個就是調整的哪一個非葉子節點;

 /**
     * 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;
    }
}

時間複雜度

在建初始堆時,其複雜度爲O(n)O(n);
交換操作需 n-1 次;
重建堆的過程中近似爲nlognnlogn
堆排序時間複雜度爲O(nlogn)O(nlogn)

穩定性

堆排序是不穩定的:
比如:10,9,6,9;
如圖:

穩定性分析用圖

當堆頂元素10和末尾元素交換後,兩個9的相對位置發生改變。

歡迎關注

歡迎大家的關注

掃描下方的二維碼或者微信搜一搜即可關注我的微信公衆號:code隨筆

微信公衆號:code隨筆

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章