常见排序算法汇总 | Go 语言实现

122 阅读4分钟

冒泡排序

思路:从第一个元素开始往后比较,如果遇到比当前数大的数,就交换,直到数组末尾,此时末尾就是数组最大元素,然后重复此操作,只是比较到上一个末尾的前一个元素即可

 func BubbleSort(sli []int) {
    if sli == nil || len(sli) < 2 {
       return
    }
 ​
    for i := 0; i < len(sli)-1; i++ {
       for j := 0; j < len(sli)-i-1; j++ {
          if sli[j] > sli[j+1] {
             sli[j], sli[j+1] = sli[j+1], sli[j]
          }
       }
    }
 }

插入排序

思路:从最左边第一个元素开始,此时只有一个元素,所以看作是有序的,然后查看下一个元素,在前面已经有序的数中找到自己应该在的位置,循环此操作。

 func InsertionSort(sli []int) {
    if sli == nil || len(sli) < 2 {
       return
    }
 ​
    for i := 0; i < len(sli)-1; i++ {
       for j := i + 1; j > 0; j-- {
          if sli[j] > sli[j-1] {
             break
          }
          sli[j], sli[j-1] = sli[j-1], sli[j]
       }
    }
 }

选择排序

思路:i = 0 ,从 i 开始遍历数组找到最小数的 index,将其放在 i 位置,i++,循环此操作

 func SelectionSort(sli []int) {
    if sli == nil || len(sli) < 2 {
       return
    }
 ​
    for i := 0; i < len(sli)-1; i++ {
       min := i
       for j := i + 1; j < len(sli); j++ {
          if sli[min] > sli[j] {
             sli[min], sli[j] = sli[j], sli[min]
          }
       }
    }
 }

归并排序

思路:将数组分为左边与右边两半部分,使其各自有序,然后做归并操作。

归并:新建一个 help 数组与原数组等大小,使用两个指针 p1,p2 分别指向原数组左右两边起始位置,按照一定规则比较 p1 p2,假设 p1 在比较中胜出,将 p1 指向的元素放入 help 数组,p1 向右移动,再次进行比较,放入 help 数组的过程,直到 p1 或 p2 越界,然后将未越界一方的剩余元素放入 help 中,将 help 拷贝到原数组中。

 func MergeSort(sli []int) {
    if sli == nil || len(sli) < 2 {
       return
    }
 ​
    process(sli, 0, len(sli)-1)
 }
 ​
 func process(arr []int, l, r int) {
    if l == r {
       return
    }
    mid := l + (r-l)>>1
    process(arr, l, mid)
    process(arr, mid+1, r)
    merge(arr, l, r, mid)
 }
 ​
 func merge(arr []int, l, r, mid int) {
    help := make([]int, r-l+1)
    i, p1, p2 := 0, l, mid+1
    for p1 <= mid && p2 <= r {
       if arr[p1] < arr[p2] {
          help[i] = arr[p1]
          p1++
       } else {
          help[i] = arr[p2]
          p2++
       }
       i++
    }
    for p1 <= mid {
       help[i] = arr[p1]
       p1++
       i++
    }
    for p2 <= r {
       help[i] = arr[p2]
       p2++
       i++
    }
    i = 0
    for l <= r {
       arr[l] = help[i]
       i++
       l++
    }
 }

快速排序

思路:每次选定一个 pivot,使数组左边全部小于 pivot,右边全部大于 pivot,中间为等于 pivot,然后在得到的左边和右边重复此操作,直到数组有序

 // QuickSort 3.0 版本,每次随机选取一个数作为基准
 func QuickSort(sli []int) {
    quickSort(sli, 0, len(sli)-1)
 }
 ​
 func quickSort(arr []int, l, r int) {
    if l < r {
       rand.Seed(time.Now().UnixNano())
       randIndex := rand.Intn(r-l+1) + l
       arr[randIndex], arr[r] = arr[r], arr[randIndex]
       p := partition(arr, l, r)
       quickSort(arr, l, p[0]-1)
       quickSort(arr, p[1]+1, r)
    }
 }
 ​
 // 返回小于区的右边界和大于区的左边界
 func partition(arr []int, l, r int) []int {
    less, more := l-1, r
    for l < more {
       if arr[l] < arr[r] {
          arr[l], arr[less+1] = arr[less+1], arr[l]
          less++
          l++
       } else if arr[l] > arr[r] {
          arr[l], arr[more-1] = arr[more-1], arr[l]
          more--
       } else {
          l++
       }
    }
    arr[r], arr[more] = arr[more], arr[r]
    return []int{less + 1, more}
 }

堆排序

思路:将数组转化为大根堆,将堆顶与堆尾交换,然后 heapSize--,重复此操作直到 heapSize减小到1

 func HeapSort(sli []int) {
    if sli == nil || len(sli) < 2 {
       return
    }
 ​
    for heapSize := heap.ConvertArrToHeap(sli, 0, len(sli)-1, heap.MaxHeapComparator()); heapSize > 1; {
       sli[0], sli[heapSize-1] = sli[heapSize-1], sli[0]
       heapSize--
       heap.Heapify(sli, 0, heapSize, heap.MaxHeapComparator())
    }
 }

计数排序

计数排序适用与数组的数在 [left,right] 之间,且 right - left 不大

思路:创建一个 count 数组,大小为 right - left + 1,遍历原数组,每次都执行 count[array[i]-left]++ 的操作,这样就得到了原数组的词频统计表 count,再次遍历count,将 count 的每一个元素都输出 count[i] 次,每一个元素的值为 i+left

 func CountingSort(sli, r []int) {
    if sli == nil || len(sli) < 2 || len(r) < 2 {
       return
    }
 ​
    left, right := r[0], r[1]
    count := make([]int, right-left+1)
 ​
    for i := 0; i < len(sli); i++ {
       count[sli[i]-left]++
    }
 ​
    index := 0
    for i := 0; i < len(count); i++ {
       for j := count[i]; j > 0; j-- {
          sli[index] = i + left
          index++
       }
    }
 }

基数排序

思路:先创建十个桶,编号为 0-9,遍历数组,每次查看元素某一位上的数(个位十位百位...),将其放入对应编号的桶中,然后将桶中的元素按编号从左往右依次倒出(先入先出),只需要执行数组中元素最多位数次即可完成排序

这里使用的是词频表法实现基数排序

 func RadixSort(sli []int) {
    if sli == nil || len(sli) < 2 {
       return
    }
 ​
    digit := maxBits(sli)
    bucket := make([]int, len(sli))
 ​
    for d := 1; d <= digit; d++ {
       count := make([]int, 10)
       for _, v := range sli {
          count[getDigit(v, d)]++
       }
 ​
       for i := 1; i < 10; i++ {
          count[i] += count[i-1]
       }
 ​
       for i := len(sli) - 1; i >= 0; i-- {
          j := getDigit(sli[i], d)
          bucket[count[j]-1] = sli[i]
          count[j]--
       }
       for i, _ := range sli {
          sli[i] = bucket[i]
       }
    }
 }
 ​
 func getDigit(x, d int) (digit int) {
    return (x / int(math.Pow10(d-1))) % 10
 }
 ​
 func maxBits(arr []int) (result int) {
    max := 0
    for _, v := range arr {
       if v > max {
          max = v
       }
    }
 ​
    for max != 0 {
       result++
       max /= 10
    }
    return
 }

复杂度

type时间复杂度空间复杂度是否稳定
冒泡排序o(N^2)O(1)
插入排序O(N^2)O(1)
选择排序O(N^2)O(1)
归并排序O(N*logN)O(N)
快速排序O(N*logN)O(logN)
堆排序O(N*logN)O(1)
计数排序O(N+K)O(N)
基数排序O(dN)

常见问题

  1. 归并排序内部缓存法可以将空间复杂度降为 O(1),但是比较难
  2. ”原地归并排序“会让时间复杂度降为 O(N^2) ,且比较难,完全没必要参考
  3. 快速排序可以做到稳定,但是比较难,参考 "01 stable sort"
  4. 目前没有找到时间复杂度为 O(N*logN),额外空间复杂度为 O(1) ,又稳定的算法

排序算法的选择

我们应该根据数据的状况,对稳定性的要求等方面考虑选择排序算法。

如我们排序的不是基本数字类型,而是 Student 类型,我们就应该选择一种稳定的排序算法,如果排序的数据量并不大,我们可以直接选择时间复杂度为 O(N^2) 的排序算法,再比如我们可以在快速排序递归到较小数据量时,我们可以使用选择排序来提高算法效率(n < 60)。

排序算法的应用

起始排序算法不止是可以应用在排序上,我们也可以应用其算法的实现过程来实现很多东西,如归并排序的归并过程,快速排序的 partition 过程都是值得我们学习的,以上列出的排序算法都是值得我们学习并掌握的。