算法基礎--堆排序

目錄

  • 堆的結構
  • 滿二叉樹
  • 完全二叉樹
    • 數組與完全二叉樹
  • 大根堆&&小根堆
  • 用數組,建立大根堆二叉樹
  • 向下調整
  • 堆排序

堆的結構

堆實際上是一顆完全二叉樹形式的數組


滿二叉樹

除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點二叉樹。

滿二叉樹屬於完全二叉樹

完全二叉樹

在滿二叉樹的基礎上,最後一層所有的結點都連續集中在最左邊,這就是完全二叉樹。


  • 數組與完全二叉樹

如果從下標從1開始存儲,則編號爲i的結點的主要關係爲:
雙親:下取整 (i/2)
左孩子:2i
右孩子:2i+1

如果從下標從0開始存儲,則編號爲i的結點的主要關係爲:
雙親:下取整 ((i-1)/2)
左孩子:2i+1
右孩子:2i+2

這個規律,通常用來對通過指定下標取得相關節點下標。

大根堆&&小根堆

處理最值問題時,堆的調整複雜度遠低於其他結構。

  • 大根堆

任一節點的關鍵碼均大小於等於它的左右孩子的關鍵碼,位於堆頂節點的關鍵碼最大


  • 小根堆

任一節點的關鍵碼均小於等於它的左右孩子的關鍵碼,位於堆頂節點的關鍵碼最小。


如果我們給每個元素都分配一個數字來標記其優先級,不妨設較小的數字具有較高的優先級,這樣我們就可以在一個集合中訪問優先級最高的元素並對其進行查找和刪除操作了。

對於這種最值問題,堆的調整複雜度遠低於其他結構。
而使用大根堆的方式,每次新入隊元素最多只需要堆整個結構進行LogN次的調整,便可以讓堆結構重歸有序。

40億的量級甚至只需要32次調整就可以實現。


用數組,建立大根堆二叉樹

將數組中元素依次放入完全二叉樹中,若大於父節點則依次比對交換。保證時刻處於大根堆排序

第i個數字被插入時排序的時間複雜度與高叉樹高度相等,即O(Logi)。
所有數字都插入依次的時間複雜度收斂於O(N)

//大根堆排序
func maximumHeapSort(arr:inout [Int]) {
    if arr.count < 2 {
        return
    }
    //大根堆排序
    for i in 0..<arr.count {
        heapInsert(arr: &arr, index: i)
    }
}

//分段大根堆排序
func heapInsert(arr:inout [Int] ,index:Int){
    
    //當前節點位置
    var currentIndex = index
    //父節點位置
    var parentIndex = (index - 1)/2
    
    //如果當前節點大於父節點,則進行交換然後繼續檢查
    while arr[currentIndex] > arr[parentIndex] {
        arr.swapAt(currentIndex, parentIndex)
        currentIndex = parentIndex
        parentIndex = (currentIndex - 1)/2
    }
    
}

向下調整

在一個大根堆中,某個位置的數被改變(並且變小)了。重新對堆數組進行調整

比對該位置與其左右子節點,並且與較大的一個進行交換,依次向下進行。

func heapify (arr:inout Array<Int>,index:Int,heapSize:Int){
    var currentIndex = index;
    var left=2*currentIndex+1//左節點位置
    var right=left+1//右節點位置
    var largest = currentIndex //最大位置暫定爲current
    while left<=heapSize {//保證左節點不越界
        
        largest = right<heapSize && arr[right] > arr[left] ?right:left //左右節點的最大值位置(右節點越界則取左)
        
        largest = arr[largest]>arr[currentIndex] ? largest:currentIndex
        
        if largest == currentIndex {
            break //如果當前已經爲最大位置,則結束
        }
        
        arr.swapAt(currentIndex, largest)//交換當前位置與左右兩端最大位置
        
        currentIndex = largest//將當前位置下移
        left=2*currentIndex+1//左節點新位置
        right=left+1//右節點新位置
    }
}

堆排序

先構建出一個大根堆,然後依次將頭部最大值轉移到有效數組的最後一位,並且將排序區域前移。

func heapSort(arr:inout [Int]) {
    maximumHeapSort(arr:&arr)//先構建出一個大根堆
    let size = arr.count

    for i in 0..<arr.count {
        arr.swapAt(size-1-i, 0) //將大根堆頭部最大值,移到有效數組末尾。
        heapify(arr: &arr, index: 0, heapSize: size-i-2)//將數組有效size前移,重新調整成大根堆
    }
}

其中構建初始堆經推導複雜度爲O(n),在交換並重建堆的過程中,需交換n-1次,而重建堆的過程中,根據完全二叉樹的性質,[log2(n-1),log2(n-2)...1]逐步遞減,近似爲nlogn。所以堆排序時間複雜度一般認爲就是O(nlogn)級


最後

本文主要是自己的學習與總結。如果文內存在紕漏、萬望留言斧正。如果願意補充以及不吝賜教小弟會更加感激。


參考資料

用數組存儲完全二叉樹時,結點的索引(數組下標)與其父子結點索引的關係.
【數據結構】堆結構小根堆,大根堆,插入,刪除等操作的實現
堆排序中建堆過程的時間複雜度O(n)的證明
堆——神奇的優先隊列(上) 【經典】
圖解排序算法(三)之堆排序

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章