十大经典排序算法 Go 实现全解:从入门到面试通关

排序算法是算法面试的必考点。本文用 Go 语言实现 10 大经典排序算法,配合图解和复杂度分析,帮你建立完整的排序知识体系。


一、算法总览速查表

算法 平均时间 最坏时间 最好时间 空间复杂度 稳定性 核心思想
冒泡排序 O(n²) O(n²) O(n) O(1) ✅ 稳定 相邻交换,大数后移
选择排序 O(n²) O(n²) O(n²) O(1) ❌ 不稳定 每次选最小放前面
插入排序 O(n²) O(n²) O(n) O(1) ✅ 稳定 像打扑克,逐张插入
希尔排序 O(n^1.3) O(n²) O(n) O(1) ❌ 不稳定 分组插入,逐步缩小间隔
归并排序 O(n log n) O(n log n) O(n log n) O(n) ✅ 稳定 分而治之,合并有序数组
快速排序 O(n log n) O(n²) O(n log n) O(log n) ❌ 不稳定 选基准,小的放左大的放右
堆排序 O(n log n) O(n log n) O(n log n) O(1) ❌ 不稳定 建大顶堆,堆顶移末尾
计数排序 O(n+k) O(n+k) O(n+k) O(k) ✅ 稳定 统计每个数出现次数
桶排序 O(n+k) O(n²) O(n+k) O(n+k) ✅ 稳定 分桶,桶内各自排序
基数排序 O(n×k) O(n×k) O(n×k) O(n+k) ✅ 稳定 按位排序,从个位到最高位

💡 稳定性:相等元素的相对顺序是否保持不变。


二、十大排序算法详解

1. 冒泡排序(Bubble Sort)

思想:像气泡一样,大的数不断"浮"到数组末尾。

图解

ini 复制代码
初始: [5, 3, 8, 4, 2]
第1轮: [3, 5, 4, 2, 8]  (8浮到最后)
第2轮: [3, 4, 2, 5, 8]  (5浮到倒数第二)
第3轮: [3, 2, 4, 5, 8]
第4轮: [2, 3, 4, 5, 8]  ✅

Go 实现

go 复制代码
package main

import "fmt"

// BubbleSort 冒泡排序
func BubbleSort(arr []int) {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        swapped := false
        for j := 0; j < n-1-i; j++ {
            if arr[j] > arr[j+1] {
                arr[j], arr[j+1] = arr[j+1], arr[j]
                swapped = true
            }
        }
        if !swapped { // 已有序,提前退出
            break
        }
    }
}

func main() {
    arr := []int{5, 3, 8, 4, 2, 9, 1, 6}
    BubbleSort(arr)
    fmt.Println(arr) // [1 2 3 4 5 6 8 9]
}

复杂度分析

  • 最好(已有序):只需 1 轮,O(n)
  • 最坏(逆序):n(n-1)/2 次比较,O(n²)
  • 空间:O(1),原地排序

2. 选择排序(Selection Sort)

思想:每次从未排序部分选出最小元素,放到已排序部分的末尾。

图解

ini 复制代码
初始: [5, 3, 8, 4, 2]
第1轮: [2, 3, 8, 4, 5]  (选2放首位)
第2轮: [2, 3, 8, 4, 5]  (选3,已在位置)
第3轮: [2, 3, 4, 8, 5]  (选4)
第4轮: [2, 3, 4, 5, 8]  ✅

Go 实现

go 复制代码
// SelectionSort 选择排序
func SelectionSort(arr []int) {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        minIdx := i
        for j := i + 1; j < n; j++ {
            if arr[j] < arr[minIdx] {
                minIdx = j
            }
        }
        arr[i], arr[minIdx] = arr[minIdx], arr[i]
    }
}

复杂度分析

  • 无论最好最坏,都要完整比较:O(n²)
  • 空间:O(1)
  • 不稳定:交换可能破坏相等元素的相对顺序

🌰 不稳定例子:[5, 8, 5, 2],第一次选 2 和 8 交换,两个 5 的相对顺序变了。


3. 插入排序(Insertion Sort)

思想:像整理扑克牌,拿起一张插入到已排序部分的正确位置。

图解

ini 复制代码
初始: [5, 3, 8, 4, 2]
拿3: [3, 5, 8, 4, 2]  (3插入5前面)
拿8: [3, 5, 8, 4, 2]  (8已在正确位置)
拿4: [3, 4, 5, 8, 2]  (4插入3和5之间)
拿2: [2, 3, 4, 5, 8]  ✅

Go 实现

go 复制代码
// InsertionSort 插入排序
func InsertionSort(arr []int) {
    n := len(arr)
    for i := 1; i < n; i++ {
        key := arr[i]
        j := i - 1
        // 将大于key的元素后移
        for j >= 0 && arr[j] > key {
            arr[j+1] = arr[j]
            j--
        }
        arr[j+1] = key
    }
}

复杂度分析

  • 最好(已有序):每个元素只比较 1 次,O(n)
  • 最坏(逆序):每个元素比较 i 次,O(n²)
  • 空间:O(1)
  • 稳定:相等元素不移动,相对顺序不变

4. 希尔排序(Shell Sort)

思想:插入排序的升级版。先让相距较远的元素有序,再逐步缩小间隔。

图解(间隔序列 5, 2, 1):

ini 复制代码
初始: [8, 3, 5, 2, 7, 1, 4, 6]
间隔5分组: [8,1], [3,4], [5,6], [2], [7]
          → [1, 3, 5, 2, 7, 8, 4, 6]
间隔2分组: [1,5,7,4], [3,2,8,6]
          → [1, 2, 4, 3, 5, 6, 7, 8]
间隔1: 普通插入排序 → [1, 2, 3, 4, 5, 6, 7, 8] ✅

Go 实现

go 复制代码
// ShellSort 希尔排序
func ShellSort(arr []int) {
    n := len(arr)
    // 间隔序列:n/2, n/4, ..., 1
    for gap := n / 2; gap > 0; gap /= 2 {
        // 对每个分组进行插入排序
        for i := gap; i < n; i++ {
            temp := arr[i]
            j := i
            for j >= gap && arr[j-gap] > temp {
                arr[j] = arr[j-gap]
                j -= gap
            }
            arr[j] = temp
        }
    }
}

复杂度分析

  • 时间复杂度取决于间隔序列,平均约 O(n^1.3)
  • 空间:O(1)
  • 不稳定:远距离交换可能破坏稳定性

5. 归并排序(Merge Sort)

思想:分而治之。先把数组分成两半分别排序,再合并两个有序数组。

图解

css 复制代码
           [5, 3, 8, 4, 2, 9, 1, 6]
           /                      \
      [5, 3, 8, 4]            [2, 9, 1, 6]
      /        \              /        \
   [5, 3]    [8, 4]        [2, 9]    [1, 6]
   /   \     /   \         /   \     /   \
 [5]  [3]  [8]  [4]      [2]  [9]  [1]  [6]
   \   /     \   /         \   /     \   /
   [3,5]     [4,8]         [2,9]     [1,6]
      \        /               \       /
    [3, 4, 5, 8]            [1, 2, 6, 9]
            \                  /
         [1, 2, 3, 4, 5, 6, 8, 9] ✅

Go 实现

go 复制代码
// MergeSort 归并排序
func MergeSort(arr []int) []int {
    n := len(arr)
    if n <= 1 {
        return arr
    }
    mid := n / 2
    left := MergeSort(arr[:mid])
    right := MergeSort(arr[mid:])
    return merge(left, right)
}

func merge(left, right []int) []int {
    result := make([]int, 0, len(left)+len(right))
    i, j := 0, 0
    for i < len(left) && j < len(right) {
        if left[i] <= right[j] {
            result = append(result, left[i])
            i++
        } else {
            result = append(result, right[j])
            j++
        }
    }
    result = append(result, left[i:]...)
    result = append(result, right[j:]...)
    return result
}

复杂度分析

  • 时间:始终 O(n log n),不受数据分布影响
  • 空间:O(n),需要额外数组存储合并结果
  • 稳定 :合并时 <= 保证相等元素左边先出

6. 快速排序(Quick Sort)

思想:选个基准数,小的放左边,大的放右边,递归处理两边。

图解(选最右为基准):

ini 复制代码
初始: [5, 3, 8, 4, 2, 9, 1, 6]  基准=6
分区: [5, 3, 4, 2, 1] | 6 | [8, 9]
     左边<6          基准   右边>6

递归左边: [5, 3, 4, 2, 1] 基准=1
        [] | 1 | [5, 3, 4, 2]
        ...
最终: [1, 2, 3, 4, 5, 6, 8, 9] ✅

Go 实现

go 复制代码
// QuickSort 快速排序
func QuickSort(arr []int, left, right int) {
    if left >= right {
        return
    }
    pivot := partition(arr, left, right)
    QuickSort(arr, left, pivot-1)
    QuickSort(arr, pivot+1, right)
}

func partition(arr []int, left, right int) int {
    pivot := arr[right] // 选最右为基准
    i := left - 1       // i指向小于基准的最后一个位置
    for j := left; j < right; j++ {
        if arr[j] < pivot {
            i++
            arr[i], arr[j] = arr[j], arr[i]
        }
    }
    arr[i+1], arr[right] = arr[right], arr[i+1]
    return i + 1
}

复杂度分析

  • 平均:O(n log n)
  • 最坏(已有序+选首尾为基准):O(n²)
  • 空间:O(log n),递归栈深度
  • 不稳定:交换会打乱相等元素顺序

💡 优化:随机选基准,或三数取中,避免最坏情况。


7. 堆排序(Heap Sort)

思想:利用堆这种数据结构。先建大顶堆,然后把堆顶(最大值)移到末尾,再调整堆。

图解

makefile 复制代码
数组: [5, 3, 8, 4, 2]
建大顶堆:       8
             /   \
            4     5
           / \
          3   2

第1轮: 交换8和2 → [2, 3, 5, 4, 8]  调整堆
       5
      / \
     3   2
    /
   4

第2轮: 交换5和4 → [2, 3, 4, 5, 8]  ...
...
最终: [2, 3, 4, 5, 8] ✅

Go 实现

go 复制代码
// HeapSort 堆排序
func HeapSort(arr []int) {
    n := len(arr)
    // 建大顶堆(从最后一个非叶子节点开始)
    for i := n/2 - 1; i >= 0; i-- {
        heapify(arr, n, i)
    }
    // 逐个提取堆顶
    for i := n - 1; i > 0; i-- {
        arr[0], arr[i] = arr[i], arr[0] // 堆顶移到末尾
        heapify(arr, i, 0)              // 调整剩余元素
    }
}

func heapify(arr []int, n, i int) {
    largest := i
    left := 2*i + 1
    right := 2*i + 2
    if left < n && arr[left] > arr[largest] {
        largest = left
    }
    if right < n && arr[right] > arr[largest] {
        largest = right
    }
    if largest != i {
        arr[i], arr[largest] = arr[largest], arr[i]
        heapify(arr, n, largest)
    }
}

复杂度分析

  • 时间:建堆 O(n),每次调整 O(log n),共 n 次 → O(n log n)
  • 空间:O(1),原地排序
  • 不稳定:远距离交换破坏稳定性

8. 计数排序(Counting Sort)

思想:统计每个数出现的次数,然后按次数输出。

适用:数据范围小且为整数的场景。

图解

makefile 复制代码
数组: [3, 1, 4, 1, 5, 9, 2, 6]
范围 0-9,统计:
索引: 0 1 2 3 4 5 6 7 8 9
次数: 0 2 1 1 1 1 1 0 0 1

输出: 1, 1, 2, 3, 4, 5, 6, 9 ✅

Go 实现

go 复制代码
// CountingSort 计数排序
func CountingSort(arr []int) {
    if len(arr) == 0 {
        return
    }
    // 找最大最小值
    maxVal, minVal := arr[0], arr[0]
    for _, v := range arr {
        if v > maxVal {
            maxVal = v
        }
        if v < minVal {
            minVal = v
        }
    }
    // 计数数组
    count := make([]int, maxVal-minVal+1)
    for _, v := range arr {
        count[v-minVal]++
    }
    // 按计数回填
    idx := 0
    for i, c := range count {
        for c > 0 {
            arr[idx] = i + minVal
            idx++
            c--
        }
    }
}

复杂度分析

  • 时间:O(n + k),k 是数据范围
  • 空间:O(k),需要计数数组
  • 稳定:可以改造为稳定版本(从后往前遍历)

9. 桶排序(Bucket Sort)

思想:把数据分到若干个桶里,每个桶内部排序,再按顺序合并。

图解

ini 复制代码
数组: [0.42, 0.32, 0.33, 0.52, 0.37, 0.47, 0.51]
分5个桶 (0-0.2, 0.2-0.4, 0.4-0.6, 0.6-0.8, 0.8-1.0)
桶0: []
桶1: [0.32, 0.33, 0.37]  → 排序后 [0.32, 0.33, 0.37]
桶2: [0.42, 0.47, 0.52, 0.51] → 排序后 [0.42, 0.47, 0.51, 0.52]
桶3: []
桶4: []

合并: [0.32, 0.33, 0.37, 0.42, 0.47, 0.51, 0.52] ✅

Go 实现

go 复制代码
// BucketSort 桶排序(适用于 0~1 之间的小数)
func BucketSort(arr []float64) {
    n := len(arr)
    if n <= 1 {
        return
    }
    // 创建 n 个桶
    buckets := make([][]float64, n)
    for i := range buckets {
        buckets[i] = make([]float64, 0)
    }
    // 入桶
    for _, v := range arr {
        idx := int(v * float64(n))
        if idx == n {
            idx = n - 1
        }
        buckets[idx] = append(buckets[idx], v)
    }
    // 每个桶内排序
    for i := range buckets {
        sort.Float64s(buckets[i]) // 使用标准库排序
    }
    // 合并
    idx := 0
    for i := range buckets {
        for _, v := range buckets[i] {
            arr[idx] = v
            idx++
        }
    }
}

复杂度分析

  • 平均:O(n + k),k 为桶数
  • 最坏(全挤在一个桶):O(n²)
  • 空间:O(n + k)
  • 稳定:桶内排序稳定则整体稳定

10. 基数排序(Radix Sort)

思想:按位排序。先按个位排,再按十位排,再按百位排...

适用:非负整数,且位数不太多的场景。

图解

ini 复制代码
数组: [170, 45, 75, 90, 2, 24, 802, 66]

按个位排序: [170, 90, 2, 802, 24, 45, 75, 66]
按十位排序: [2, 802, 24, 45, 66, 170, 75, 90]
按百位排序: [2, 24, 45, 66, 75, 90, 170, 802] ✅

Go 实现

go 复制代码
// RadixSort 基数排序
func RadixSort(arr []int) {
    if len(arr) == 0 {
        return
    }
    // 找最大值,确定最大位数
    maxVal := arr[0]
    for _, v := range arr {
        if v > maxVal {
            maxVal = v
        }
    }
    // 对每一位进行计数排序
    for exp := 1; maxVal/exp > 0; exp *= 10 {
        countingSortByDigit(arr, exp)
    }
}

func countingSortByDigit(arr []int, exp int) {
    n := len(arr)
    output := make([]int, n)
    count := make([]int, 10)

    // 统计当前位数字出现次数
    for i := 0; i < n; i++ {
        digit := (arr[i] / exp) % 10
        count[digit]++
    }
    // 累加计数,确定位置
    for i := 1; i < 10; i++ {
        count[i] += count[i-1]
    }
    // 从后往前,稳定排序
    for i := n - 1; i >= 0; i-- {
        digit := (arr[i] / exp) % 10
        output[count[digit]-1] = arr[i]
        count[digit]--
    }
    // 复制回原数组
    for i := 0; i < n; i++ {
        arr[i] = output[i]
    }
}

复杂度分析

  • 时间:O(n × k),k 为最大数的位数
  • 空间:O(n + k)
  • 稳定:按位排序时使用稳定的计数排序

三、综合对比与选择指南

3.1 一图看懂复杂度

scss 复制代码
速度(快 → 慢)

O(n)        ████                    计数/桶/基数(数据范围小)
O(n log n)  ████████                归并、快排、堆排
O(n²)       ████████████████        冒泡、选择、插入
O(n^1.3)    ██████████              希尔排序

3.2 如何选择排序算法?

场景 推荐算法 原因
数据量小(n < 50) 插入排序 常数小,实现简单
数据基本有序 插入/冒泡 最好情况 O(n)
通用场景(大数据) 快速排序 平均最快,缓存友好
需要稳定排序 归并排序 稳定 + O(n log n)
内存受限 堆排序 原地排序,O(1)空间
数据范围小且整数 计数排序 O(n) 线性时间
浮点数且分布均匀 桶排序 平均 O(n)
多位整数 基数排序 O(n×k),k为位数

3.3 面试高频考点

  1. 快排 vs 归并:快排平均更快但最坏 O(n²);归并稳定但需 O(n) 空间
  2. 堆排序为什么不如快排:堆排序缓存不友好(跳跃访问),常数因子大
  3. 稳定性有什么用:多关键字排序时,稳定排序保证第一关键字相同时,第二关键字顺序不变

四、完整测试代码

以下代码包含所有排序算法的测试,可直接运行:

go 复制代码
package main

import (
    "fmt"
    "math/rand"
    "sort"
    "time"
)

func main() {
    // 生成随机测试数据
    n := 10000
    original := make([]int, n)
    for i := 0; i < n; i++ {
        original[i] = rand.Intn(100000)
    }

    algorithms := []struct {
        name string
        fn   func([]int)
    }{
        {"冒泡排序", BubbleSort},
        {"选择排序", SelectionSort},
        {"插入排序", InsertionSort},
        {"希尔排序", ShellSort},
        {"归并排序", func(arr []int) {
            copyArr := make([]int, len(arr))
            copy(copyArr, arr)
            result := MergeSort(copyArr)
            copy(arr, result)
        }},
        {"快速排序", func(arr []int) { QuickSort(arr, 0, len(arr)-1) }},
        {"堆排序", HeapSort},
        {"计数排序", CountingSort},
    }

    for _, algo := range algorithms {
        arr := make([]int, n)
        copy(arr, original)

        start := time.Now()
        algo.fn(arr)
        elapsed := time.Since(start)

        // 验证正确性
        if !isSorted(arr) {
            fmt.Printf("❌ %s 排序错误!\n", algo.name)
            continue
        }
        fmt.Printf("✅ %s: %v\n", algo.name, elapsed)
    }
}

func isSorted(arr []int) bool {
    for i := 1; i < len(arr); i++ {
        if arr[i] < arr[i-1] {
            return false
        }
    }
    return true
}

// ========== 排序算法实现 ==========

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

func SelectionSort(arr []int) {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        minIdx := i
        for j := i + 1; j < n; j++ {
            if arr[j] < arr[minIdx] {
                minIdx = j
            }
        }
        arr[i], arr[minIdx] = arr[minIdx], arr[i]
    }
}

func InsertionSort(arr []int) {
    for i := 1; i < len(arr); i++ {
        key, j := arr[i], i-1
        for j >= 0 && arr[j] > key {
            arr[j+1] = arr[j]
            j--
        }
        arr[j+1] = key
    }
}

func ShellSort(arr []int) {
    n := len(arr)
    for gap := n / 2; gap > 0; gap /= 2 {
        for i := gap; i < n; i++ {
            temp, j := arr[i], i
            for j >= gap && arr[j-gap] > temp {
                arr[j] = arr[j-gap]
                j -= gap
            }
            arr[j] = temp
        }
    }
}

func MergeSort(arr []int) []int {
    if len(arr) <= 1 {
        return arr
    }
    mid := len(arr) / 2
    left := MergeSort(arr[:mid])
    right := MergeSort(arr[mid:])
    return merge(left, right)
}

func merge(left, right []int) []int {
    result := make([]int, 0, len(left)+len(right))
    i, j := 0, 0
    for i < len(left) && j < len(right) {
        if left[i] <= right[j] {
            result = append(result, left[i])
            i++
        } else {
            result = append(result, right[j])
            j++
        }
    }
    result = append(result, left[i:]...)
    result = append(result, right[j:]...)
    return result
}

func QuickSort(arr []int, left, right int) {
    if left >= right {
        return
    }
    pivot := partition(arr, left, right)
    QuickSort(arr, left, pivot-1)
    QuickSort(arr, pivot+1, right)
}

func partition(arr []int, left, right int) int {
    pivot := arr[right]
    i := left - 1
    for j := left; j < right; j++ {
        if arr[j] < pivot {
            i++
            arr[i], arr[j] = arr[j], arr[i]
        }
    }
    arr[i+1], arr[right] = arr[right], arr[i+1]
    return i + 1
}

func HeapSort(arr []int) {
    n := len(arr)
    for i := n/2 - 1; i >= 0; i-- {
        heapify(arr, n, i)
    }
    for i := n - 1; i > 0; i-- {
        arr[0], arr[i] = arr[i], arr[0]
        heapify(arr, i, 0)
    }
}

func heapify(arr []int, n, i int) {
    largest, left, right := i, 2*i+1, 2*i+2
    if left < n && arr[left] > arr[largest] {
        largest = left
    }
    if right < n && arr[right] > arr[largest] {
        largest = right
    }
    if largest != i {
        arr[i], arr[largest] = arr[largest], arr[i]
        heapify(arr, n, largest)
    }
}

func CountingSort(arr []int) {
    if len(arr) == 0 {
        return
    }
    maxVal, minVal := arr[0], arr[0]
    for _, v := range arr {
        if v > maxVal {
            maxVal = v
        }
        if v < minVal {
            minVal = v
        }
    }
    count := make([]int, maxVal-minVal+1)
    for _, v := range arr {
        count[v-minVal]++
    }
    idx := 0
    for i, c := range count {
        for c > 0 {
            arr[idx] = i + minVal
            idx++
            c--
        }
    }
}

五、总结

要点 内容
必会算法 快排、归并、堆排(面试三巨头)
必会分析 时间/空间复杂度、稳定性判断
最佳实践 小数据用插入,大数据用快排,要稳定用归并
避坑指南 快排注意最坏情况,归并注意空间开销

排序算法是算法大厦的基石,掌握这 10 种算法,面试中 90% 的排序相关问题都能迎刃而解。

建议:把本文代码复制到本地,逐个打断点跑一遍,理解每一行代码在做什么。纸上得来终觉浅,绝知此事要躬行。

相关推荐
liliangcsdn30 分钟前
如何对IC时间序列进行汇总统计分析示例
人工智能·算法·机器学习
leeyi34 分钟前
把规则搬回家:三个 BC 的贫血→充血重构实录(第103篇)
后端
土司大王39 分钟前
LeetCode hot100——缺失的第一个正数
数据结构·算法·leetcode
Cccp.1231 小时前
【leetcode】(二)认识O(NlogN)的排序
算法·leetcode
kakawzw1 小时前
Netty源码笔记
java·服务器·后端
明月_清风1 小时前
算法时间复杂度:给小白的一堂"算快慢"课
后端·算法
vivo互联网技术1 小时前
TinySR:面向真实世界图像超分辨率的轻量级扩散模型
人工智能·算法
Nturmoils1 小时前
内网穿透原来这么简单:Natapp 从注册到公网访问完整教程
后端
寻求出路的程序媛2 小时前
分布式 & 高性能 & 高可用 体系、学习重点、面试点
分布式·后端·面试·性能优化