堆(資料結構)及堆排序

2021-06-19 20:06:34 字數 4520 閱讀 9926

這裡的堆是指一種資料結構(或資料結構屬性),非指堆記憶體。堆屬性用二叉樹來體現,具堆屬性的資料結構才可被叫做為堆。具堆屬性的資料結構滿足以下筆記的「順序」和「形狀」兩個條件。

將某資料結構如陣列,將陣列的元素依次安排在二叉樹中的根結點、根結點的左孩子、根結點的右孩子位置之上,再將剩餘元素依次安排在根結點的左孩子的左孩子、根結點左孩子的右孩子、根結點右孩子的左孩子、根結點右孩子的右孩子位置之上

……按照如此順序得到的二叉樹,若每個根結點元素都小(大)於其左右孩子,則稱此二叉樹為小(大)堆,稱對應的陣列具有堆屬性(或此陣列此時就是堆資料結構)

[1]用來表示堆的二叉樹中不能有「洞」。

[2]用來表示堆的二叉樹最多有兩層具「終止結點」。

這兩點主要是為了保證堆中任意乙個結點(主要是指最後乙個終止結點)距離根結點的距離不超過

logn

(底數為2,

n表示二叉樹中的元素個數)。在

n十分大的時候,

logn比n

小太多了。這對於減少程式的時間複雜度很有用。

如果二叉樹不具有「堆形狀」屬性,那麼它就有可能不能限制二叉樹中任一結點到根結點的最大距離不超過

logn

。下圖包含不滿足「堆形狀」的二叉樹,它們表現出來的性質表現在圖中:

3層具終止結點

具洞的二叉樹不構成堆

堆的第乙個元素下標為

1(如果是自己編寫**,為

0也可以,找到堆資料結構中父結點和子結點之間的下標關係即可)。

因為看上了可以利用堆來實現時間複雜度不超過o(nlogn)的特色。

堆是建立在二叉樹之上的資料結構,只要將堆屬性賦予某一種資料結構,那麼此資料結構便也可以按照堆來操作。堆除了可以向二叉樹那樣進行元素遍歷之外,平常還主要操作堆的子節點跟父節點,這樣的操作即使在最壞的情況下長度也只有

logn

(底數為2,

n為元素總個數)。

2:遊走堆的父子結點

這是在堆中的父子結點的操作。這一點還可以用到建立某資料結構(如陣列)的堆屬性上。

《programmingpearls,程式設計珠璣》

中主要介紹了用堆屬性來實現兩個功能:實現優先佇列和堆排序,都用到了堆在父子結點之間遊走時時間複雜度不超過

logn

的特性。

看完程式設計珠璣堆

14章「堆」後,也覺得堆操作的最核心函式是遊走於父子結點的具上下移結點功能的函式(

siftup

,siftdown

)。因為程式設計珠璣中使用堆操作不與《資料結構》課程一樣採取純粹「建堆」操作來讓乙個陣列具「堆屬性」。如在前面「常見內部排序

」筆記中的「堆排序」就是採用「建堆」和「堆篩選」的思路來完成堆排序,所以關於堆的核心函式是「建堆函式」,這個過程中不需要額外的空間。程式設計珠璣中以往堆

a[i]

中加入新元素的方式來形成堆

a[i+1]

,在「優先佇列」和「堆排序」之上也是靠這個思想實現。每次新加入的元素和其父(子)節點發生可能的交換,直到小(大)等於當前父(子)結點或到了根(終止)結點,操作複雜度不超過

logn

(log(n-

陣列中剩餘元素個數

))。再加上外圍的

o(n)

,就得到了時間複雜度不超過o(nlogn)

的堆演算法,為陣列賦予堆屬性也可以不需要額外的空間。將與父結點比較交換的函式定義為

siftup

,與子結點比較交換的函式定義為

siftdown

,那麼在實現優先佇列和堆排序的時候,呼叫這兩個函式就可以了。

siftup()

函式的功能是往已經是堆的結構中加入新元素,以小堆為例,

siftup()

函式的偽碼可如下所示:

siftup(n)    //n表示堆最後乙個元素下標

}

siftup()

函式能夠在堆為空的情況或者

a[1...i]

為堆的情況下,依次在堆的最後位置加入新元素後,可以將堆擴充套件乙個元素後形成新堆。這段**的時間複雜度不超過

o(logn)

,操作n

個元素時時間複雜度不超過

o(nlogn)。

可以通過預設第乙個元素有序的方式通過以下**為乙個陣列實現堆屬性,不需要額外的空間:

for(i = 2; i <=n, ++i)

siftup(i);

時間複雜度不超過

o(nlogn)

,經此操作後,陣列

a[1...i]

也就具有了堆屬性。也可以說此時的陣列就是乙個堆資料結構。

siftdown()

的物件是堆。是將元素往下移和較小的子結點發生可能的交換,所以新加入的元素是在堆的根元素處。偽碼如下所示:

siftdown(n)    //n表示堆的的大小,最後乙個位置

}

操作單個、

n個元素的複雜度跟

siftup()

相同。但單用

siftdown()

不能直接為乙個陣列賦予堆屬性,因為它預設從所有的操作都會從第乙個元素到最後乙個。

優先佇列被定義時為空,然後提供插入、刪除最小最大等操作。那麼就可以不等優先佇列的所有元素插入完畢後,再對優先佇列進行「建堆」操作以賦予優先佇列「堆屬性」。可以利用

siftup()

函式,在每向優先佇列插入乙個元素時就將所有的元素調整為堆(往優先佇列中插入第乙個元素時,

siftup()

函式也能將乙個元素形成堆),插入

n個元素的時間複雜度不超過

o(nlogn)。

將優先佇列賦予堆屬性主要是為了方便優先佇列的其它操作,如刪除。當要尋找、刪除優先佇列的最小(大)元素時,只需要直接訪問小(大)堆的根元素(

q[1]

),然後將優先佇列最後乙個元素賦值到第乙個元素位置(優先佇列的大小

--),再重新呼叫

siftdown()

將優先佇列調整為堆即可。尋找、刪除

n個最小(大)元素的操作的時間複雜度不超過

o(nlogn)。

那麼用堆就可以形成乙個時間複雜度不超過

o(nlogn)

的函式或介面,屬比較高效的介面。在《

programming pearls

,程式設計珠璣》作者

jon bentley

那個年代指定的機器上執行,o(nlogn)

執行10^7(100m

左資料)

數量級需要

11s,若n成

10倍增長,時間會以大於

10倍的速度增長

。如面前擺著乙個陣列

a[n]

。要求時間複雜度不超過

o(nlogn)。

將a[n]

陣列元素乙個乙個插入到優先佇列中,在優先佇列內部形成小(大)堆。然後再由優先佇列的找最小(大)函式返回堆頂元素即可。時間複雜度符合要求。但確實在這個過程中會需要優先佇列中的堆結構來儲存來儲存

a[n]

陣列,會需要額外的

sizeof(a)

位元組空間。

這個過程分建堆和堆篩選。與前面筆記的「常見內部排序筆記」不同之處在於「建堆」的方法,這裡的簡單得多。

建堆,首先用前面筆記到的用

siftup()

賦予陣列對屬性的**建立堆:

for(i = 2; i <=n, ++i)

siftup(i);

堆篩選,思路及**實現跟「常見內部排序演算法筆記

」堆篩選一樣,都是通過交換最後乙個未被交換過的元素和堆頂元素來篩選出最小(大)的元素,從而使得整個堆有序。

利用這個方法得到有序堆的時間複雜度同樣不超過

o(nlogn)

,而且不需要額外的空間。

利用堆的o(

nlogn

)的時間複雜度可能還不是最好的選擇。如快速排序在排序物件隨機情況下的時間複雜度為

o(logn)

。所以在進行程式設計時要在審題清楚後選擇最合適的資料結構。當然程式的效率要根據需求來設計,如果根本不需要效率,用最簡單的演算法就天下太平了。 讀《

programming pearls

,程式設計珠璣》第

14章獲得的關鍵字:堆、庫、介面

。anote over。

資料結構 堆以及堆排序

堆可以看作是一棵完全二叉樹,除最後一層外,每一層都是填滿的,最後一層從左到右依次填入 在堆上,對任意乙個結點來說,越接近頂部,權值就越大 一般指大頂堆 並且它的權值大於等於它所在子樹所有點的權值 我們把根結點權值大於等於樹中結點權值的稱為大根堆,小於等於樹種結點權值的稱為小根堆 下圖就是乙個大根堆的...

資料結構 五 堆排序

1 演算法流程 1 對原始資料構建大根堆 a 從下至上,遍歷每個非葉子父節點,保證每個非葉子父節點都比它的左右子節點來的大,非葉子父節點的對應索引範圍為 0,n 2 1 b 在遍歷每個非葉子父節點的時候,如果發生該節點交換 下沉 那麼要遞迴下去 2 交換大根堆構建後的陣列的首個元素與末尾元素,這時候...

資料結構 堆與堆排序

堆其實是從完全二叉樹演變過來的並且用來儲存資料的,什麼是完全二叉樹呢?完全二叉樹就是 若設二叉樹的深度為h,除第h層外,其它各層 1 h 1 的結點數都達到最大個數,第h層所有的結點都連續集中 在最左邊,這就是完全二叉樹。我們知道二叉樹可以用陣列模擬,堆自然也可以。現在讓我們來畫一棵完全二叉樹 從圖...