1. 程式人生 > >淺談演算法和資料結構: 五 優先順序佇列與堆排序

淺談演算法和資料結構: 五 優先順序佇列與堆排序

在很多應用中,我們通常需要按照優先順序情況對待處理物件進行處理,比如首先處理優先順序最高的物件,然後處理次高的物件。最簡單的一個例子就是,在手機上玩遊戲的時候,如果有來電,那麼系統應該優先處理打進來的電話。

在這種情況下,我們的資料結構應該提供兩個最基本的操作,一個是返回最高優先順序物件,一個是新增新的物件。這種資料結構就是優先順序佇列(Priority Queue) 。

本文首先介紹優先順序佇列的定義,有序和無序陣列以及堆資料結構實現優先順序佇列,最後介紹了基於優先順序佇列的堆排序(Heap Sort)

一 定義

優先順序佇列和通常的棧和佇列一樣,只不過裡面的每一個元素都有一個”優先順序”,在處理的時候,首先處理優先順序最高的。如果兩個元素具有相同的優先順序,則按照他們插入到佇列中的先後順序處理。

優先順序佇列可以通過連結串列,陣列,堆或者其他資料結構實現。

二 實現

陣列

最簡單的優先順序佇列可以通過有序或者無序陣列來實現,當要獲取最大值的時候,對陣列進行查詢返回即可。程式碼實現起來也比較簡單,這裡就不列出來了。

unordered and ordered array implementation for priority queue

如上圖:

· 如果使用無序陣列,那麼每一次插入的時候,直接在陣列末尾插入即可,時間複雜度為O(1),但是如果要獲取最大值,或者最小值返回的話,則需要進行查詢,這時時間複雜度為O(n)。

· 如果使用有序陣列,那麼每一次插入的時候,通過插入排序將元素放到正確的位置,時間複雜度為O(n),但是如果要獲取最大值的話,由於元阿蘇已經有序,直接返回陣列末尾的 元素即可,所以時間複雜度為O(1).

所以採用普通的陣列或者連結串列實現,無法使得插入和排序都達到比較好的時間複雜度。所以我們需要採用新的資料結構來實現。下面就開始介紹如何採用二叉堆(binary heap)來實現優先順序佇列

二叉堆

二叉堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。 有了這一性質,那麼二叉堆上最大值就是根節點了。

二叉堆的表現形式:我們可以使用陣列的索引來表示元素在二叉堆中的位置。

Heap representation

從二叉堆中,我們可以得出:

· 元素k的父節點所在的位置為[k/2]

· 元素k的子節點所在的位置為2k和2k+1

跟據以上規則,我們可以使用二維陣列的索引來表示二叉堆。通過二叉堆,我們可以實現插入和刪除最大值都達到O(nlogn)的時間複雜度。

對於堆來說,最大元素已經位於根節點,那麼刪除操作就是移除並返回根節點元素,這時候二叉堆就需要重新排列;當插入新的元素的時候,也需要重新排列二叉堆以滿足二叉堆的定義。現在就來看這兩種操作。

從下至上的重新建堆操作: 如果一個節點的值大於其父節點的值,那麼該節點就需要上移,一直到滿足該節點大於其兩個子節點,而小於其根節點為止,從而達到使整個堆實現二叉堆的要求。

Swim operation in Priority queue

由上圖可以看到,我們只需要將該元素k和其父元素k/2進行比較,如果比父元素大,則交換,然後迭代,一直到比父元素小為止。

private static void Swim(int k)
{
    //如果元素比其父元素大,則交換
    while (k > 1 && pq[k].CompareTo(pq[k / 2]) > 0)
    {
        Swap(pq, k, k / 2);
        k = k / 2;
    }
}

這樣,往堆中插入新元素的操作變成了,將該元素從下往上重新建堆操作:

Insert item into the binary heap

程式碼實現如下:

public static void Insert(T s)
{
    //將元素新增到陣列末尾
    pq[++N] = s;
    //然後讓該元素從下至上重建堆
    Swim(N);
}

動畫如下:

inset item into the binary heap

由上至下的重新建堆操作:當某一節點比其子節點要小的時候,就違反了二叉堆的定義,需要和其子節點進行交換以重新建堆,直到該節點都大於其子節點為止:

Sink operation in Priority queue

程式碼實現如下:

private static void Sink(int k)
{
    while (2 * k < N)
    {
        int j = 2 * k;
        //去左右子節點中,稍大的那個元素做比較
        if (pq[j].CompareTo(pq[j + 1]) < 0) j++;
        //如果父節點比這個較大的元素還大,表示滿足要求,退出
        if (pq[k].CompareTo(pq[j]) > 0) break;
        //否則,與子節點進行交換
        Swap(pq, k, j);
        k = j;
    }
}

這樣,移除並返回最大元素操作DelMax可以變為:

1. 移除二叉堆根節點元素,並返回

2. 將陣列中最後一個元素放到根節點位置

3. 然後對新的根節點元素進行Sink操作,直到滿足二叉堆要求。

移除最大值並返回的操作如下圖所示:

Delete the max item in binary heap

以上操作的實現如下:

public static T DelMax()
{
    //根元素從1開始,0不存放值
    T max = pq[1];
    //將最後一個元素和根節點元素進行交換
    Swap(pq, 1, N--);
    //對根節點從上至下重新建堆
    Sink(1);
    //將最後一個元素置為空
    pq[N + 1] = default(T);
    return max;
}

動畫如下:

del max in the binary heap

三 堆排序

Sorting_heapsort_anim

概念

運用二叉堆的性質,可以利用它來進行一種就地排序,該排序的步驟為:

1. 使用序列的所有元素,建立一個最大堆。

2. 然後重複刪除最大元素。

如下圖,以對S O R T E X A M P L E 排序為例,首先本地構造一個最大堆,即對節點進行Sink操作,使其符合二叉堆的性質。

然後再重複刪除根節點,也就是最大的元素,操作方法與之前的二叉堆的刪除元素類似。

heapsort

建立最大二叉堆

使用至下而上的方法建立二叉堆的方法為,分別對葉子結點的上一級節點以重上之下的方式重建堆。

程式碼如下:

for (int k = N / 2; k >= 1; k--)
{
    Sink(pq, k, N);
}

Build heap using bottom-up method

排序

利用二叉堆排序其實就是迴圈移除頂部元素到陣列末尾,然後利用Sink重建堆的操作。如下圖,實現程式碼如下:

while (N > 1)
{
    Swap(pq, 1, N--);
    Sink(pq, 1, N);
}

Remove the max in the heap sort

堆排序的動畫如下:

heap sort animattion

分析

1. 在構建最大堆的時候,最多需要2N次比較和交換

2. 堆排序最多需要2NlgN次比較和交換操作

優點:堆排序最顯著的優點是,他是就地排序,並且其最壞情況下時間複雜度為NlogN。經典的合併排序不是就地排序,它需要線性長度的額外空間,而快速排序其最壞時間複雜度為N2

heap sort2

缺點:堆排序對時間和空間都進行了優化,但是:

1. 其內部迴圈要比快速排序要長。

2. 並且其操作在N和N/2之間進行比較和交換,當陣列長度比較大的時候,對CPU快取利用效率比較低。

3. 非穩定性排序。

四 排序演算法的小結

本文及前面文章介紹了選擇排序插入排序希爾排序合併排序快速排序以及本文介紹的堆排序。各排序的穩定性,平均,最壞,最好的時間複雜度如下表:

Sort summary

可以看到,不同的排序方法有不同的特徵,有的速度快,但是不穩定,有的穩定,但是不是就地排序,有的是就地排序,但是最壞情況下時間複雜度不好。那麼有沒有一種排序能夠集合以上所有的需求呢?

五 結語

本文介紹了二叉堆,以及基於二叉堆的堆排序,他是一種就地的非穩定排序,其最好和平均時間複雜度和快速排序相當,但是最壞情況下的時間複雜度要優於快速排序。但是由於他對元素的操作通常在N和N/2之間進行,所以對於大的序列來說,兩個運算元之間間隔比較遠,對CPU快取利用不太好,故速度沒有快速排序快。

轉載自:https://www.cnblogs.com/yangecnu/p/Introduce-Priority-Queue-And-Heap-Sort.html