1. 程式人生 > 實用技巧 >資料結構與演算法(十二):堆排序

資料結構與演算法(十二):堆排序

一、什麼是堆排序

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}進行排序。

第一遍排序

  1. 我們從最後一個非葉子結點開始排序。第一個非葉子結點為arr.length/2-1=5/2-1=1,也就是元素6.,我們對他進行對比並調整位置;

  2. 在{6,5,4}中,5比6小,而9比6大,所以9和6交換位置;

  3. 接著找到第二個非葉子節點4,由於9是{9,4,8}這個樹中最大的,故9與4交換位置

  4. 由於9與4交換位置打亂了原先{9,5,6}這棵樹順序,所以繼續對新樹{4,5,6}進行排序

  5. 由此得到了一個大頂堆,然後將堆頂元素9與末尾元素4進行交換,得到陣列{4,6,8,5,9}

至此,第一遍排序已經完成,我們確定了最大元素9的位置

第二遍排序

第二遍排序開始時,最大元素9的位置已經確定,實際上要排序的陣列變成了{4,6,8,5}

  1. 繼續從6開始比較,{6,5}排序正常,所以接著比較{4,6,8},8是最大的,所以與4交換位置

  2. 由此得到了一個大頂堆,然後將堆頂元素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;
    }

}