算法基礎--遞歸入門の歸併排序

本文只是自己的筆記,並不具備過多的指導意義。

爲了理解很多都使用了遞歸,而不是自己通過while進行壓棧處理。
代碼的初衷是便於理解,網上大神優化過的代碼很多,也不建議在項目中copy本文代碼。

目錄

  • 歸併排序
    • 如何合併兩個有序數組
    • 使兩端分別有序
    • 最外側還有一個入口方法
    • 圖解排序過程
  • 遞歸時間複雜度的估算
    • master公式

歸併排序

衆所周知,分治策略中使用遞歸來求解問題分爲三步走,分別爲分解、解決和合並。

所以歸併排序中心思想是通過二分的方式,將一個段數組拆分成左右兩端,然後進行合併

  • 如何合併兩個有序數組

通過將最小值依次外排的方式,進行合併

  1. 兩個指針p1p2指向兩個數組的首位置,每次將較小的值外排進輔助數組並且右移指針
  2. 當一個指針越界後,將另一個剩餘的元素放入輔助數組
  3. 最後的輔助數組將是一個有序數組。
/// 對一個數組的兩個有序分段進行整合排序
///
/// - Parameters:
///   - arr: 數組
///   - left: 左側起點
///   - mid: 中心點,左側末尾
///   - right: 右側末尾
func merge(arr: inout [Int] ,left: Int ,mid: Int ,right:Int) {
    
    var help : [Int] = [Int](repeating: 0, count: right-left+1) //輔助數組
    var p1 = left//左側指針
    var p2 = mid+1 //右側指針
    var i = 0 //容器指針位置
    
    while p1<=mid && p2<=right {
        if arr[p1] <= arr[p2] {//左側指針位置小於等於右側指針位置
            help[i] = arr[p1] //將左側指針位置放入輔助數組
            p1 = p1+1 //左側指針右移
        }else {//右側指針大於左側指針位置
            help[i] = arr[p2] //將右側指針位置放入輔助數組
            p2 = p2+1 //左側指針右移
        }
        i = i+1  //容器指針右移
    }
    
    //到這裏,p1,p2之中已經有一個指針越界了
    //沒有越界的那個,重複上面的操作寫入輔助數組
    while p1<=mid {
        help[i] = arr[p1]
        p1 = p1+1
        i = i+1
    }
    while p2<=right {
        help[i] = arr[p2]
        p2 = p2+1
        i = i+1
    }
    //寫入完畢,用輔助數組替換進原數組
    i = 0
    for index in left...right { //left,left+1...right-1,right
        arr[index] = help[i]
        i = i+1
    }
}

之所以先說這個,是因爲這個方法是核心方法,並且寫完滯後直接將可以測試。
測試通過之後,再去測試下面的遞歸方法可以更加準確的確定問題所在。

  • 使兩端分別有序

通過遞歸的方式,將長數組最終分解成有序的數組進行排序處理。

比如[4,2,6,1,9,7]

/// 在一個數組的某一段上進行排序
///
/// - Parameters:
///   - arr: 數組
///   - left: 左側
///   - right: 右側
func mergeProcess(arr: inout [Int] ,left: Int ,right:Int) {
    
    if left==right { //左右相等,說明這次要調整的只有一個數
        return
    }
    
    let mid = (left+right)/2 //取得中心點位置
    mergeProcess(arr: &arr, left: left, right: mid)//對左半邊進行排序
    mergeProcess(arr: &arr, left: mid+1, right: right)//對右半邊進行排序
    //此時左右兩側都已經排序完成
    merge(arr: &arr, left: left, mid: mid, right: right)//對左右兩側進行合併排序
}
  • 最外側還有一個入口方法
/// 歸併排序
///
/// - Parameter arr: 數組
func mergeSort(arr: inout [Int]) {
    if arr.count<2 {
        return
    }
    mergeProcess(arr: &arr, left: 0, right: arr.count-1)
}

時間複雜度O(N * logN),額外空間複雜度O(N)

  • 圖解排序過程

網上還看到一個動圖,結合上面的應該更容易理解了。

這個過程中,不管數組多長,最後都將被劃分成2個單位長度的數組進行排序。
然後向上依次合併並且排序。

這也很好的體現出了分治或者遞歸的思想所在,將大樣本劃分成小樣本並依次解決。


遞歸時間複雜度的估算

只說一種普遍的通用情況

  • master公式
T(N) = A*T(N/B) + O(N^D)

量本量爲N的情況下,整個樣本被劃分成B樣本量,共執行A次
出去調用遞歸過程之外,剩下的部分的時間複雜度O(N^D)

1) log(b,a) > d -> 複雜度爲O(N^log(b,a)) 
2) log(b,a) = d -> 複雜度爲O(N^d * logN) 
3) log(b,a) < d -> 複雜度爲O(N^d)

以歸併排序爲例:

func mergeProcess(arr: inout [Int] ,left: Int ,right:Int) {
    
    if left==right { //左右相等,說明這次要調整的只有一個數
        return
    }
    
    let mid = (left+right)/2 //取得中心點位置
    mergeProcess(arr: &arr, left: left, right: mid)//T(N/2)
    mergeProcess(arr: &arr, left: mid+1, right: right)//T(N/2)
  
    merge(arr: &arr, left: left, mid: mid, right: right)//O(N)
}

T(N) = T(N/2) + T(N/2) + O(N) = 2T(N/2) + O(N)
A = 2,B=2,D=1

帶入master公式log(b,a) = 1,與d值相等。

於是歸併排序的時間複雜度爲O(N * logN)

參考資料

【圖解數據結構】 一組動畫演示歸併排序
左神牛課網算法課
十大經典排序算法(動圖演示)

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