日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

数据结构--堆Heap

發(fā)布時間:2024/3/13 编程问答 58 豆豆
生活随笔 收集整理的這篇文章主要介紹了 数据结构--堆Heap 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

數(shù)據(jù)結(jié)構(gòu):堆(Heap)

堆就是用數(shù)組實現(xiàn)的二叉樹,所以它沒有使用父指針或者子指針。堆根據(jù)“堆屬性”來排序,“堆屬性”決定了樹中節(jié)點的位置。

堆的常用方法:

  • 構(gòu)建優(yōu)先隊列
  • 支持堆排序
  • 快速找出一個集合中的最小值(或者最大值)
  • 在朋友面前裝逼

堆屬性

堆分為兩種:最大堆和最小堆,兩者的差別在于節(jié)點的排序方式。

在最大堆中,父節(jié)點的值比每一個子節(jié)點的值都要大。在最小堆中,父節(jié)點的值比每一個子節(jié)點的值都要小。這就是所謂的“堆屬性”,并且這個屬性對堆中的每一個節(jié)點都成立。

例子:

這是一個最大堆,,因為每一個父節(jié)點的值都比其子節(jié)點要大。10 比 7 和 2 都大。7 比 5 和 1都大。

根據(jù)這一屬性,那么最大堆總是將其中的最大值存放在樹的根節(jié)點。而對于最小堆,根節(jié)點中的元素總是樹中的最小值。堆屬性非常有用,因為堆常常被當做優(yōu)先隊列使用,因為可以快速地訪問到“最重要”的元素。

注意:堆的根節(jié)點中存放的是最大或者最小元素,但是其他節(jié)點的排序順序是未知的。例如,在一個最大堆中,最大的那一個元素總是位于 index 0 的位置,但是最小的元素則未必是最后一個元素。–唯一能夠保證的是最小的元素是一個葉節(jié)點,但是不確定是哪一個。

堆和普通樹的區(qū)別

堆并不能取代二叉搜索樹,它們之間有相似之處也有一些不同。我們來看一下兩者的主要差別:

節(jié)點的順序。在二叉搜索樹中,左子節(jié)點必須比父節(jié)點小,右子節(jié)點必須必比父節(jié)點大。但是在堆中并非如此。在最大堆中兩個子節(jié)點都必須比父節(jié)點小,而在最小堆中,它們都必須比父節(jié)點大。

內(nèi)存占用。普通樹占用的內(nèi)存空間比它們存儲的數(shù)據(jù)要多。你必須為節(jié)點對象以及左/右子節(jié)點指針分配內(nèi)存。堆僅僅使用一個數(shù)據(jù)來存儲數(shù)組,且不使用指針。

平衡。二叉搜索樹必須是“平衡”的情況下,其大部分操作的復雜度才能達到O(log n)。你可以按任意順序位置插入/刪除數(shù)據(jù),或者使用 AVL 樹或者紅黑樹,但是在堆中實際上不需要整棵樹都是有序的。我們只需要滿足堆屬性即可,所以在堆中平衡不是問題。因為堆中數(shù)據(jù)的組織方式可以保證O(log n) 的性能。

搜索。在二叉樹中搜索會很快,但是在堆中搜索會很慢。在堆中搜索不是第一優(yōu)先級,因為使用堆的目的是將最大(或者最小)的節(jié)點放在最前面,從而快速的進行相關(guān)插入、刪除操作。

來自數(shù)組的樹

用數(shù)組來實現(xiàn)樹相關(guān)的數(shù)據(jù)結(jié)構(gòu)也許看起來有點古怪,但是它在時間和空間上都是很高效的。

我們準備將上面例子中的樹這樣存儲:

[ 10, 7, 2, 5, 1 ]

就這么多!我們除了一個簡單的數(shù)組以外,不需要任何額外的空間。

如果我們不允許使用指針,那么我們怎么知道哪一個節(jié)點是父節(jié)點,哪一個節(jié)點是它的子節(jié)點呢?問得好!節(jié)點在數(shù)組中的位置index 和它的父節(jié)點以及子節(jié)點的索引之間有一個映射關(guān)系。

如果 i 是節(jié)點的索引,那么下面的公式就給出了它的父節(jié)點和子節(jié)點在數(shù)組中的位置:

parent(i) = floor((i - 1)/2) left(i) = 2i + 1 right(i) = 2i + 2

注意 right(i) 就是簡單的 left(i) + 1。左右節(jié)點總是處于相鄰的位置。

我們將寫公式放到前面的例子中驗證一下。

注意:根節(jié)點(10)沒有父節(jié)點,因為 -1 不是一個有效的數(shù)組索引。同樣,節(jié)點 (2),(5)和(1) 沒有子節(jié)點,因為這些索引已經(jīng)超過了數(shù)組的大小,所以我們在使用這些索引值的時候需要保證是有效的索引值。

復習一下,在最大堆中,父節(jié)點的值總是要大于(或者等于)其子節(jié)點的值。這意味下面的公式對數(shù)組中任意一個索引 i都成立:

array[parent(i)] >= array[i]

可以用上面的例子來驗證一下這個堆屬性。

如你所見,這些公式允許我們不使用指針就可以找到任何一個節(jié)點的父節(jié)點或者子節(jié)點。事情比簡單的去掉指針要復雜,但這就是交易:我們節(jié)約了空間,但是要進行更多計算。幸好這些計算很快并且只需要O(1)的時間。

理解數(shù)組索引和節(jié)點位置之間的關(guān)系非常重要。這里有一個更大的堆,它有15個節(jié)點被分成了4層:


圖片中的數(shù)字不是節(jié)點的值,而是存儲這個節(jié)點的數(shù)組索引!這里是數(shù)組索引和樹的層級之間的關(guān)系:

由上圖可以看到,數(shù)組中父節(jié)點總是在子節(jié)點的前面。

注意這個方案與一些限制。你可以在普通二叉樹中按照下面的方式組織數(shù)據(jù),但是在堆中不可以:


在堆中,在當前層級所有的節(jié)點都已經(jīng)填滿之前不允許開是下一層的填充,所以堆總是有這樣的形狀:

注意:你可以使用普通樹來模擬堆,但是那對空間是極大的浪費。

小測驗,假設我們有這樣一個數(shù)組:

[ 10, 14, 25, 33, 81, 82, 99 ]

這是一個有效的堆嗎?答案是 yes !一個從低到高有序排列的數(shù)組是以有效的最小堆,我們可以將這個堆畫出來:


堆屬性適用于每一個節(jié)點,因為父節(jié)點總是比它的字節(jié)點小。(你也可以驗證一下:一個從高到低有序排列的數(shù)組是一個有效的最大堆)

注意:并不是每一個最小堆都是一個有序數(shù)組!要將堆轉(zhuǎn)換成有序數(shù)組,需要使用堆排序。

更多數(shù)學公式

如果你好奇,這里有更多的公式描述了堆的一些確定屬性。你不需要知道這些,但它們有時會派上用場。 可以直接跳過此部分!

樹的高度是指從樹的根節(jié)點到最低的葉節(jié)點所需要的步數(shù),或者更正式的定義:高度是指節(jié)點之間的邊的最大值。一個高度為 h 的堆有 h+1 層。

下面這個對的高度是3,所以它有4層:


如果一個堆有 n 個節(jié)點,那么它的高度是 h = floor(log2(n))。這是因為我們總是要將這一層完全填滿以后才會填充新的一層。上面的例子有 15 個節(jié)點,所以它的高度是 floor(log2(15)) = floor(3.91) = 3。

如果最下面的一層已經(jīng)填滿,那么那一層包含 2^h 個節(jié)點。樹中這一層以上所有的節(jié)點數(shù)目為 2^h - 1。同樣是上面這個例子,最下面的一層有8個節(jié)點,實際上就是 2^3 = 8。前面的三層一共包含7的節(jié)點,即:2^3 - 1 = 8 - 1 = 7。

所以整個堆中的節(jié)點數(shù)目為:* 2^(h+1) - 1*。上面的例子中,2^4 - 1 = 16 - 1 = 15

葉節(jié)點總是位于數(shù)組的 floor(n/2) 和 n-1 之間。

可以用堆做什么?

有兩個原始操作用于保證插入或刪除節(jié)點以后堆是一個有效的最大堆或者最小堆:

  • shiftUp(): 如果一個節(jié)點比它的父節(jié)點大(最大堆)或者小(最小堆),那么需要將它同父節(jié)點交換位置。這樣是這個節(jié)點在數(shù)組的位置上升。
  • shiftDown(): 如果一個節(jié)點比它的子節(jié)點小(最大堆)或者大(最小堆),那么需要將它向下移動。這個操作也稱作“堆化(heapify)”。

shiftUp 或者 shiftDown 是一個遞歸的過程,所以它的時間復雜度是 O(log n)。

基于這兩個原始操作還有一些其他的操作:

  • insert(value): 在堆的尾部添加一個新的元素,然后使用 shiftUp 來修復對。
  • remove(): 移除并返回最大值(最大堆)或者最小值(最小堆)。為了將這個節(jié)點刪除后的空位填補上,需要將最后一個元素移到根節(jié)點的位置,然后使用 shiftDown 方法來修復堆。
  • removeAtIndex(index): 和 remove() 一樣,差別在于可以移除堆中任意節(jié)點,而不僅僅是根節(jié)點。當它與子節(jié)點比較位置不時無序時使用 shiftDown(),如果與父節(jié)點比較發(fā)現(xiàn)無序則使用 shiftUp()。
  • replace(index, value):將一個更小的值(最小堆)或者更大的值(最大堆)賦值給一個節(jié)點。由于這個操作破壞了堆屬性,所以需要使用 shiftUp() 來修復堆屬性。

上面所有的操作的時間復雜度都是 O(log n),因為 shiftUp 和 shiftDown 都很費時。還有少數(shù)一些操作需要更多的時間:

  • search(value):堆不是為快速搜索而建立的,但是 replace() 和 removeAtIndex() 操作需要找到節(jié)點在數(shù)組中的index,所以你需要先找到這個index。時間復雜度:O(n)。
  • buildHeap(array):通過反復調(diào)用 insert() 方法將一個(無序)數(shù)組轉(zhuǎn)換成一個堆。如果你足夠聰明,你可以在 O(n) 時間內(nèi)完成。
  • 堆排序:由于堆就是一個數(shù)組,我們可以使用它獨特的屬性將數(shù)組從低到高排序。時間復雜度:O(n lg n)。

堆還有一個 peek() 方法,不用刪除節(jié)點就返回最大值(最大堆)或者最小值(最小堆)。時間復雜度 O(1) 。

注意:到目前為止,堆的常用操作還是使用 insert() 插入一個新的元素,和通過 remove()移除最大或者最小值。兩者的時間復雜度都是O(log n)。其其他的操作是用于支持更高級的應用,比如說建立一個優(yōu)先隊列。

插入

我們通過一個插入例子來看看插入操作的細節(jié)。我們將數(shù)字 16 插入到這個堆中:

堆的數(shù)組是: [ 10, 7, 2, 5, 1 ]。

第一股是將新的元素插入到數(shù)組的尾部。數(shù)組變成:

[ 10, 7, 2, 5, 1, 16 ]

相應的樹變成了:

16 被添加最后一行的第一個空位。

不行的是,現(xiàn)在堆屬性不滿足,因為 2 在 16 的上面,我們需要將大的數(shù)字在上面(這是一個最大堆)

為了恢復堆屬性,我們需要交換 16 和 2。

現(xiàn)在還沒有完成,因為 10 也比 16 小。我們繼續(xù)交換我們的插入元素和它的父節(jié)點,直到它的父節(jié)點比它大或者我們到達樹的頂部。這就是所謂的 shift-up,每一次插入操作后都需要進行。它將一個太大或者太小的數(shù)字“浮起”到樹的頂部。

最后我們得到的堆:

現(xiàn)在每一個父節(jié)點都比它的子節(jié)點大。

刪除根節(jié)點

我們將這個樹中的 (10) 刪除:

現(xiàn)在頂部有一個空的節(jié)點,怎么處理?

當插入節(jié)點的時候,我們將新的值返給數(shù)組的尾部。現(xiàn)在我們來做相反的事情:我們?nèi)〕鰯?shù)組中的最后一個元素,將它放到樹的頂部,然后再修復堆屬性。

現(xiàn)在來看怎么 shift-down (1)。為了保持最大堆的堆屬性,我們需要樹的頂部是最大的數(shù)據(jù)。現(xiàn)在有兩個數(shù)字可用于交換 7 和 2。我們選擇這兩者中的較大者稱為最大值放在樹的頂部,所以交換 7 和 1,現(xiàn)在樹變成了:


繼續(xù)堆化直到該節(jié)點沒有任何子節(jié)點或者它比兩個子節(jié)點都要大為止。對于我們的堆,我們只需要再有一次交換就恢復了堆屬性:

刪除任意節(jié)點

絕大多數(shù)時候你需要刪除的是堆的根節(jié)點,因為這就是堆的設計用途。

但是,刪除任意節(jié)點也很有用。這是 remove() 的通用版本,它可能會使用到 shiftDown 和 shiftUp。

我們還是用前面的例子,刪除 (7):

對應的數(shù)組是

[ 10, 7, 2, 5, 1 ]

你知道,移除一個元素會破壞最大堆或者最小堆屬性。我們需要將刪除的元素和最后一個元素交換:

[ 10, 1, 2, 5, 7 ]

最后一個元素就是我們需要返回的元素;然后調(diào)用 removeLast() 來將它刪除。 (1) 比它的子節(jié)點小,所以需要 shiftDown() 來修復。

然而,shift down 不是我們要處理的唯一情況。也有可能我們需要 shift up。考慮一下從下面的堆中刪除 (5) 會發(fā)生什么:

現(xiàn)在 (5) 和 (8) 交換了。因為 (8) 比它的父節(jié)點大,我們需要 shiftUp()。

非商業(yè)轉(zhuǎn)載
作者:唐先僧
鏈接:https://www.jianshu.com/p/6b526aa481b1
來源:簡書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

總結(jié)

以上是生活随笔為你收集整理的数据结构--堆Heap的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。