數(shù)據(jù)結(jié)構(gòu)與算法分析 chapter06 -優(yōu)先隊(duì)列(堆)

?優(yōu)先隊(duì)列是允許至少兩種操作的數(shù)據(jù)結(jié)構(gòu):insert(插入),以及deleteMin(刪除最小者),它的工作是找出,返回并刪除優(yōu)先隊(duì)列中最小的元素。insert操作等價(jià)于enqueue(入隊(duì)),而deleteMin則是隊(duì)列運(yùn)算dequeue(出隊(duì))在優(yōu)先隊(duì)列中的等價(jià)操作。
?實(shí)現(xiàn)優(yōu)先隊(duì)列的一種方法是使用二叉查找樹,操作的平均運(yùn)行時(shí)間三O(logN)

二叉堆

?堆是一顆完全被填滿的二叉樹,有可能的例外是在底層,底層上的元素從左到右填入。這樣的樹稱為完全二叉樹。


完全二叉樹.png

?因?yàn)橥耆鏄溥@么有規(guī)律,它可以用一個(gè)數(shù)組表示而不使用鏈。


完全二叉樹的數(shù)組實(shí)現(xiàn).png

?對于數(shù)組中任一位置 i 上的元素,其左兒子在位置 2i 上,右兒子在左兒子后的單元 (2i+1) 中,它的父親則在位置 i/2。

?因此,一個(gè)堆結(jié)構(gòu)將有一個(gè)數(shù)組和一個(gè)代表當(dāng)前堆的大小的整數(shù)組成。

堆序性質(zhì)

?讓操作快速執(zhí)行的性質(zhì)是堆序性質(zhì)。由于我們想要快速找出最小元,因此最小元應(yīng)該在根上。如果我們考慮任意子樹也應(yīng)該是一個(gè)堆,那么任意節(jié)點(diǎn)就應(yīng)該小于它的所有后裔。
堆序性質(zhì): 在一個(gè)堆中,對于每一個(gè)節(jié)點(diǎn)X,X的父親中的關(guān)鍵字小于(或等于)X中的關(guān)鍵字,根節(jié)點(diǎn)除外(它沒有父親)。

image.png

insert.png
deleteMin.png
package heap

//AnyType 實(shí)現(xiàn)compareTo的int
type AnyType int

func (a AnyType) compareTo(b AnyType) int {
   result := 0
   if a < b {
       result = -1
   } else if a == b {
       result = 0
   } else if a > b {
       result = 1
   }
   return result
}

// BinaryHeap 二叉堆
type BinaryHeap struct {
   CurrentSize int // 二叉堆的大小
   Aarray      []AnyType
}

// Insert 插入
func (h *BinaryHeap) Insert(a AnyType) {
   child := h.CurrentSize + 1
   if child > len(h.Aarray)-1 {
       array := make([]AnyType, child*2)
       for i := 0; i < len(h.Aarray); i++ {
           array[i] = h.Aarray[i]
       }
       h.Aarray = array
   }
   father := child / 2
   for {
       if father == 0 {
           break
       }
       if a.compareTo(h.Aarray[father]) < 0 {
           h.Aarray[child] = h.Aarray[father]
           child = child / 2
           father = father / 2
       } else {
           break
       }
   }
   h.Aarray[child] = a
   h.CurrentSize = h.CurrentSize + 1
}

// DeleteMin 刪除最小元素
func (h *BinaryHeap) DeleteMin() AnyType {
   if len(h.Aarray) == 0 {
       return AnyType(-1)
   }
   any := h.Aarray[1]
   last := h.Aarray[h.CurrentSize]
   father := 1
   childleft := father * 2
   childRight := father*2 + 1

   for {
       if len(h.Aarray)-1 < childleft {
           break
       }
       if h.Aarray[childleft] == 0 && h.Aarray[childRight] == 0 {
           break
       }
       if len(h.Aarray)-1 >= childRight {
           if h.Aarray[childleft] >= h.Aarray[childRight] {
               if last <= h.Aarray[childRight] {
                   h.Aarray[father] = last
                   break
               } else {
                   h.Aarray[father] = h.Aarray[childRight]
                   father = childRight
               }
           } else {
               if last <= h.Aarray[childRight] {
                   h.Aarray[father] = last
                   break
               } else {
                   h.Aarray[father] = h.Aarray[childleft]
                   father = childleft
               }
           }
           childleft = father * 2
           childRight = father*2 + 1
       }
   }
   h.Aarray[father] = last
   h.Aarray[h.CurrentSize] = 0
   h.CurrentSize = h.CurrentSize - 1
   return any
}

package heap

import (
   "testing"
)

func Test_BinaryHeap(t *testing.T) {
   binaryHeap := &BinaryHeap{
       CurrentSize: 0,
       Aarray:      []AnyType{-1},
   }

   binaryHeap.Insert(AnyType(13))
   binaryHeap.Insert(AnyType(14))
   binaryHeap.Insert(AnyType(16))
   binaryHeap.Insert(AnyType(19))
   binaryHeap.Insert(AnyType(21))
   binaryHeap.Insert(AnyType(19))
   binaryHeap.Insert(AnyType(68))
   binaryHeap.Insert(AnyType(65))
   binaryHeap.Insert(AnyType(26))
   binaryHeap.Insert(AnyType(32))
   binaryHeap.Insert(AnyType(31))

   binaryHeap.DeleteMin()
}

func Test_BinaryHeap2(t *testing.T) {
   binaryHeap := &BinaryHeap{
       CurrentSize: 0,
       Aarray:      []AnyType{-1},
   }

   binaryHeap.Insert(AnyType(13))
   binaryHeap.Insert(AnyType(14))
   binaryHeap.Insert(AnyType(16))
   binaryHeap.Insert(AnyType(35))
   binaryHeap.Insert(AnyType(21))
   binaryHeap.Insert(AnyType(19))
   binaryHeap.Insert(AnyType(68))
   binaryHeap.Insert(AnyType(65))
   binaryHeap.Insert(AnyType(36))
   binaryHeap.Insert(AnyType(32))
   binaryHeap.Insert(AnyType(31))

   binaryHeap.DeleteMin()
}

其他函數(shù)

decreaseKey(降低關(guān)鍵字的值)
?decreaseKey(p,△)操作降低在位置p處的項(xiàng)的值,降值的幅度為正的能量△。由于這可能破壞堆序性質(zhì),因此必須通過上濾對堆進(jìn)行調(diào)整。

increaseKey(增加關(guān)鍵字的值)
?increaseKey(p,△)操作增加在位置p處的項(xiàng)的值,增加的幅度為正的能量△。由于這可能破壞堆序性質(zhì),因此必須通過下濾對堆進(jìn)行調(diào)整。

delete(刪除)
?delete(p)操作刪除堆中位置p上的節(jié)點(diǎn)。該操作通過首先執(zhí)行decreaseKep(p, ∞)然后再執(zhí)行deleteMin()來完成。

buildHeap(構(gòu)建堆)
?有時(shí)二叉堆是由一些項(xiàng)的初始集合構(gòu)造而得。這種構(gòu)造方法以N項(xiàng)作為輸入,并把它們放入一個(gè)堆中。顯然,這可以使用N個(gè)相繼的insert操作來完成。由于每個(gè)insert將花費(fèi)O(1)平均時(shí)間以及O(logN)的最壞情形時(shí)間,因此該算法的總運(yùn)行時(shí)間是O(N)平均時(shí)間而不是最O(NlogN)最壞情形時(shí)間。


func (h *BinaryHeap) buildHeap(array []AnyType) {
   h.Aarray = make([]AnyType, len(array)+1)
   for i := 0; i < len(array); i++ {
       size := i + 1
       h.Aarray[size] = array[i]
   }
   h.CurrentSize = len(array)
   for i := h.CurrentSize / 2; i > 0; i-- {
       h.percolateDown(i)
   }
}

func (h *BinaryHeap) percolateDown(i int) {
   if i >= h.CurrentSize {
       return
   }
   for {
       if i*2 <= h.CurrentSize {
           if h.Aarray[i*2] <= h.Aarray[i*2+1] && h.Aarray[i] > h.Aarray[i*2] {
               tmp := h.Aarray[i*2]
               h.Aarray[i*2] = h.Aarray[i]
               h.Aarray[i] = tmp
           }
           if h.Aarray[i*2] > h.Aarray[i*2+1] && h.Aarray[i] > h.Aarray[i*2+1] {
               tmp := h.Aarray[i*2+1]
               h.Aarray[i*2+1] = h.Aarray[i]
               h.Aarray[i] = tmp
           }
           i = i / 2
       }
       break
   }
}

二叉堆的應(yīng)用場景

選擇問題
? 輸入N個(gè)元素以及一個(gè)整數(shù)k,找出第k個(gè)最大的元素。
解決:將N個(gè)元素讀入一個(gè)數(shù)組。然后對該數(shù)組應(yīng)該buildHeap算法。最后,執(zhí)行k次delteMin操作,從該堆最前提取的元素就是我們的元素。---->堆排序(heapsort)TODO

d-堆

?d-堆是二叉堆的簡單推廣,它就像一個(gè)二叉堆,只是所有的節(jié)點(diǎn)都有d個(gè)兒子(因此,二叉堆是2-堆)。


3-堆.png

d-堆要比二叉堆淺得多,它將insert操作的運(yùn)行時(shí)間改為O(log?N)。然而,對于大的d,deleteMin操作費(fèi)時(shí)得多,雖然樹是淺了,但是d個(gè)兒子中的最小者是必須要找出的,如使用標(biāo)準(zhǔn)的算法,這會花費(fèi)d-1次比較,于是將操作的用時(shí)提高到O(d log?N)。


image.png

二項(xiàng)隊(duì)列

? 二項(xiàng)隊(duì)列支持所有這三種操作(merge + insert + deleteMin), 每次操作的最壞情形運(yùn)行時(shí)間為O(logN), 而插入操作平均花費(fèi)常數(shù)時(shí)間;

?二項(xiàng)隊(duì)列與優(yōu)先隊(duì)列的區(qū)別:一個(gè)二項(xiàng)隊(duì)列不是一顆堆序的樹,而是堆序的樹的集合,稱為森林。每一科堆序樹都是用約束的形式,叫做二項(xiàng)樹

二項(xiàng)樹.png

?從圖中看到,二項(xiàng)樹B?由一個(gè)帶有兒子B?,B? ....B?-?的根組成。高度為k的二項(xiàng)樹恰好有2? 個(gè)節(jié)點(diǎn)。

image.png
image.png

二項(xiàng)隊(duì)列與二叉堆的比較

基本操作: insert(平均情況下) deleteMin merge
二項(xiàng)隊(duì)列: O(1) O(logN) O(logN)
二叉堆: O(1) O(logN) O(N)

可見,二項(xiàng)隊(duì)列有效地支持了merge操作。

但是,需要注意的是:二項(xiàng)隊(duì)列的實(shí)現(xiàn)用到了鏈表,樹中的每個(gè)元素存儲在一個(gè)結(jié)點(diǎn)中,結(jié)點(diǎn)之間采用“左孩子右兄弟”表示法進(jìn)行鏈接,此外還需要一個(gè)額外的數(shù)組來保存各棵二項(xiàng)樹的根結(jié)點(diǎn),存儲開銷要比二叉堆大。

而對于二叉堆的存儲,則簡單得多。它只需要一個(gè)一維數(shù)組即可存儲各個(gè)元素。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容