算法#10--用簡單的思維理解堆排序

定義

首先理解什麼是堆。 堆(英語:Heap)是計算機科學中一類特殊的數據結構的統稱。堆通常是一個可以被看做一棵樹的數組對象。

n個元素序列{k1,k2…ki…kn},當且僅當滿足下列關係時稱之爲堆:

(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n\/2)

堆的實現通過構造二叉堆(binary heap),實爲二叉樹的一種;由於其應用的普遍性,當不加限定時,均指該數據結構的這種實現。

堆(二叉堆)可以視爲一棵完全的二叉樹,完全二叉樹的一個“優秀”的性質是,除了最底層之外,每一層都是滿的,這使得堆可以利用數組來表示(普通的一般的二叉樹通常用鏈表作爲基本容器表示),每一個結點對應數組中的一個元素。

如下圖,是一個堆和數組的相互關係 :

對於給定的某個結點的下標 i,可以很容易的計算出這個結點的父結點、孩子結點的下標:

  • Parent(i) = floor(i\/2),i 的父節點下標
  • Left(i) = 2i,i 的左子節點下標
  • Right(i) = 2i + 1,i 的右子節點下標

二叉堆一般分爲兩種:最大堆和最小堆。

最大堆

  • 最大堆中的最大元素值出現在根結點(堆頂)
  • 堆中每個父節點的元素值都大於等於其孩子結點(如果存在)

最小堆

  • 最小堆中的最小元素值出現在根結點(堆頂)
  • 堆中每個父節點的元素值都小於等於其孩子結點(如果存在)

堆排序

原理

堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法 。

通常堆是通過一維數組來實現的。在數組起始位置爲0的情形中:

  • 父節點i的左子節點在位置(2*i+1);
  • 父節點i的右子節點在位置(2*i+2);
  • 子節點i的父節點在位置floor((i-1)\/2);

在堆的數據結構中,堆中的最大值總是位於根節點。堆中定義以下幾種操作:

  • 最大堆調整(Max_Heapify):將堆的末端子節點作調整,使得子節點永遠小於父節點
  • 創建最大堆(Build_Max_Heap):將堆所有數據重新排序
  • 堆排序(HeapSort):移除位在第一個數據的根節點,並做最大堆調整的遞歸運算

最大堆調整(MAX‐HEAPIFY)

作用是保持最大堆的性質,是創建最大堆的核心子程序,作用過程如圖所示:

由於一次調整後,堆仍然違反堆性質,所以需要遞歸的測試,使得整個堆都滿足堆性質。–遞歸代碼maxHeapifyByRecursion部分。

遞歸調用需要壓棧\/清棧,和迭代相比,性能上有略微的劣勢 。–迭代代碼maxHeapifyByIteration部分。

創建最大堆(Build-Max-Heap)

是將一個數組改造成一個最大堆,自下而上的調用 Max-Heapify 來改造數組,建立最大堆。因爲 Max-Heapify 能夠保證下標 i 的結點之後結點都滿足最大堆的性質,所以自下而上的調用 Max-Heapify 能夠在改造過程中保持這一性質。如果最大堆的數量元素是 n,那麼 Build-Max-Heap 從 Parent(n) 開始,往上依次調用 Max-Heapify。流程如下:

堆排序(HeapSort)

是堆排序的接口算法,Heap-Sort先調用Build-Max-Heap將數組改造爲最大堆,然後將堆頂和堆底元素交換,之後將底部上升,最後重新調用Max-Heapify保持最大堆性質。由於堆頂元素必然是堆中最大的元素,所以一次操作之後,堆中存在的最大元素被分離出堆,重複n-1次之後,數組排列完畢。整個流程如下:

代碼

https://github.com/tclxspy/Articles/blob/master/algorithm/Code/HeapSort.java


public class HeapSort {    

    /**
     * the implement method of HeapSort
     * @param data which prepare for sorting
     * @return the array after sorted
     */
    public static int[] heapSort(int[] data) {    
        //from the end index, build the Max-Heapify        
        int startIndex = getParentIndex(data.length - 1);
        for(int i = startIndex; i >= 0; i--) {
            maxHeapifyByIteration(data, data.length, i);
            //or
            //maxHeapifyByIteration(data, data.length, i);
        }

        //swap the head and end. then maintain the Max-Heapify
        for (int i = data.length - 1; i > 0; i--) {
            swap(data, 0, i);
            maxHeapifyByIteration(data, i, 0);
            //or
            //maxHeapifyByIteration(data, i, 0);
        }
        return data;
    }
    /**
     * build the Max-Heapify by recursion method
     * @param data input data
     * @param heapSize length of data
     * @param index from the end index, build the Max-Heapify
     */
    private static void maxHeapifyByRecursion(int[] data, int heapSize, int index) {        
        //get the left and right index
        int left = getChildLeftIndex(index);
        int right = getChildRightIndex(index);

        //get the max data's index
        int largest = index;
        if(left < heapSize && data[index] < data[left]) {
            largest = left;
        }

        if(right < heapSize && data[largest] < data[right]) {
            largest = right;
        }

        //swap the max data to parent node. and then maintain the Max-Heapify again
        if(largest != index) {
            swap(data, largest, index);
            maxHeapifyByRecursion(data, heapSize, largest);
        }        
    }

    /**
     * build the Max-Heapify by iteration method
     * @param data input data
     * @param heapSize length of data
     * @param index from the end index, build the Max-Heapify
     */
    private static void maxHeapifyByIteration(int[] data, int heapSize, int index) {    
        while(true) {
            //get the left and right index
            int left = getChildLeftIndex(index);
            int right = getChildRightIndex(index);

            //get the max data's index
            int largest = index;
            if(left < heapSize && data[index] < data[left]) {
                largest = left;
            }

            if(right < heapSize && data[largest] < data[right]) {
                largest = right;
            }

            //swap the max data to parent node. and then maintain the Max-Heapify again
            if(largest != index) {
                swap(data, largest, index);
                index = largest;
            }    
            else {
                break;
            }
        }
    }

    /**
     * swap data[i] and data[j]
     * @param data
     * @param i
     * @param j
     */
    private static void swap(int[] data, int i, int j) {
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }

    private static int getParentIndex(int current) {
        return (current - 1) / 2;
    }

    private static int getChildLeftIndex(int current) {
        return 2 * current + 1;
    }

    private static int getChildRightIndex(int current) {
        return 2 * (current + 1);
    }    

    public static void main(String[] args) {    
        int[] sort = new int[]{1, 0, 10, 20, 3, 5, 6, 4, 9, 8, 12, 17, 34, 11};        
        int[] result = heapSort(sort);
        for (int i = 0; i < result.length; i++) {
            System.out.print(result[i] + " ");
        }
    }
}

輸出

0 1 3 4 5 6 8 9 10 11 12 17 20 34 

複雜度

最差時間複雜度 О(nlogn)

最優時間複雜度 О(nlogn)

平均時間複雜度 О(nlogn)

最差空間複雜度 總共О(n), 需要輔助空間O(1)

函數maxHeapify將指定子樹的根節點”下沉”到合適的位置, 最終子樹變成最大堆, 該過程最壞時間複雜度爲O(logn)。因此總共時間複雜度爲 O(nlogn) 。

堆排序算法的空間複雜度是O(1),從實現上很容易看出來,也叫原地堆排序

動態圖:

堆排序在排序複雜性的研究中有着重要的地位,因爲它是我們所知的唯一能夠同時最優地利用空間和時間的方法–在最壞的情況下它也能保證使用~2nlogn次比較和恆定的額外空間。當空間十分緊張的時候(例如在嵌入式系統或低成本的移動設備中)它很流行。

平均時間上,堆排序的時間常數比快排要大一些,因此通常會慢一些,但是堆排序最差時間也是O(nlogn)的,這點比快排好。

快排在遞歸進行部分的排序的時候,只會訪問局部的數據,因此緩存能夠更大概率的命中;而堆排序的建堆過程是整個數組各個位置都訪問到的,後面則是所有未排序數據各個位置都可能訪問到的,所以不利於緩存發揮作用。簡答的說就是快排的存取模型的局部性(locality)更強,堆排序差一些。

速度和緩存的問題都反映了堆排序讓數據過於大距離的移動,你觀察某個元素在整個排序過程中的移動過程,會發現它是前後大幅度的跑動;而快速排序則是儘快的移動到最終的位置,然後做小範圍的跳動。

發佈了45 篇原創文章 · 獲贊 21 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章