【資料結構與演算法】堆排序

gonghr 發表於 2021-08-06
演算法 資料結構

樹、二叉樹的簡單介紹

image

image

可以用陣列表示一顆二叉樹(陣列下標從0開始)

  • 左子節點下標是 2n+1 (n是父節點下標)
  • 右子節點下標是 2n+2 (n是父節點下標)
  • 父節點下標是 n/2-1 (n是左子節點或者右子節點下標)

堆的概念

  • 二叉堆是完全二叉樹或者是近似完全二叉樹
  • 二叉堆滿足兩個特性:
    • 父節點的鍵值總是大於或等於(小於或等於)任何一個子節點的鍵值
    • 每個節點的左子樹和右子樹都是一個二叉堆(都是最大堆或最小堆)
  • 任意節點的值都大於其子節點的值————大頂堆,堆的最大值在根節點。

image

  • 任意節點的值都小於其子節點的值————小頂堆,堆的最小值在根節點。

image

堆排序

步驟

image

  • 第一步:堆化

    • 反向調整使得每個子樹都是大頂堆或者小頂堆

    • 從n/2-1個元素開始向下修復,將每個節點修復為大(小)頂堆,修復完成後,陣列具有大(小)頂堆的性質

      舉個例子:
      初始給定陣列為[2,7,26,25,19,17]
      它所形成的二叉堆是
      image

      堆化以後變成大頂堆
      image

      陣列變為[26,25,17,7,19,2]

  • 第二步:調整

    • 不停地把堆頂未處理陣列的最後一個元素交換,把堆頂也就是較大元素放到陣列末端,每交換一次都要進行調整,維持二叉堆結構。

程式碼實現

法一:採用遞迴方式進行調整操作

    public static void main(String[] args) {
        int[] arr = {2,5,6,21,6,4,2,6,2};
        heapSort(arr);
        for (int i : arr) {
            System.out.print(i+" ");
        }
    }
    private static void heapSort(int[] arr){
        mikeMaxHeap(arr);                   //1.先進行堆化
        for(int x = arr.length-1;x>=0;x--){ //2.再進行調整
            swap(arr,0,x);  //把堆頂(0號元素)和最後一個元素對調
            maxHeapFixDown(arr,0,x);  //縮小堆的範圍,對堆頂元素進行向下調整
        }
    }
    private static void mikeMaxHeap(int[] arr) {
        int n = arr.length;
        for (int i = (n - 1 - 1) / 2; i >= 0; i--) { //從「第一個非葉子節點」開始向前調整,葉子節點沒必要調整
            maxHeapFixDown(arr, i, n);
        }
    }

    private static void maxHeapFixDown(int[] arr, int i, int size) {
        // 1.找到左右孩子
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        // 2.讓max指向了左右孩子中較大的那個
        if (left >= size)     //左孩子已經越界,i就是葉子節點
            return;
        int max = left; //先預設左右孩子中較大的那個是左孩子,簡化程式碼
        if (right >= size)    //右孩子越界,孩子中較大的就是左孩子
            max = left;
        else {          //左右孩子都沒越界,max指向較大的那個
            if (arr[right] > arr[left])
                max = right;
        }
        // 3.交換孩子節點和父親節點
        if(arr[i]>=arr[max])  // 如果arr[i]比兩個孩子都要大,不用調整
            return;

        swap(arr,i,max);   //否則,找到兩個孩子中較大的,和i交換

        // 4.大孩子那個位置的值發生了變化,i變更為大孩子那個位置,遞迴調整
        maxHeapFixDown(arr, max, size);
    }

    private static void swap(int[] arr, int a, int b) {
        int tmp = arr[a];
        arr[a] = arr[b];
        arr[b] = tmp;
    }

法二:採用迴圈方式進行調整操作

只需要修改maxHeapFixDown方法

    public static void main(String[] args) {
        int[] arr = {2,5,6,21,6,4,2,6,2};
        heapSort(arr);
        for (int i : arr) {
            System.out.print(i+" ");
        }
    }
    private static void heapSort(int[] arr) {
        mikeMaxHeap(arr);       //1.先進行堆化
        for (int x = arr.length - 1; x >= 0; x--) { //2.再進行調整
            swap(arr, 0, x);  //把堆頂(0號元素)和最後一個元素對調
            maxHeapFixDown(arr, 0, x);  //縮小堆的範圍,對堆頂元素進行向下調整
        }
    }

    private static void mikeMaxHeap(int[] arr) {
        int n = arr.length ;
        //從「第一個非葉子節點」開始向前調整,葉子節點沒必要調整
        for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
            maxHeapFixDown(arr, i, n);
        }
    }

    private static void maxHeapFixDown(int[] arr, int index, int size) {
        int left = index * 2 + 1;
        while (left < size) {
            //如果存在右節點,則largest等於左右節點中較大者的下標
            //如果不存在右節點,則largest等於左節點的下標
            //一條語句同時滿足兩個條件,選出子節點中的較大者給largest
            int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
            //選出子節點和父節點中的較大者
            largest = arr[largest] > arr[index] ? largest : index;
            if (largest == index) {
                break;
            }
            swap(arr, largest, index);  //交換
            index = largest;        //進入下一次迴圈(下沉),重新計算index和left
            left = index * 2 + 1;
        }
    }

    private static void swap(int[] arr, int a, int b) {
        int tmp = arr[a];
        arr[a] = arr[b];
        arr[b] = tmp;
    }

時間複雜度分析

第一步:建立大根堆的過程。

image

只從陣列第2n-1(n 是陣列長度)個元素開始倒著往前處理。
最後一層節點數大約n/2,因為是葉子節點,所以對其操作就只是看了一眼,運算元為1。
倒數第二層節點數大約n/4,操作是看一眼加上一次交換,運算元為2。
同理,倒數第三層節點數為n/8,運算元為3。
寫出總的複雜度公式:

\(\mathrm{T}\left( \mathrm{n} \right) =\frac{\mathrm{n}}{2}\times 1+\frac{\mathrm{n}}{4}\times 2+\frac{\mathrm{n}}{8}\times 3+\mathrm{……}\)

利用錯位相減法可以求出通項公式

\[\mathrm{T}\left( \mathrm{n} \right) =\frac{\mathrm{n}}{2}\times 1+\frac{\mathrm{n}}{4}\times 2+\frac{\mathrm{n}}{8}\times 3+\frac{\mathrm{n}}{16}\times 4+\mathrm{……} \\ 2\mathrm{T}\left( \mathrm{n} \right) =\frac{\mathrm{n}}{2}\times 2+\frac{\mathrm{n}}{2}\times 2+\frac{\mathrm{n}}{4}\times 3+\frac{\mathrm{n}}{8}\times 4+\mathrm{……} \\ \mathrm{T}\left( \mathrm{n} \right) =\mathrm{n}+\frac{\mathrm{n}}{2}+\frac{\mathrm{n}}{4}+\frac{\mathrm{n}}{8}+\mathrm{……} \\ \approx \mathrm{O}\left( \mathrm{n} \right) \]

所以把陣列變成大根堆的時間複雜度是O(n)

第二步:調整的過程需要交換 n 次,每交換一次都需要進行調整,每次調整複雜度是logn級別,總的複雜度是O(nlogn)

所以堆排序的總時間複雜度是O(nlogn)

空間複雜度分析

沒用申請額外空間,空間複雜度是 O(1)