堆排序的實現原理分析(剖析上浮下沉操作, 讓你重新認識堆排序)

    網絡上關於堆排序的算法博客多是給圖給真相(因爲翻了很多文章還是不清楚,堆排序的過程是什麼?所以自己來搞),今天我介紹一下我對堆排序的理解及實戰分析。

    先介紹一下堆:它分最大堆和最小堆,二者的不同就是父節點和子節點的關係;最大堆要求父節點要比子節點大,而最小堆要求比父節點要比子節點小。其次是堆所依賴的數據結構,它本身是一顆完全二叉樹,由於完全二叉樹特性,父節點和左右子節點間位置的關係可通過公式得出(如果根節點位置從0開始,那麼左右節點和父節點位置的關係爲:nl = n*2+1, nr = n*2+2;同樣可以通過逆運算,由左右子節點位置信息獲取父節點的位置信息:n = (nl-1)/2, n = (nr-1)/2,`這裏依賴int類型做除法的時候結果會將小數點後面的數刪除,只保留整數部分`),所以數組便可成爲堆數據結構的不二之選(從父節點訪問堆子節點的時間複雜度爲0(1),而堆所佔的空間複雜度爲O(n),n爲堆的大小)。

 

    然後介紹算法的核心過程:堆排序之前有一個堆的初始化過程,這個過程的目的是爲了得到一個"所有的父節點都大於其左右子節點"這樣的堆,堆初始化的過程是 "從第一個非葉子節點開始,  第一個父節點下沉,然後順序進行第二父節點的下沉,直到根節點下沉完畢" 這樣的過程。當然還有其它辦法進行堆的初始化,比如利用二叉樹的特性(遍歷左右子樹等),不過 "無論怎麼交換位置,二叉樹各個路徑上的節點保持一致,順序可以不一致"(原因,只有父子節點間位置的替換,並無兄弟節點間位置的替換),這是題外話。(細節會在算法中體現,比如左右子節點相等的情況怎麼處理)

   這裏給出一個假設“真相”(算法是否正確也和這個假設是否成立相關):

   【

        在堆完成初始化後, 所有的父節點都將大於子節點(這一步爲後續操作的基礎); 

        堆的刪除:將根節點和堆的尾節點交換位置並且堆的大小-1; 此時堆的平衡(父節點大於其子節點)可能會被打破, 再讓新的根節點下沉,直至滿足該節點大於其子節點或者該節點成爲葉子節點時, 堆將重新維持到平衡狀態。

        堆的新增:堆的大小+1,新增節點成爲堆末尾的葉子節點;此時堆的平衡(父節點大於其子節點)可能會被打破, 再讓末尾節點上浮,直至滿足其父節點大於該節點或該節點成爲根節點時,堆將重新維持到平衡狀態。

    】

    最後是排序:其實就是一個堆刪除的過程(把整個堆刪除完,排序也就完成了)。

    實現如下(Go):

//堆排序實現
/**
    算法核心:
        1. 堆重構
        [
            (以最大堆舉例)
            (增加)上浮: 與父節點比較, 如果大於父節點, 與父結點交換位置後(上浮節點繼續垂直上浮直到成爲根節點或者小於父節點);
            (刪除)下沉: 與子節點比較, 如果小於子節點, 選擇較大子節點與之交換位置, 然後繼續下沉(直至其成爲葉子節點);
        ]
        2. 排序: 將堆重構後的根節點與末尾節點置換, 根節點開始下沉, 直到末尾節點成爲根節點
    原理:
        在"堆的初始化"完成後, 所有的父節點都大於子節點(這一步是後續操作的基礎), 當根節點和堆的尾節點交換位置後(刪除操作),
    堆的平衡(父節點大於子節點)會被打破了,可以通過新的根節點的持續下沉操作, 將堆重新維持到平衡狀態。
 */
package main

import "fmt"

//垂直上浮
func heapVerUp(heap []int, i int) {
    if i < 0 {
        return
    }
    for ; i >= 1; {
        pi := (i-1)/2
        if heap[i] > heap[pi] {
            heap[i], heap[pi] = heap[pi], heap[i]
            i = pi
            continue
        }
        break
    }
}

//堆的初始化
func heapInit(heap []int) {
    //讓非葉子節點進行下沉
    for i := (len(heap)-1)/2+1; i >= 0; i-- {
        heapDown(heap, i, len(heap)-1)
    }
}

//節點下沉
func heapDown(heap []int, i, hl int) {
    if hl == 0 {
        return
    }
    for {
        nl := i*2+1
        nr := i*2+2
        if nl <= hl && nr <= hl{
            //與左節點比較, 如果左右節點相等的情況, 左子節點優先進行
            if heap[nl]>=heap[nr] && heap[i]<heap[nl] {
                heap[nl], heap[i] = heap[i], heap[nl]
                i = nl
                continue
            //與右節點比較
            } else if heap[i]<heap[nr] {
                heap[nr], heap[i] = heap[i], heap[nr]
                i = nr
                continue
            }
        } else if nl<=hl && heap[i]<heap[nl] {
            //同末尾節點比較
            heap[nl], heap[i] = heap[i], heap[nl]
        }
        break
    }
}

func heapSort(heap []int) {
    //設置根節點位置從0開始, 則左右節點與父結點位置的關係爲 n左 = n*2+1 n右 = n*2+2
    li := len(heap)-1
    if li == 0 {
        return
    }
    //平移上浮
    heapInit(heap)
    fmt.Println(heap)
    for ; li >= 1; li-- {
        heap[li], heap[0] = heap[0], heap[li]
        heapDown(heap, 0, li-1)
    }
}

func main() {
    //sli := []int {1, 2, 3, 4, 5, 6, 7, 8, 9} //OK
    //sli := []int {9, 8, 7, 6, 5, 4, 3, 2, 1} //OK
    //sli := []int {4, 3, 5, 6, 2, 8, 7, 9, 1} //OK
    //sli := []int {2, 2, 1, 3, 4, 1, 2, 1, 2, 2} //OK
    //sli := []int {1, 1, 1, 1, 1, 1, 2, 1, 1, 1} //OK
    sli := []int {1, 44, 22, 11}
    heapSort(sli)
    fmt.Println(sli)
}

  而堆新增節點的操作,可應用到優先級任務隊列。

 

發佈了34 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章