數據結構與算法之美-堆和堆排序

堆和堆排序


如何理解堆

堆是一種特殊的樹,只要知足如下兩點,這個樹就是一個堆。算法

①徹底二叉樹,徹底二叉樹要求除了最後一層,其餘層的節點個數都是滿的,最後一層的節點都靠左排列。api

②樹中每個結點的值都必須大於等於(或小於等於)其子樹中每一個節點的值。大於等於的狀況稱爲大頂堆,小於等於的狀況稱爲小頂堆。數組

 


如何實現堆


如何存儲一個堆

徹底二叉樹適合用數組來存儲,由於數組中對於下標從1開始的狀況,下標爲i的節點的左子節點就是下標爲i*2的節點,右子節點就是i下標爲i*2+1的節點,其父節點時下標爲i/2的節點緩存


堆支持哪些操做

往堆中插入一個元素數據結構

把新插入的元素放到堆的最後就不符合第二個特性了,因此咱們須要進行調整,讓其從新知足堆的特性,這個過程咱們起了一個名字,就叫做堆化(heapify)。性能

堆化就是順着節點所在的路徑,向上或者向下,對比,而後交換。咱們先使用從下往上的堆化方法。測試

讓新插入的節點與父節點對比大小。若是不知足子節點小於等於父節點的大小關係,咱們就互換兩個節點。一直重複這個過程,直到父子節點之間知足剛說的那種大小關係。ui

public class Heap{
    private int[] data;//數組,從下標1開始存儲
    private int maxNum;//數組容量
    private int count;//當前數組成員數量
    //構造器初始化數組,大小和數量
    public Heap(int size){
        data = new int[size + 1];
        maxNum = size;
        count = 0;
    }
    public void Insert(int item){
        //堆滿返回
        if (count >= maxNum) return;
        //先將節點插入堆尾
        data[count++] = item;
        int i = count;
        //再自下向上堆化,直到堆頂或者父節點比子節點大爲止
        while (i / 2 > 0 && data[i] > data[i / 2]){
            //交換位置
            int temp = data[i];
            data[i] = data[i / 2];
            data[i / 2] = temp;
            //更新下標
            i = i / 2;
        }
    }
}

刪除堆頂元素spa

根據對的第二條定義,堆頂元素存儲的就是堆中的最大值或最小值。code

這裏咱們使用從上往下的堆化方法。將最後一個節點放到堆頂,而後利用一樣的父子節點對比法,進行互換節點直到父子節點之間知足大小關係爲止。

這樣移除的就是數組中的最後一個元素,不會破環徹底二叉樹的定義。

public void RemoveMax(){
    //堆空返回
    if (count == 0) return;
    //將最後一個節點提到堆頂
    data[1] = data[count--];
    //進行堆化
    Heapify(data,count,1);
}
public static void Heapify(int[] data,int n,int i){
    while (true){
        //記錄更大節點的位置,初始化爲當前節點的位置
        int maxPos = i;
        //若是其左右子節點存在,且比當前節點大,就將左右節點下標設爲更大的節點
        if (i * 2 <= n && data[i] < data[i * 2]) maxPos = i * 2;
        if (i * 2 + 1 <= n && data[maxPos] < data[i * 2 + 1]) maxPos = i * 2 + 1;
        //不然就結束循環,堆化結束
        if (maxPos == i) break;
        //節點交換位置
        int temp = data[i];
        data[i] = data[maxPos];
        data[maxPos] = temp;
        //更新當前節點的下標,循環繼續與下一個左右子節點比較
        i = maxPos;
    }
}

 


如何基於堆實現排序

咱們藉助於堆這種數據結構實現的排序算法,就叫做堆排序。

咱們能夠把堆排序的過程大體分解成兩個大的步驟,建堆和排序。


建堆

首先將數組原地建成一個堆。藉助另外一個數組,就在原數組上操做。咱們要實現從後往前處理數組,而且每一個數據都是從上往下堆化的建堆方法。

public static void BuildHeap(int[] data, int n){
    //從下標n/2到1開始進行堆化,n/2就是最後一個葉子節點的父節點。
    for (int i = n / 2; i >= 1; --i)
        Heapify(data,n,i);
}

咱們對下標從n/2開始到 111 的數據進行堆化,下標是n/2+1到n的節點是葉子節點,咱們不須要堆化。

建堆操做的時間複雜度

排序的建堆過程的時間複雜度是 O(n)。


排序

建堆結束以後,數組中的數據已是按照大頂堆的特性來組織的。數組中的第一個元素就是堆頂,也就是最大的元素。咱們把它跟最後一個元素交換,那最大元素就放到了下標爲n的位置。

這個過程有點相似刪除堆頂元素的操做,當堆頂元素移除以後,咱們把下標爲n的元素放到堆頂,而後再經過堆化的方法,將剩下的n-1個元素從新構建成堆。

堆化完成以後,咱們再取堆頂的元素,放到下標是的位置,一直重複這個過程,直到最後堆中只剩下標爲1的一個元素,排序工做就完成了。

public static void Sort(int[] data,int n){
    //將數組建造爲堆
    BuildHeap(data, n);
    //獲取堆尾的下標
    int k = n;
    //循環直到k爲1
    while (k > 1){
        //交換堆頂和堆尾的元素
        int temp = data[k];
        data[k] = data[1];
        data[1] = temp;
        //將堆尾的下標遞減並對1到k的下標的數組成員進行堆化
        Heapify(data,--k,1);
    }
}

堆排序的時間複雜度、空間複雜度以及穩定性

堆排序是原地排序算法。堆排序包括建堆和排序兩個操做,建堆過程的時間複雜度是O(n),排序過程的時間複雜度是O(nlogn)因此,堆排序總體的時間複雜度是O(nlogn)。

堆排序不是穩定的排序算法,由於在排序的過程,存在將堆的最後一個節點跟堆頂節點互換的操做,因此就有可能改變值相同數據的原始相對順序。

測試 

//Main方法
int[] data = new int[] {0,3,5,2,9,4,7 };
Heap.Sort(data,data.Length-1);
for (int i=0;i<data.Length;i++)
    Console.Write(data[i]+",");
//測試結果
0,2,3,4,5,7,9,

數組的第1個成員,即下標0的數據是不做爲數據的一部分的,這是爲了算法上的方便,若是下標是從0開始,那麼左右子節點的下標公式就是i*2+1和i*2+2。

 


思考

在實際開發中,爲何快速排序要比堆排序性能好?

對於快速排序來講,數據是順序訪問的而對於堆排序來講,數據是跳着訪問的。這樣對 CPU 緩存是不友好的。

對於一樣的數據,在排序過程當中,堆排序算法的數據交換次數要多於快速排序。堆排序的第一步是建堆,建堆的過程會打亂數據原有的相對前後順序,致使原數據的有序度下降。好比,對於一組已經有序的數據來講,通過建堆以後,數據反而變得更無序了。

相關文章
相關標籤/搜索