Go: 八大常用排序算法

1. 選擇排序

原理

每次選擇數組中的最小元素,排在最新的第一個位置

算法步驟

[38,65,97,76,13,27,49]
13 [65 97 76 38 27 49]
13 27 [97 76 38 65 49]
13 27 38 [76 97 65 49]
13 27 38 49 [97 65 76]
13 27 38 49 65 [97 76]
13 27 38 49 65 76 [97]
13 27 38 49 65 76 97

代碼實現

func SelectSort(data []int) {
 length := len(data)

 for i := 0; i < length; i++ {
  // 記錄第一個數據
  min := data[i]
  index := i

  // 獲取到最小的值以及對應的索引
  for j := i + 1; j < length; j++ {
   if min > data[j] {
    min = data[j]
    index = j
   }
  }
  // 如果最小的值不是當前序列的第一個值
  // 那麼需要進行交換
  if index != i {
   // 交換最小值
   data[index], data[i] = data[i], min
  }
 }
}

2. 插入排序

原理

假設之前的數組都是一個有序序列,其餘的記錄爲無序序列,從這些無序序列中不斷選取數據,插入到前面已經排序好的有序序列中

算法步驟

[38] 65 97 76 13 27 49
[38 65] 97 76 13 27 49
[38 65 97] 76 13 27 49
[38 65 76 97] 13 27 49
[13 38 65 76 97] 27 49
[13 27 38 65 76 97] 49
[13 27 38 49 65 76 97]

代碼實現

func InsertSort(data []int) {
 for i := 1; i < len(data); i++ {
  tmp, j := data[i], i
  // 如果第j-1個元素比第i個元素大
  // 不滿足遞增的條件,第i個需要移動到前面
  if data[j-1] > tmp {
   // 將第i個元素移動到前面的適當位置
   // 也就是將元素不斷的向右邊移動,直到找到合適的位置擺放
   for j >= 1 && data[j-1] > tmp {
    data[j] = data[j-1]
    j--
   }
  }
  // 將當前遍歷到的值填充到對應的位置
  data[j] = tmp
 }
}

3. 冒泡排序

原理

基本思路: 從第一個元素開始一次對相鄰的記錄進行比較, 當前面的記錄大於後面的記錄,交換其位置,進行一輪比較和換位之後,n 個記錄中的最大記錄將位於第 n 位

算法步驟

{38 65 97 76 13 27 49}
38 65 76 13 27 49 [97]
38 65 13 27 49 [76 97]
38 13 27 49 [65 76 97]
13 27 38 [49 65 76 97]
13 27 [38 49 65 76 97]
13 [27 38 49 65 76 97]
[13 27 38 49 65 76 97]

代碼實現

func BubbleSort(nums []int) {
 flag := false
 n := len(nums)
 // 這裏採用的是不斷將最大的移動到數組的最後面
 // 當然也可以將最小的移動到最前面來
 for i := 0; i < n ; i ++{
  for j := 0; j < n-1 -i; j ++{
   if nums[j] > nums[j+1]{
    nums[j], nums[j+1] = nums[j+1], nums[j]
    flag = true
   }
  }
  if !flag{
   break
  }
  fmt.Println(nums)
 }
}

4. 歸併排序

原理

將一個數組拆成兩份,每一份進行遞歸操作之後都是有序的,然後進行合併,最終是有序的。

算法步驟

{38 65 97 76 13 27 49}
[38 65 97 76] [13 27 49] 
[38 65] [97 76] [13 27] [49]
[38] [65] [97] [76] [13] [27] [49]  // 全部分裂
[38 65] [76 97] [13 27] [49] // 開始進行合併
[38 65 76 97] [13 27 49]
[13 27 38 49 65 76 97]

代碼實現

func MergerSort(nums []int) {
 mergeSort(nums, 0, len(nums)-1)
}

func mergeSort(nums []int, left, right int) {
 if left >= right {
  return
 }
 mid := (right-left)/2 + left
 mergeSort(nums, left, mid)
 mergeSort(nums, mid+1, right)
 merge(nums, left, mid, right)
}

// 合併兩個數組,分別是從 left -> mid , mid -> right 索引開始
func merge(nums []int, left, mid, right int) {
 // 輔助數據
 tmp := make([]int, right-left+1)

 for i := left; i <= right; i++ {
  tmp[i-left] = nums[i]
 }
 i, j := 0, mid-left+1
 k := left
 for k <= right && i <= mid-left && j <= right-left {
  if tmp[i] > tmp[j] {
   nums[k] = tmp[j]
   k++
   j++
  } else {
   nums[k] = tmp[i]
   i++
   k++
  }
 }
 for i <= mid-left {
  nums[k] = tmp[i]
  i++
  k++
 }

 for j <= right-left {
  nums[k] = tmp[j]
  j++
  k++
 }
}

5. 快速排序

快速排序是一種非常高效的排序算法,它採用分而治之的思想,把大的拆分成小的,小的再拆分成更小的。

原理

對於一組給定的記錄,通過一趟排序之後,將原來的序列分成兩部分,其中一部分的所有記錄均比後一部分的所有記錄小,然後再依次對前後兩部分的記錄進行快速排序,遞歸改過程,直到序列中的所有記錄均有序爲止

可以直接認爲是找到了一個分界點,比如如果需要實現遞增,那麼左邊都是小於該數,右邊都是大於該數

算法步驟

  1. 分解:將輸入的序列array[m...n] 劃分成兩個非空子序列array[m...k]array[k+1...n],使得array[k+1...n]中的任意一個元素都不小於array[m...k]中的元素 

  2. 遞歸求解:通過遞歸調用快速排序算法分別對array[m...k]array[k+1...n]進行排序 

  3. 合併:由於對分解出來的兩個子序列的排序是就地進行的,所以在array[m...k]array[k+1...n]都排好序後不需要執行任何計算array[m...n]

代碼實現

func sort(arr []int, left, right int) {
 if left >= right {
  return
 }

 i := left
 j := right

 // 選定一個分界點
 // 如果直接下面這種方式取的話,如果數據是有序的,那麼時間複雜度相比較會比較高
 index := arr[i]
 // 也可以隨機挑選一個  
 // rand.Seed(time.Now().UnixNano())
 // var random int 
 // random = rand.Int() % (right-left+1) + left
 // 將隨機選中的移動到最左邊
 // arr[i], arr[random] = arr[random], arr[i]
 // 這裏只是爲了更新上面的index,如果直接寫的話,可以去掉上的賦值,直接使用index:= arr[i]
 // index = arr[i]

 for i < j {
  // 右邊界不斷向左移動,如果碰到比index小的數據
  // 則需要將左邊的i對應的數據賦值爲對應的值
  for i < j && arr[j] >= index {
   j--
  }
  if i < j {
   arr[i] = arr[j]
   i++
  }
  // 左邊界不斷往右移動,如果碰到比index小的數據
  // 則將右邊的這個數據賦值索引j對應的數據
  for i < j && arr[i] <= index {
   i++
  }

  if i < j {
   arr[j] = arr[i]
   j--
  }


 }
 // 索引i表示的就是
 arr[i] = index

 // 排序左邊子數組
 sort(arr, left, i-1)
 // 排序右邊
 sort(arr, i+1, right)
}

func QuickSort(arr []int) {
 sort(arr, 0, len(arr)-1)
}

基準關鍵字的選取

常用的基準關鍵字的選取有以下方式:

  1. 選取首尾,中間位置上的中值作爲基準關鍵字

  2. 選取隨機數

6. 希爾排序

原理

類似插入排序,但是不是相鄰的進行,而是會跳過幾個位置

算法步驟

{38, 65, 97, 76, 13, 27, 49}
step爲: 3   [38 65 97 76 13 27 49]  // 索引 3 和 0 進行比較並交換
step爲: 3   [38 13 97 76 65 27 49]  // 索引 4 和 1 進行比較並交換
step爲: 3   [38 13 27 76 65 97 49]  // 索引 5 和 2 進行比較並交換
step爲: 3   [38 13 27 49 65 97 76]  // 索引 6 和 3 進行比較並交換
step爲: 1   [13 38 27 49 65 97 76]  // 索引 1 和 0 進行比較並交換
step爲: 1   [13 27 38 49 65 97 76]  // 索引 2 和 1 進行比較並交換
step爲: 1   [13 27 38 49 65 97 76]  // 索引 3 和 2 進行比較並交換
step爲: 1   [13 27 38 49 65 97 76]  // 索引 4 和 3 進行比較並交換
step爲: 1   [13 27 38 49 65 97 76]  // 索引 5 和 4 進行比較並交換
step爲: 1   [13 27 38 49 65 76 97]  // 索引 6 和 5 進行比較並交換

代碼實現

func ShellSort(nums []int) {
 // 步長
 for step := len(nums) / 2; step > 0; step /= 2 {
  for i := step ; i < len(nums); i++ {
   // 直接插入排序
   if nums[i] < nums[i-step] {
    j, tmp := i, nums[i]
    // 找出同一組中比tmp小的值,往後面移動step個位置
    for j >= step && tmp < nums[j-step] {
     nums[j] = nums[j-step]
     j -= step
    }
    nums[j] = tmp
   }
  }
 }
}

7. 堆排序

原理

這裏說一下遞增排序,使用大根堆

大根堆有一個特點,那就是堆的頂部是整個數組中最大的元素,一個比較簡單的想法就是,我把這個元素拿出來,然後插入到其他的數組中,堆中 pop 出堆頂的元素,依次進行,便可以獲取到一個排序序列,不過這種思路需要多餘的一個數組,其實沒有必要,我們來自己看看刪除元素的過程。

首先,將堆頂的元素和最後一個元素進行交換,然後 size--,進行向下冒泡 down,刪除掉最後一個元素。

func pop(arr *[]int) int {
 nums := *arr
 n := len(nums) - 1
    // 交換
 nums[0], nums[n] = nums[n], nums[0]
 down(nums, 0, n)
 x := nums[n]
 // 刪除最後一個元素
 *arr = (*arr)[:n]
 return x
}

注意這裏刪除最後一個元素,我們可以把這個元素保留在這裏,但是讓 down 的時候不接觸到這個元素不就行了?這個很好實現,因爲一般我們都是使用元素的長度作爲 down 的一個 size 參數,我們指定一下就好,主要的邏輯代碼如下:

func heapSort(nums []int) {
 size := len(nums)
 // 首先建堆
 buildHeap(nums)
 // 然後這個時候最大的元素在第一個
 for size > 1 {
  // 第一個元素和最後一個進行交換
  nums[0], nums[size-1] = nums[size-1], nums[0]
  size--
  down(nums, 0, size)
 }
}

代碼實現

// 建大根堆
func buildHeap(nums []int) {
 size := len(nums)
 for i := size / 2; i >= 0; i-- {
  down(nums, i, size)
 }
}

// 插入元素的時候需要使用
func up(nums []int, i int) {
 for {
  // 父結點計算公式!!
  parent := (i - 1) / 2
  // 如果父結點大於子節點了,說明不需要調換,已經滿足條件了
  // 如果 i == parent 說明 i = 0 了,沒有父結點,也不需要調換
  if i == parent || nums[parent] > nums[i] {
   break
  }
  // 父結點沒有子節點i的值大,需要調換
  nums[parent], nums[i] = nums[i], nums[parent]
 }
}

// 初始化的時候需要建堆
// 刪除元素的時候需要down:
// 將第一個元素和最後一個元素進行調換, 然後重新 down(nums, 0, size)
func down(nums []int, i int, size int) {
 left, right := 2*i+1, 2*i+2
 j := i

 // 找到最大的元素的下標
 if left < size && nums[left] > nums[j] {
  j = left
 }
 if right < size && nums[right] > nums[j] {
  j = right
 }

 if j != i {
  nums[i], nums[j] = nums[j], nums[i]
  down(nums, j, size)
 }
}

// 不用遞歸也可以
func down2(nums []int, i, size int) {
 for {
  left, right := i*2+1, i*2+2
  // 說明i是葉子節點
  if left >= size {
   break
  }
  // 找到最大的節點
  largest := i
  if left < size && nums[left] > nums[largest] {
   largest = left
  }
  if right < size && nums[right] > nums[largest] {
   largest = right
  }

  if largest != i {
   nums[i], nums[largest] = nums[largest], nums[i]
   i = largest
  } else {
   // 如果最大的節點就是本身,那麼後面也不需要進行操作了
   break
  }
 }
}

// 從堆中彈出一個數
// 首先將堆頂的元素和最後一個元素進行調換,然後進行down操作
func pop(arr *[]int) int {
 nums := *arr
 n := len(nums) - 1
 nums[0], nums[n] = nums[n], nums[0]
 down(nums, 0, n)
 x := nums[n]

 *arr = (*arr)[:n]
 return x
}

func HeapSort(nums []int) {
 size := len(nums)
 // 首先建堆
 buildHeap(nums)
 // 然後這個時候最大的元素在第一個
 for size > 1 {
  // 第一個元素和最後一個進行交換
  nums[0], nums[size-1] = nums[size-1], nums[0]
  size--
  down(nums, 0, size)
 }
}

8. 計數排序

原理

設置一個長度爲元素最大值最小值的空間,將元素一個一個對應一個索引,如果這個索引中已經存在元素,那麼對應的值 + 1,說明這個索引處對應多個元素,最後遍歷一遍這個索引空間,值爲 0 的不需要添加,值爲多個的一次添加到原數組中

計數排序適合於數組中的值在一定範圍內的數據,比如說年齡,數據可能需要進行整理,比如 [10000, 20000] 的數據,我們可以首先整理爲 [0, 10000],然後再轉換回去。

代碼實現

// 比如我們需要排序年齡,那麼我們可以估算年齡在[0,200]中
func CountingSort(nums []int) {
    count := make([]int, 200)
    for _, num := range nums {
        count[num] ++ 
    }
    index := 0 
    for i :=0; i < len(count); i ++ {
        // 出現了多少次,排序後添加到原數組多少次
        for j:= 0; j < count[i]; j++ {
            nums[index] = i 
            index ++ 
        }
    }
}

9. 總結

下圖給出了每種排序算法的穩定性和效率的比較:

如果想要更直觀的看排序過程,推薦使用 Data Structure Visualizations[1] 這個網站,進行查看,比如想要看插入排序的,可以選擇 Insertion Sort 即可

具體代碼實現參見 GitHub[2]

鏈接

[1] Data Structure Visualizations: https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

[2] GitHub: https://github.com/junhaideng/Blog/tree/main/alg/sort

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/dtCaWKuxkA73L3kRLRn-jA