資料結構與演算法系列19 堆

2021-09-02 08:17:15 字數 4357 閱讀 2886

堆其實就是一種特殊的樹,那它特殊在**呢?只要滿足了以下兩點的,我們就可以稱之為堆。

1.堆是乙個完全二叉樹

2.堆中每乙個節點的值都必須大於等於(或者小於等於)其子樹中每個節點的值。

這裡稍作解釋,對於第一點,我們前面講過,完全二叉樹就是除了最後一層,其他層的節點個數都是滿的,最後一層的節點都靠左排列。

對於第二點,其實我們可以換一種說法,堆中每乙個節點的值都必須大於等於(或者小於等於)其左右子節點的值。對於堆中每乙個節點的值都大於等於子樹中每乙個節點值的堆,我們叫做「大頂堆」,對於堆中每乙個節點的值都小於等於子樹中每乙個節點值的堆,我們叫做「小頂堆」

前面我們說過,完全二叉樹適合使用陣列來儲存,用陣列來儲存可以非常的節省儲存空間,因為我們不用像使用鍊錶那樣,需要儲存左右子節點的指標,單純的通過陣列的下標,就可以找到乙個節點的左右子節點和父節點。下面這張圖是用陣列儲存堆的乙個例子,可以看下(來自極客時間《資料結構與演算法之美》)

從圖中我們可以看到,下標為i的節點的左子節點,就是下標為i2的節點,右子節點就是下標為i2+1的節點,父節點就是下標為i/2的節點。

1.插入乙個元素

插入乙個元素後,我們仍然必須滿足堆的兩個特性,所以要想插入乙個元素,我們必須先查找到乙個合適的位置再給它插入,這樣也就需要不斷調整,讓其滿足堆的特性,這個進行調整的過程,我們叫做堆化(heapify)

堆化的過程是這樣的,我們順著節點所在的路徑,向上或者向下,對比,然後交換。例如:我們可以讓新插入的節點與父節點對比大小,如果不滿足子節點小於等於父節點的大小關係,我們就互換兩個節點。一直重複這個過程,直到父子節點之間滿足剛才說的那種大小關係。(這裡以大頂堆作為例子)

結合著**看下:

public class heap 

public void insert(int data)

} }

2.刪除堆頂元素從堆的定義的第二條中,任何節點的值都大於等於(或小於等於)子樹節點的值,我們可以發現,堆頂元素儲存的就是堆中資料的最大值或者最小值。

具體怎麼做呢?

我們把最後乙個節點放到堆頂,然後利用同樣的父子節點對比方法。對於不滿足父子節點大小關係的,互換兩個節點,並且重複進行這個過程,直到父子節點之間滿足大小關係為止。

因為我們移除的是陣列中的最後乙個元素,而在堆化的過程中,都是交換操作,不會出現陣列中的「空洞」,所以這種方法堆化之後的結果,肯定滿足完全二叉樹的特性。

**例項:

public void removemax() 

private void heapify(int a, int n, int i)

}

乙個包含n個節點的完全二叉樹,樹的高度不會超過logn。堆化的過程是順著節點所在路徑比較交換的,所以堆化的時間複雜度跟樹的高度成正比,也就是o(logn)。插入資料和刪除堆頂元素的主要邏輯就是堆化,所以,往堆中插入乙個元素和刪除堆頂元素的時間複雜度都是o(log⁡n)。

所謂的堆排序,就是我們借助於堆這種資料結構實現的排序演算法,就叫作堆排序。這種排序方法的時間複雜度非常穩定,是o(nlogn)(和快排一樣),並且它還是原地排序演算法。

假設現在我們有乙個大頂堆,陣列中的第乙個元素就是堆頂,也就是最大的元素。我們把它跟最後乙個元素交換,那最大元素就放到了下標為n的位置。

這個過程有點類似上面講的「刪除堆頂元素」的操作,當堆頂元素移除之後,我們把下標為n的元素放到堆頂,然後再通過堆化的方法,將剩下的n−1個元素重新構建成堆。堆化完成之後,我們再取堆頂的元素,放到下標是n−1的位置,一直重複這個過程,直到最後堆中只剩下標為1的乙個元素,排序工作就完成了。

例項**:

// n 表示資料的個數,陣列 a 中的資料從下標 1 到 n 的位置。

public static void sort(int a, int n)

}

事實上,堆排序包括建堆和排序兩個過程。建堆就是構造乙個堆,它的時間複雜度是o(n),而排序過程的時間複雜度是o(nlogn),所以堆排序的整體時間複雜度是o(nlogn)。

堆排序不是穩定的排序演算法,因為在排序的過程,存在將堆的最後乙個節點跟堆頂節點互換的操作,所以就有可能改變值相同資料的原始相對順序。

堆排序(這個我們上面講過),優先順序佇列,求topk,求中位數。

應用一:優先順序佇列

優先順序佇列它本身是乙個佇列,我們知道佇列的特性就是先進先出,但是對於優先順序佇列,資料的出隊順序並不是先進先出,而是安裝優先順序來的,優先順序高的,最先出隊。

實現優先順序佇列的方法有很多,但是用堆來實現最直接,最高效。這是因為堆和優先順序佇列很相似,乙個堆可以看作乙個優先順序佇列。往優先順序佇列中插入乙個元素,就相當於往堆中插入乙個元素;從優先順序佇列中取出優先順序最高的元素,就相當於取出堆頂元素。

例1合併有序小檔案:

假設我們有100個小檔案,每個檔案的大小是100mb,每個檔案中儲存的都是有序的字串。我們希望將這些100個小檔案合併成乙個有序的大檔案。這裡就會用到優先順序佇列,也可以說是堆。將從小檔案中取出來的字串放入到小頂堆中,那堆頂的元素,也就是優先順序佇列隊首的元素,就是最小的字串。我們將這個字串放入到大檔案中,並將其從堆中刪除。然後再從小檔案中取出下乙個字串,放入到堆中。迴圈這個過程,就可以將100個小檔案中的資料依次放入到大檔案中。

刪除堆頂資料和往堆中插入資料的時間複雜度都是o(logn),n表示堆中的資料個數,這裡就是100。

例2高效能定時器:

假設我們有乙個定時器,定時器中維護了很多定時任務,每個任務都設定了乙個要觸發執行的時間點。定時器每過乙個很小的單位時間(比如1秒),就掃瞄一遍任務,看是否有任務到達設定的執行時間。如果到達了,就拿出來執行。但是這種每過1秒就掃瞄一遍任務列表的做法比較低效,主要原因有兩點:第一,任務的約定執行時間離當前時間可能還有很久,這樣前面很多次掃瞄其實都是徒勞的;第二,每次都要掃瞄整個任務列表,如果任務列表很大的話,勢必會比較耗時。

看看如何用優先順序佇列來解決,按照任務設定的執行時間,將這些任務儲存在優先順序佇列中,佇列首部(也就是小頂堆的堆頂)儲存的是最先執行的任務。這樣就不用每隔1s去掃瞄了,拿到隊首任務的執行時間與當前的時間點相減,得到乙個時間間隔t。這個時間間隔t就是從當前時間開始,需要等待多久,才會有第乙個任務需要被執行。這樣我們的定時器就可以設定在t秒後再來執行這個任務,而不需要每隔一秒去掃瞄,整體效能就提高了。

應用二:求top k問題

求top k問題可以分為兩類,一種是針對靜態資料集合,一種是針對動態資料集合。

針對靜態資料集合,如何在乙個包含n個資料的陣列中,查詢前k大資料呢?

可以通過維護乙個大小為k的小頂堆,順序遍歷陣列,從陣列中取出取資料與堆頂元素比較。如果比堆頂元素大,我們就把堆頂元素刪除,並且將這個元素插入到堆中;如果比堆頂元素小,則不做處理,繼續遍歷陣列。這樣等陣列中的資料都遍歷完之後,堆中的資料就是前k大資料了。

遍歷陣列需要o(n)的時間複雜度,一次堆化操作需要o(logk)的時間複雜度,所以最壞情況下,n個元素都入堆一次,所以時間複雜度就是o(nlogk)。

針對動態資料集合,我們可以一直都維護乙個k大小的小頂堆,當有資料被新增到集合中時,我們就拿它與堆頂的元素對比。如果比堆頂元素大,我們就把堆頂元素刪除,並且將這個元素插入到堆中;如果比堆頂元素小,則不做處理。這樣,無論任何時候需要查詢當前的前k大資料,我們都可以裡立刻返回給他。

應用三:求中位數

如何求動態資料集合中的中位數?

針對動態資料集合,中位數在不停地變動,如果採用先排序的方法,每次詢問中位數的時候,都要先進行排序,那效率就不高了。

事實上,借助堆這種資料結構,我們不用排序,就可以非常高效地實現求中位數操作。具體如何做呢?

我們需要維護兩個堆,乙個大頂堆,乙個小頂堆。大頂堆中儲存前半部分資料,小頂堆中儲存後半部分資料,且小頂堆中的資料都大於大頂堆中的資料。這樣,大頂堆中的堆頂元素就是我們要找的中位數。

那當新新增乙個資料的時候,我們如何調整兩個堆,讓大頂堆中的堆頂元素繼續是中位數呢?

如果新加入的資料小於等於大頂堆的堆頂元素,我們就將這個新資料插入到大頂堆;否則插入小頂堆中。

但是這樣就有可能出現,兩個堆中的資料個數不符合前面約定的情況,那怎樣解決呢?我們可以從乙個堆中不停地將堆頂元素移動到另乙個堆,通過這樣的調整,來讓兩個堆中的資料滿足上面的約定。

插入資料因為需要涉及堆化,所以時間複雜度變成了o(logn),但是求中位數我們只需要返回大頂堆的堆頂元素就可以了,所以時間複雜度就是o(1)。

python演算法與資料結構(19)堆

堆 一種完全二叉樹,有最大堆和最小堆兩種。最大堆 根總是最大值,最小的值儲存在葉節點中,最小堆 每個非葉子節點的兩個孩子的值都比它大。堆的操作 插入新的值,依然保證堆的最大堆或者最小堆的結構。刪除乙個值。堆的表示 使用陣列表示堆。parent int i 1 2 left 2i 1 right 2i...

資料結構與演算法系列 字典樹

一 背景 什麼是字典樹?trie樹,即字典樹,又稱單詞查詢樹或鍵樹,是一種樹形結構,是一種雜湊樹的變種。典型應用是用於統計和排序大量的字串 但不僅限於字串 所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是 最大限度地減少無謂的字串比較,查詢效率比雜湊表高。trie的核心思想是空間換時間。利用字串的...

資料結構與演算法系列 Sunday演算法詳解

一 背景 sunday演算法是daniel m.sunday於1990年提出的字串模式匹配。其效率在匹配隨機的字串時比其他匹配演算法還要更快。sunday演算法的實現可比kmp,bm的實現容易太多。二 分析假設我們有如下字串 a lessons tearned in software te b so...