經典排序算法總結與Go實現

學習Go語言第二周,本周任務嘗試實現七大經典排序算法以及分析算法復雜度、優劣及應用場景等,七大經典算法分別為冒泡排序,插入排序,選擇排序,希爾排序,歸并排序,快速排序,堆排序。

冒泡排序

  • 思路

正如“冒泡”二字,我的理解是重復依次比較相鄰的兩個數,大的數放在后面,小的數放在前面,一直重復到沒有任何一對數字需要交換位置為止。就像冒泡一樣,大的數不斷浮上來。

  • 偽代碼
do
  swapped = false
  for i = 1 to indexOfLastUnsortedElement-1
    if leftElement > rightElement
      swap(leftElement, rightElement)
      swapped = true; swapCounter++
while swapped
  • Go實現

func Bubble_Sort(arr []int) {
    swapped := true
    len := len(arr)
    for swapped {
        swapped = false
        for i := 0; i < len-1; i++ {
            if arr[i] > arr[i+1] {
                arr[i], arr[i+1] = arr[i+1], arr[i] 
                swapped = true
            }
        }
    }
}

選擇排序

  • 思路

先假設第一個元素為最小值,然后與剩余的 len-1 個元素依次進行比較,標記最小數的位置,如果有更小的數,則在進行下一輪遍歷比較之前交換位置。

  • 偽代碼
repeat (numOfElements - 1) times
  set the first unsorted element as the minimum
  for each of the unsorted elements
    if element < currentMinimum
      set element as new minimum
  swap minimum with first unsorted position
  • Go實現
func Selection_Sort(arr []int) {
    len := len(arr)
    for i := 0; i < len-1; i++ {
        min := i
        for j := i+1; j < len; j++ {
            if arr[j] < arr[min] {
                min = j
            }
        }
        arr[min], arr[i] = arr[i], arr[min]
    }
}

插入排序

  • 思路
    這個排序感覺和選擇排序的思路有點相似的。首先1個長度的數組肯定是有序的,假設數組的長度為n,第一位是有序的,然后從第二位開始在已排序序列中從后向前掃描,找到相應位置并插入。

  • 偽代碼

mark first element as sorted
for each unsorted element X
  'extract' the element X
  for j = lastSortedIndex down to 0
    if current element j > X
      move sorted element to the right by 1
    break loop and insert X here
  • Go實現
func Insertion_Sort(arr []int) {
    len := len(arr)
    for i := 0; i < len; i++ {
        selected := arr[i]
        for j := i-1; j >= 0; j-- {
            if arr[j] > selected {
                arr[j], arr[j+1] = arr[j+1], arr[j]
            } else {
                arr[j+1] = selected
                break
            }
        }
    }
}

歸并排序

  • 思路

歸并排序是采用分治法的一個非常典型的應用。歸并排序的思想就是先遞歸分解數組,再合并數組。先考慮合并兩個有序數組,基本思路是比較兩個數組的最前面的數,誰小就先取誰,取了后相應的指針就往后移一位。然后再比較,直至一個數組為空,最后把另一個數組的剩余部分復制過來即可。再考慮遞歸分解,基本思路是將數組分解成left和right,如果這兩個數組內部數據是有序的,那么就可以用上面合并數組的方法將這兩個數組合并排序。如何讓這兩個數組內部是有序的?可以再二分,直至分解出的小組只含有一個元素時為止,此時認為該小組內部已有序。然后合并排序相鄰二個小組即可。(摘抄)

  • 偽代碼
split each element into partitions of size 1
recursively merge adjancent partitions
  for i = leftPartStartIndex to rightPartLastIndex inclusive
    if leftPartHeadValue <= rightPartHeadValue
      copy leftPartHeadValue
    else: copy rightPartHeadValue; Increase InvIdx
copy elements back to original array
  • Go實現
func Merge_Sort(arr []int) []int{
    if len(arr) <= 1 {
        return arr
    }
    var middle int = len(arr)/2
    left := Merge_Sort(arr[:middle])
    right := Merge_Sort(arr[middle:])
    return merge(left, right)
}

func merge(a, b []int) []int {
    alen, blen := len(a), len(b)
    var z []int = make([]int, alen + blen)
    k := 0//數組切片z的下標 
    i, j := 0, 0//a、b起始下標均未0
    for i < alen && j < blen  {
        if a[i] < b[j] {
            z[k] = a[i] 
            i++ 
        } else { 
            z[k] = b[j] 
            j++ 
        } 
        k++ 
    }
    for i != alen {
        z[k] = a[i] 
        k++ 
        i++ 
    } 
    for j != blen { 
        z[k] = b[j] 
        k++ 
        j++ 
    } 
    return z 
}

快速排序

  • 思路

快速排序可能是當前應用最廣泛的排序算法。快速排序(英語:Quicksort),又稱劃分交換排序(partition-exchange sort),一種排序算法,最早由東尼·霍爾提出。在平均狀況下,排序n個項目要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況并不常見。事實上,快速排序通常明顯比其他Ο(n log n)算法更快,因為它的內部循環(inner loop)可以在大部分的架構上很有效率地被實現出來。快速排序引人注目的特點包括它是原地排序(只需要一個很小的輔助棧)。
該方法的基本思想是:1.先從數列中取出一個數作為基準數。2.分區過程,將比這個數大的數全放到它的右邊,小于或等于它的數全放到它的左邊。3.再對左右區間重復第二步,直到各區間只有一個數。

  • 偽代碼
for each (unsorted) partition
set first element as pivot
  storeIndex = pivotIndex + 1
  for i = pivotIndex + 1 to rightmostIndex
    if element[i] < element[pivot]
      swap(i, storeIndex); storeIndex++
  swap(pivot, storeIndex - 1)
  • Go實現

func Quick_Sort(arr []int) {
    sort(arr, 0, len(arr)-1)
}
func sort(arr []int, left int, right int) {
    if right <= left {
        return
    }
    p := partition(arr, left, right)//快速排序切分
    sort(arr, left, p-1)
    sort(arr, p+1, right)
}
func partition(arr []int, left int, right int) int {
    pivot := arr[left]
    i, j := left, right+1
    for true {
        for i++; arr[i] < pivot; i++ {
            if i==right {
                break
            }
        }
        for j--; pivot < arr[j]; j-- {
            if j==left {
                break
            }
        }
        if i>=j {
            break
        }
        arr[i], arr[j] = arr[j], arr[i]
    }
    arr[left], arr[j] = arr[j], arr[left] 
    return j
}

希爾排序

  • 思路

希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。該方法因DL.Shell于1959年提出而得名。希爾排序是基于插入排序的以下兩點性質而提出改進方法的:1. 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率2. 但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位

  • Go實現
func Shell_Sort(arr []int) {
    N := len(arr)
    var gap int = N/2    //初始步長
    for gap > 0 {
        for i := gap; i < N; i++ {    //每一列進行插入排序 , 從gap 到 n-1
            temp := arr[i]
            j := i
            for j>=gap && arr[j-gap]>temp {    //插入排序
                arr[j] = arr[j-gap]
                j = j-gap
            }
            arr[j] = temp
        }
        gap = gap/2    //重新設置步長
    }
}

堆排序

堆排序是一種選擇排序,其時間復雜度為O(nlogn)

  • 堆的定義

n個元素的序列{k1,k2,…,kn}當且僅當滿足下列關系之一時,稱之為堆。  情形1:ki <= k2i 且ki <= k2i+1 (最小化堆或小頂堆)  情形2:ki >= k2i 且ki >= k2i+1 (最大化堆或大頂堆)  其中i=1,2,…,n/2向下取整;
若將和此序列對應的一維數組(即以一維數組作此序列的存儲結構)看成是一個完全二叉樹,則堆的含義表明,完全二叉樹中所有非終端結點的值均不大于(或不小于)其左、右孩子結點的值。
例如,下列兩個序列為堆,對應的完全二叉樹如圖:

完全二叉樹.png

若在輸出堆頂的最小值之后,使得剩余n-1個元素的序列重又建成一個堆,則得到n個元素的次小值。如此反復執行,便能得到一個有序序列,這個過程稱之為堆排序。堆排序(Heap Sort)只需要一個記錄元素大小的輔助空間(供交換用),每個待排序的記錄僅占有一個存儲空間。

  • 堆的存儲

一般用數組來表示堆,若根結點存在序號0處, i結點的父結點下標就為(i-1)/2。i結點的左右子結點下標分別為2i+1和2i+2。(注:如果根結點是從1開始,則左右孩子結點分別是2i和2i+1。)如第0個結點左右子結點下標分別為1和2。如最大化堆如下:左圖為其存儲結構,右圖為其邏輯結構。

最大化堆.png
  • 堆的排序實現
  1. 構造最大堆(Build_Max_Heap):若數組下標范圍為0~n,考慮到單獨一個元素是大根堆,則從下標n/2開始的元素均為大根堆。于是只要從n/2-1開始,向前依次構造大根堆,這樣就能保證,構造到某個節點時,它的左右子樹都已經是大根堆。2. 堆排序(HeapSort):由于堆是用數組模擬的。得到一個大根堆后,數組內部并不是有序的。因此需要將堆化數組有序化。思想是移除根節點,并做最大堆調整的遞歸運算。第一次將heap[0]與heap[n-1]交換,再對heap[0...n-2]做最大堆調整。第二次將heap[0]與heap[n-2]交換,再對heap[0...n-3]做最大堆調整。重復該操作直至heap[0]和heap[1]交換。由于每次都是將最大的數并入到后面的有序區間,故操作完后整個數組就是有序的了。3. 最大堆調整(Max_Heapify):該方法是提供給上述兩個過程調用的。目的是將堆的末端子節點作調整,使得子節點永遠小于父節點 。
  • Go實現
func Heap_Sort(arr []int) {
    N := len(arr)
    var first int = N/2    //最后一個非葉子節點
    for start := first; start > -1; start-- {    //構造大根堆
        max_heapify(arr, start, N-1)
    }
    for end := N-1; end > 0; end-- {    //堆排,將大根堆轉換成有序數組
        arr[end],arr[0] = arr[0],arr[end]
        max_heapify(arr, 0, end-1)
    }
}
func max_heapify(arr []int, start int, end int) {
    root := start
    for true {
        child := root*2 + 1    //調整節點的子節點
        if child > end {
            break
        }
        if child + 1 <= end && arr[child] < arr[child+1] {
            child = child + 1    //取較大的子節點
        }
        if arr[root] < arr[child] {
            arr[root], arr[child] = arr[child], arr[root]    //較大的子節點成為父節點
            root = child
        } else {
            break
        }
    }
}

七種經典排序算法指標對比

指標.png

參考資料

可視化排序動態圖](https://visualgo.net/en/sorting))
經典排序算法總結與實現 (Python實現)
堆排序 Heap Sort
算法(中文版?第4版)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 概述 排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部...
    蟻前閱讀 5,223評論 0 52
  • 概述:排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部...
    每天刷兩次牙閱讀 3,747評論 0 15
  • 概述排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部的...
    Luc_閱讀 2,309評論 0 35
  • 熱情總是快速褪去,而差距讓人更加落寞。
    柏強閱讀 187評論 0 0
  • 對于死亡,并不陌生。還是幼年的時候,村上有人去世,媽媽就會帶我去幫忙。和村上的人一起為去世的人送葬。 那時候,理解...
    小考拉俱樂部閱讀 282評論 4 1