排序演算法 四 優先佇列 二叉堆以及堆排序

2022-04-22 03:10:49 字數 4215 閱讀 8580

我們經常會碰到下面這種情況,並不需要將所有資料排序,只需要取出資料中最大(或最小)的幾個元素,如排行榜。

那麼這種情況下就可以使用優先佇列,優先佇列是乙個抽象資料型別,最重要的操作就是刪除最大元素和插入元素,插入元素的時候就順便將該元素排序(其實是堆有序,後面介紹)了。

二叉堆其實是優先佇列的一種實現,下面主要講的是用陣列實現二叉堆。

如有乙個陣列a

用二叉樹來表示陣列更直觀:

從這張圖我們可以總結一些規律:

當乙個二叉樹的每個結點都大於等於它的兩個子節點時,稱為堆有序

根節點是堆有序的二叉樹中的最大結點

在陣列中,位置為k的結點的父節點,位置為k/2,它的兩個子節點位置分別為:2k和2k+1(下標從1開始,a[0]不使用)

上面這三點應該非常好理解

下面就引出乙個問題,怎樣讓乙個陣列變成堆有序呢?

首先,需要介紹兩個操作:

由下至上的堆有序化(上浮)

當插入乙個結點,或改變乙個結點的值時,上浮指的是交換它和它的父節點以達到堆有序

在上面的堆有序的圖中,如果我們把0換成10,那麼上浮的操作具體為:

(1)10比它的父節點7大,所以交換

(2)交換後,10比它的父節點9還要打,交換

之後得到的二叉樹如下圖:

**如下(需要注意,下標是從1開始,a[0]保留不用,以下所有**相同):

//

index based on 1

public

void

swim(integer a,integer key)

}

2.由上至下的堆有序化(下沉)由上浮可以很容易得出下沉的概念:

當插入乙個結點,或改變乙個結點的值時,下沉指的是交換它和它的較大子節點以達到堆有序。

在原來的二叉樹中,如果將根節點9換成4,操作如下:

(1)4與它的最大子節點8交換位置

(2)4與它的最大子節點6交換位置

交換後的二叉樹如下圖:

**如下:

//

index based on 1

public

void

sink(integer a,integer key)

else

if(a[key] >a[max])

break

;

change(a,key,max);

key =max;}}

那麼將乙個陣列構造成有序堆,相應的也有兩種方法:使用上浮以及使用下沉:

初始陣列如下:

integer a = ;
上浮構造有序堆:從陣列左邊到右邊依次使用上浮,因為根節點a[1]沒有父節點,所以從a[2]開始:

public

void

buildbinaryheapwithswim(integer a)

}

結果如下:

a: [null ,9 ,7 ,8 ,5 ,0 ,2 ,6 ,1 ,3] 讀者有興趣可以自己畫一下二叉樹,看是否有序
下沉構造有序堆:

**:

public

void

buildbinaryheapwithsink(integer a)

}

為什麼使用下沉只需要遍歷陣列左半邊呢?

因為對於乙個陣列,每乙個元素都已經是乙個子堆的根節點了,sink()對於這些自對也適用。如果乙個結點的兩個子節點都已經是有序堆了,那麼在該結點上呼叫sink(),可以讓整個陣列變成有序堆,這個過程會遞迴的建立起有序堆的秩序。我們只需要掃瞄陣列中一半的元素,跳過葉子節點。

a: [null ,9 ,7 ,8 ,3 ,0 ,6 ,5 ,1 ,2]
可以看到使用下沉和上浮構造出來的有序堆並不相同,那麼用哪乙個更好呢?

答案是使用下沉構造有序堆更好,構造乙個有n個元素的有序堆,只需少於2n次比較以及少於n次交換。

證明過程就略過了。

前面說了那麼多,終於要說到堆排序了,其實前面的優先佇列和二叉堆都是為了堆排序做準備。

現在我們知道如果將乙個陣列構造成有序堆的話,那麼陣列中最大的元素就是有序堆的根節點。

那麼很容易想到乙個排序的思路:

第一種:將陣列構造成有序堆,將根節點拿出來,即將a[1]拿出(因為a[0]不用,當然也可以使用,讀者可以自己程式設計實現),對剩下的陣列再構造有序堆……

不過第一種思路只能降序排列,並且需要構造乙個陣列用來存放取出的最大元素,以及最大的弊端是取出最大元素後,陣列剩下的其它所有元素需要左移。

那麼第二種辦法就可以避免以上的問題:

第二種:先看圖:

先來解釋下這幅圖:

一開始先將陣列構造成乙個有序二叉堆,如圖1

因為有序二叉堆的最大元素就是根節點,將根節點和最後乙個元素交換。

從index=1到index=a.lenth-1開始呼叫sink方法重新構造有序二叉堆。(即第二步交換過的最大元素不參與這次的構造)

經過第三步後,得到陣列中第二大的元素即為根節點。

再次交換根節點和倒數第二個元素

這樣迴圈下去,即得到按公升序排序的陣列

**:

public

void

heapsort(integer a)

integer n = a.length - 1;

while(n > 0)

}

注意在while迴圈中,sink()方法多了乙個引數,這個引數的目的是去掉上乙個有序堆的最大元素。

全部**如下:

public

class heapsort extends

sortbase

public

void

buildbinaryheapwithsink(integer a)

}public

void

buildbinaryheapwithswim(integer a)

}public

void

heapsort(integer a)

integer n = a.length - 1;

while(n > 0)

}//index based on 1

public

void

swim(integer a,integer key)

}//index based on 1

public

void

sink(integer a,integer key)

else

if(a[key] >a[max])

break

;

change(a,key,max);

key =max;}}

public

void

sink(integer a,integer key,integer n)

else

if(a[key] >a[max])

break

;

change(a,key,max);

key =max;}}

public

static

void

main(string args) ;

//(new heapsort()).sort(a);

(new

heapsort()).buildbinaryheapwithsink(a);

print("a",a);}}

堆排序的平均時間複雜度為nlogn

優先佇列 二叉堆

優先佇列 二叉堆 二叉堆是一棵完全二叉樹,最大堆 最小堆 中,所有根節點的鍵值都要比對應的子樹要大 小 由於是完全二叉樹,所以儲存結構可以採用陣列。最大堆的建立 include include include include include include define inf 0x3f3f3f3f...

二叉堆 優先佇列

堆 堆常見的二叉堆,這種資料結構有大根堆和小根堆。對於大根堆來說,每個父節點是大於他的兩個孩子節點的。也就是最大值在根節點。小根堆與之相反。如果堆用陣列實現的話,如果從1開始計數 因為0的位置可以在上慮或者下慮的時候做個暫存的位置 那麼乙個孩子的父節點是i 2 如果知道了父節點,而左孩子的節點位置為...

二叉堆(優先佇列)

0.1 本文總結於 資料結構與演算法分析,但源 均為原創 旨在理清二叉堆 優先佇列 堆的其他操作及其應用,以便讓朋友些知道為什麼要學習優先佇列 二叉堆 1.0 優先佇列定義 優先佇列是允許至少下列兩種操作的資料結構,insert 插入 它的工作時顯而易見的,以及 deletemin 刪除最小者 它的...