排序算法是算法面试的必考点。本文用 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 面试高频考点
- 快排 vs 归并:快排平均更快但最坏 O(n²);归并稳定但需 O(n) 空间
- 堆排序为什么不如快排:堆排序缓存不友好(跳跃访问),常数因子大
- 稳定性有什么用:多关键字排序时,稳定排序保证第一关键字相同时,第二关键字顺序不变
四、完整测试代码
以下代码包含所有排序算法的测试,可直接运行:
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% 的排序相关问题都能迎刃而解。
建议:把本文代码复制到本地,逐个打断点跑一遍,理解每一行代码在做什么。纸上得来终觉浅,绝知此事要躬行。