基于二叉堆的優先隊列和堆排序(golang實現)


二叉堆

堆有序定義:當一顆二叉樹的每個節點都大于等于它的兩個子節點時, 被稱為堆有序。
二叉堆定義: 二叉堆是一組能夠用堆有序的完全二叉樹排序的元素,并在數組中按照層級儲存(不使用數組中第一個位置)。
在一個二叉堆中,位置k的節點的父節點位置為|k/2|(k/2向下取整),兩個子節點的位置分別為:2k、2k+1。
下文中二叉堆簡稱為堆。
堆的有序化定義:堆的操作會首先進行一些改動,打破堆的狀態,然后再遍歷堆并按照要求將堆的狀態恢復。
在有序化的過程中, 會遇到兩種情況:
1、 當某個節點的優先級上升(或者堆底加入新元素),需要由下至上恢復堆的順序。 算法實現如下:

 func swim(k int) {
    for k>1&&less(k/2, k) {
        exch(k/2, k)
        k /= 2
    }
}

2、 當某個節點的優先級下降(例如, 將根節點替換為一個較小的元素)時, 需要由上至下恢復堆的順序。
算法實現如下:

func sink(k int) {
    for 2*k <= N {  //N為二叉堆的元素數量
        j := 2*k
        if 2*k<N && less(j, j+1) {
            j += 1
        }
        if less(j, k) {
            break
        }
        exch(k, j)
        k = j
    }
}

算法使用的exch、less方法:

func less(source []int, i, j int) {
    return source[i] < source[j]
}
func exch(source []int, i, j int) {
    source[i], source[j] = source[j], source[i]
    return
}

優先隊列

優先隊列使用場景很多, 例如:處理很多程序中優先級最高的程序;收集一些數據,處理當前最大/最小的元素,再收集一些數據,再處理當前最大/最小的數據等等。
這些使用場景可以抽象為從N個輸入元素中找到最大/最小的M個元素。可以看一下下面表格中一些方法實現這種需求需要的資源,特別是在N非常大的時候,如何利用有限的資源高效的實現是比較大的挑戰。

示例 時間 空間
排序算法 NlogN N
初級實現(數組/鏈表)的優先隊列 NM M
堆實現的優先隊列 NlogM M

優先隊列也是很多重要算法的基礎, 例如一些重要的圖搜索算法、數據壓縮算法等。
優先隊列兩個最重要的操作是:刪除最大(小)元素插入元素。使用上跟隊列、棧使用比較相似,在實現上, 和棧、隊列最大的不同是: 對于性能上的要求。隊列和棧的實現能夠在常數時間內完成所有的操作,而對于優先隊列,初級實現(有序/無序的數組/鏈表)在刪除元素和插入元素之一操作的最壞情況下需要線性時間完成,而基于二叉堆的實現能夠保證這兩種操作更快(對數級別)。

基于二叉堆實現的優先隊列:
1、 插入操作:
將新元素插入到數組的尾部,增加堆的大小并讓這個元素swim(對應上面二叉堆的優先級上升操作)到相應的位置。
2、 刪除操作:
從數組頂端刪除最大的元素,并將數組中最后一個元素放置到頂端,減小堆的大小, 并讓這個元素sink(對應上面二叉堆的優先級下降操作)到響應的位置
具體實現代碼:

type MaxPQ struct {
    source []int
    s int
}

func NewMaxPQ(k int) *MaxPQ {
    return &MaxPQ{
        source: make([]int, k+1),
        s: 0,
    }
}

func (q *MaxPQ) insert(key int) { //插入操作
    q.s++
    q.source[q.s] = key
    q.swim(q.s)
}

func (q *MaxPQ) delMax() (x int) { //刪除操作
    x = q.source[1]
    q.exch(1, q.s)
    q.s--
    q.sink(1)
    return 
}

func (q *MaxPQ) swim(k int) {
    for k>1&&q.less(k/2, k) {
        q.exch(k/2, k)
        k /= 2
    }
}

func (q *MaxPQ) sink(k int) {
    for 2*k <= q.s {
        j := 2*k
        if j<q.s&&q.less(j, j+1) {
            j += 1
        }
        if !q.less(k, j) {
            break
        }
        q.exch(k, j)
        k = j
    }
}

func (q *MaxPQ) isEmpty() bool {  
    return q.s == 0
}

func (q *MaxPQ) size() int {
    return q.s
}

func (q *MaxPQ) less(i, j int) bool {
    return q.source[i] < q.source[j]
}

func (q *MaxPQ) exch(i, j int) {
    q.source[i], q.source[j] = q.source[j], q.source[i]
}

func (q *MaxPQ) show() {
    var (
        i = 0
    )
    for i<=q.s {
        fmt.Fprintf(os.Stdout, "%v ", q.source[i])
        i++
    }
    fmt.Println()
}

索引優先隊列

索引優先隊列在多向歸并的使用場景中非常有效, 例如:從多個有序的輸入流歸并成一個有序序列,如果有足夠的空間,你可以簡單的把它們讀入一個數組并排序,但如果使用了優先隊列,無論輸入有多長你都可以把他們全部讀入并排序。
索引優先隊列的實現如下:

type IndexMinPQ struct {
    keys []int //元素存放的數組
    pq []int //二叉堆實現的索引
    qp []int //索引二叉堆的逆序, 可以很方便的做contains判斷
    N int //PQ中元素的數量
}
func NewIndexMinPQ(max int) (imq *IndexMinPQ) {
    imq = &IndexMinPQ{
        keys: make([]int, max+1),
        pq: make([]int, max+1),
        qp: make([]int, max+1),
    }
    for i:=0; i<=max; i++ {
        imq.qp[i] = -1  //初始化為-1
    }
    return
}

func (imq *IndexMinPQ) insert(k int, key int) { //插入操作
    imq.N++
    imq.pq[imq.N] = k
    imq.keys[k] = key
    imq.qp[k] = imq.N
    imq.swim(imq.N)
}

func (imq *IndexMinPQ) delMin() (indexOfMin int) {  //刪除操作
    indexOfMin = imq.keys[imq.pq[1]]
    imq.exch(1, imq.N)
    imq.N--
    imq.sink(1)
    imq.keys[imq.pq[imq.N+1]] = -1
    imq.qp[imq.pq[imq.N+1]] = -1
    return
}

func (imq *IndexMinPQ) swim(k int) { 
    for k>1 && (!imq.less(k/2, k)) {
        imq.exch(k/2, k)
        k /= 2
    }
}

func (imq *IndexMinPQ) sink(k int) {  
    for 2*k <= imq.N {
        j := 2*k
        if 2*k<imq.N && imq.less(j+1, j) {
            j += 1
        }
        if imq.less(k, j) {
            break
        }
        imq.exch(k, j)
        k = j
    }
}

func (imq *IndexMinPQ) less(i, j int) bool {
    return imq.keys[imq.pq[i]] < imq.keys[imq.pq[j]]
}

func (imq *IndexMinPQ) exch(i, j int) {
    imq.pq[i], imq.pq[j] = imq.pq[j], imq.pq[i]
    imq.qp[imq.pq[i]], imq.qp[imq.pq[j]] = imq.qp[imq.pq[j]], imq.qp[imq.pq[i]]
}

func (imq *IndexMinPQ) show() {
    for i:=1; i<=imq.N; i++ {
        fmt.Printf("%v ", imq.keys[imq.pq[i]])
    }
    fmt.Println()
}

func (imq *IndexMinPQ) min() int {
    return imq.keys[imq.pq[1]]
}

func (imq *IndexMinPQ) contains(k int) bool {
    return imq.qp[k] != -1
}

堆排序

有了上面優先隊列的例子, 再看堆排序就很簡單了。
堆排序算法分為兩階段:在堆的構造階段,將原始數組重新組織到一個堆中,然后在下沉階段,從堆中按遞減順序取出所有元素并得到排序結果。
在堆的構造階段,可以從左到右遍歷數組,用swim()保證掃描到的位置左側都是一顆堆有序的完全樹,但更高效的方法是:從右至左用sink()函數構造子堆,開始時我們只需要掃描一半的元素,因為我們可以跳過大小為1的子堆。

func heapSort(source []int) {
    var (
        l = len(source)-1
        j = l
    )
    for i:=l/2; i>=1; i-- {  //構造堆階段,只需要掃描一半的元素
        sink(source, i, l)
    }
    for j>1 {   //下沉階段
        exch(source, 1, j)
        j--
        sink(source, 1, j)
    }
}

func sink(source []int, i, length int) {
    for 2*i<=length {
        x := 2*i
        if x<length&&less(source, x, x+1) {
            x += 1
        }
        if !less(source, i, x) {
            break
        }
        exch(source, i, x)
        i *= 2
    }
}

less函數和exch函數參照上文。

多叉堆

參考資料: 《算法》第四版

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容