排序算法详解

一、基础概念

1.1 复杂度

  • 时间复杂度:衡量随数据规模 n 增长,操作次数的量级。常用平均、最坏、最好三种。
  • 空间复杂度:除输入外,额外占用内存的量级(如递归栈、辅助数组)。

本文用 O 表示渐进上界,如 O(n²)、O(n log n)、O(n + k) 等。下文中各算法的复杂度表统一为:平均时间最坏时间最好时间空间稳定性

1.2 稳定性

稳定排序:相等元素在排序前后相对顺序不变。

  • 为何重要:多关键字排序时(先按分数再按学号),稳定排序能保证同分时学号顺序不变。
  • 典型场景:先按日期再按金额,希望同一天内金额顺序保持。

本文中的稳定算法:冒泡、插入、归并、计数、基数。


二、九大排序算法

以下各算法均对 IntArray 原地排序(计数与基数会写回原数组),结构为:思想 → 步骤 → 复杂度 → 代码

2.1 简单比较排序 O(n²)

1. 冒泡排序 (Bubble Sort)

思想:多轮扫描数组,每轮只比较相邻两项,若逆序则交换,使较大值像「气泡」一样逐步移到右侧;若某轮未发生交换则提前结束。

步骤

  1. i 从 0 到 n-2(共 n-1 轮),每轮设 swapped = false
  2. j 从 0 到 n-2-i:若 arr[j] > arr[j+1] 则交换并设 swapped = true
  3. 若未发生交换则 break;否则进入下一轮。
平均时间 最坏时间 最好时间 空间 稳定性
O(n²) O(n²) O(n) O(1) 稳定
kotlin 复制代码
fun bubbleSort(arr: IntArray) {
    for (i in 0 until arr.size - 1) {
        var swapped = false
        for (j in 0 until arr.size - 1 - i) {
            if (arr[j] > arr[j + 1]) {
                arr[j] = arr[j + 1].also { arr[j + 1] = arr[j] }
                swapped = true
            }
        }
        if (!swapped) break // 已有序则提前结束
    }
}

2. 选择排序 (Selection Sort)

思想:将数组视为「已确定的前段 + 未排序的后段」。每轮在后段中选出最小值,与当前待放位置交换,从而把最小元依次放到前段末尾。

步骤

  1. i 从 0 到 n-2,表示当前待放位置;令 minIdx = i
  2. j 从 i+1 到 n-1:若 arr[j] < arr[minIdx]minIdx = j
  3. minIdx != i 则交换 arr[i]arr[minIdx];i 由外层循环自增。
平均时间 最坏时间 最好时间 空间 稳定性
O(n²) O(n²) O(n²) O(1) 不稳定
kotlin 复制代码
fun selectionSort(arr: IntArray) {
    for (i in 0 until arr.size - 1) {
        var minIdx = i
        for (j in i + 1 until arr.size) {
            if (arr[j] < arr[minIdx]) minIdx = j
        }
        if (minIdx != i) arr[i] = arr[minIdx].also { arr[minIdx] = arr[i] }
    }
}

3. 插入排序 (Insertion Sort)

思想:维护「前段有序、后段未排」。每次取后段第一个元素为 key,在前段中从后往前找插入位置,将大于 key 的元素后移,再把 key 放入空位。

步骤

  1. i 从 1 到 n-1,令 key = arr[i]j = i - 1
  2. j >= 0arr[j] > key 时:arr[j+1] = arr[j]j--
  3. 将 key 放入 arr[j+1](j 可能为 -1,则插入到 0);i 由外层循环自增。
平均时间 最坏时间 最好时间 空间 稳定性
O(n²) O(n²) O(n) O(1) 稳定
kotlin 复制代码
fun insertionSort(arr: IntArray) {
    for (i in 1 until arr.size) {
        val key = arr[i]
        var j = i - 1
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j]
            j--
        }
        arr[j + 1] = key
    }
}

2.2 进阶比较排序 O(n log n)

4. 希尔排序 (Shell Sort)

思想:在插入排序基础上引入「步长」:先按较大步长 gap 分组做插入排序,再逐步缩小 gap 直至 1,使数据先大致有序再细调,减少移动次数。

步骤

  1. gap = n / 2;当 gap > 0 时重复以下步骤。
  2. 对 i 从 gap 到 n-1:令 key = arr[i]j = i;当 j >= gaparr[j-gap] > key 时执行 arr[j] = arr[j-gap]j -= gap;最后 arr[j] = key
  3. 本轮结束后 gap /= 2,直到 gap 为 0。
平均时间 最坏时间 最好时间 空间 稳定性
O(n^1.3) O(n²) O(n) O(1) 不稳定
kotlin 复制代码
fun shellSort(arr: IntArray) {
    var gap = arr.size / 2
    while (gap > 0) {
        for (i in gap until arr.size) {
            val key = arr[i]
            var j = i
            while (j >= gap && arr[j - gap] > key) {
                arr[j] = arr[j - gap]
                j -= gap
            }
            arr[j] = key
        }
        gap /= 2
    }
}

5. 归并排序 (Merge Sort)

思想:分治。将当前区间一分为二,先递归使左右各自有序,再通过双指针合并两个有序段到原数组;合并时取较小者写回并移动指针,保证稳定。

步骤

  1. left >= right 直接返回。
  2. mid = left + (right - left) / 2,递归排序 [left, mid][mid+1, right]
  3. merge :左半、右半分别复制到 leftArrrightArr;双指针 i、j 从 0 开始,k 从 left 开始,每次取两段中较小者写入 arr[k] 并移动对应指针;任一指针扫完后,将另一段剩余元素依次写回。
平均时间 最坏时间 最好时间 空间 稳定性
O(n log n) O(n log n) O(n log n) O(n) 稳定
kotlin 复制代码
fun mergeSort(arr: IntArray, left: Int = 0, right: Int = arr.size - 1) {
    if (left >= right) return
    val mid = left + (right - left) / 2
    mergeSort(arr, left, mid)
    mergeSort(arr, mid + 1, right)
    merge(arr, left, mid, right)
}

private fun merge(arr: IntArray, left: Int, mid: Int, right: Int) {
    val leftArr = arr.copyOfRange(left, mid + 1)
    val rightArr = arr.copyOfRange(mid + 1, right + 1)
    var i = 0
    var j = 0
    var k = left
    while (i < leftArr.size && j < rightArr.size) {
        arr[k++] = if (leftArr[i] <= rightArr[j]) leftArr[i++] else rightArr[j++]
    }
    while (i < leftArr.size) { arr[k++] = leftArr[i++] }
    while (j < rightArr.size) { arr[k++] = rightArr[j++] }
}

6. 快速排序 (Quick Sort)

思想:分治。在区间内选一基准(此处取最后一个),用 partition 将「小于等于基准」的放左侧、「大于」的放右侧,基准放到分界位置,再对左右两段递归排序。

步骤

  1. low >= high 直接返回。
  2. partitionpivot = arr[high]i = low - 1;j 从 low 到 high-1,若 arr[j] <= pivot 则 i++ 并交换 arr[i]arr[j];最后交换 arr[i+1]arr[high],返回 pivotIdx = i + 1
  3. 递归排序 [low, pivotIdx-1][pivotIdx+1, high]
平均时间 最坏时间 最好时间 空间 稳定性
O(n log n) O(n²) O(n log n) O(log n) 不稳定
kotlin 复制代码
fun quickSort(arr: IntArray, low: Int = 0, high: Int = arr.size - 1) {
    if (low >= high) return
    val pivotIdx = partition(arr, low, high)
    quickSort(arr, low, pivotIdx - 1)
    quickSort(arr, pivotIdx + 1, high)
}

/** 将 [low, high] 按 arr[high] 划分:<= 的在左,最后基准就位并返回其下标 */
private fun partition(arr: IntArray, low: Int, high: Int): Int {
    val pivot = arr[high]
    var i = low - 1  // 小于等于 pivot 的区间末尾
    for (j in low until high) {
        if (arr[j] <= pivot) {
            i++
            arr[i] = arr[j].also { arr[j] = arr[i] }
        }
    }
    arr[i + 1] = arr[high].also { arr[high] = arr[i + 1] }
    return i + 1
}

7. 堆排序 (Heap Sort)

思想:把数组视为完全二叉树,先自底向上建大顶堆(父 ≥ 子);再反复将堆顶(当前最大)与末尾交换,并对新的堆顶做下沉(heapify),使剩余部分仍为大顶堆,从而得到升序。

步骤

  1. 建堆:i 从 n/2-1 到 0,对当前节点 i 做 heapify(下沉),得到大顶堆。
  2. i 从 n-1 到 1:交换 arr[0]arr[i],再对长度 i、根下标 0 做 heapify;i 减 1 重复。
  3. heapify(arr, n, i) :取左右子下标 2i+1、2i+2;若子存在且大于当前节点则记 largest 为子下标;若 largest != i 则交换 arr[i] 与 arr[largest] 并递归 heapify(arr, n, largest)。
平均时间 最坏时间 最好时间 空间 稳定性
O(n log n) O(n log n) O(n log n) O(1) 不稳定
kotlin 复制代码
fun heapSort(arr: IntArray) {
    val n = arr.size
    // 自底向上建大顶堆
    for (i in n / 2 - 1 downTo 0) heapify(arr, n, i)
    // 每次把堆顶(最大)换到末尾,再对剩余做下沉
    for (i in n - 1 downTo 1) {
        arr[0] = arr[i].also { arr[i] = arr[0] }
        heapify(arr, i, 0)
    }
}

private fun heapify(arr: IntArray, n: Int, i: Int) {
    var largest = i
    val left = 2 * i + 1
    val 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].also { arr[largest] = arr[i] }
        heapify(arr, n, largest)
    }
}

2.3 非比较排序

8. 计数排序 (Counting Sort)

思想:适用于整数且取值范围不大。先统计每个值出现次数,再做前缀和得到每个值在有序结果中的起始下标;从后往前遍历原数组,按该下标写入辅助数组并减 1,保证相同值相对顺序不变(稳定)。

步骤

  1. 若数组为空则返回;求 min、max,range = max - min + 1,开辟 count 长度为 range。
  2. 遍历原数组:对每个值 num 执行 count[num - min]++
  3. 前缀和:i 从 1 到 range-1,count[i] += count[i-1]
  4. 从后往前遍历原数组:令 v = arr[idx] - min,将 arr[idx] 写入 output[count[v]-1],然后 count[v]--
  5. 将 output 拷贝回 arr。
平均时间 最坏时间 最好时间 空间 稳定性
O(n + k) O(n + k) O(n + k) O(n + k) 稳定

k = 数据范围(max - min + 1)

kotlin 复制代码
fun countingSort(arr: IntArray) {
    if (arr.isEmpty()) return
    val min = arr.minOrNull()!!
    val max = arr.maxOrNull()!!
    val range = max - min + 1
    val count = IntArray(range)
    for (num in arr) count[num - min]++
    for (i in 1 until range) count[i] += count[i - 1]
    val output = IntArray(arr.size)
    for (idx in arr.indices.reversed()) {
        val v = arr[idx] - min
        output[count[v] - 1] = arr[idx]
        count[v]--
    }
    output.copyInto(arr)
}

9. 基数排序 (Radix Sort)

思想:不直接比较大小,而是按「位」排序。从个位到最高位,每次按当前位做一次稳定排序(如按该位的计数排序),这样低位有序后,再按高位排一次即得到整体有序(当前实现仅适用于非负整数)。

步骤

  1. 若数组为空则返回;求 max,exp = 1
  2. max / exp > 0 时:按当前位 (值/exp) % 10 调用 countingSortByDigit(arr, exp),然后 exp *= 10,直到最高位处理完。
  3. countingSortByDigit(arr, exp):统计该位 0 到 9 的个数到 count;做前缀和;从后往前按该位写入 output,再拷贝回 arr。
平均时间 最坏时间 最好时间 空间 稳定性
O(n × k) O(n × k) O(n × k) O(n + k) 稳定

k = 位数

kotlin 复制代码
fun radixSort(arr: IntArray) {
    if (arr.isEmpty()) return
    val max = arr.maxOrNull()!!
    var exp = 1
    while (max / exp > 0) {
        countingSortByDigit(arr, exp)
        exp *= 10
    }
}

private fun countingSortByDigit(arr: IntArray, exp: Int) {
    val n = arr.size
    val output = IntArray(n)
    val count = IntArray(10)
    for (i in 0 until n) count[(arr[i] / exp) % 10]++
    for (i in 1..9) count[i] += count[i - 1]
    for (i in n - 1 downTo 0) {
        val d = (arr[i] / exp) % 10
        output[count[d] - 1] = arr[i]
        count[d]--
    }
    output.copyInto(arr)
}

三、总结与选型

3.1 对比表

算法 平均时间 最坏时间 最好时间 空间 稳定性 典型场景
冒泡排序 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) 不稳定 不能递归、Top-K
计数排序 O(n + k) O(n + k) O(n + k) O(n + k) 稳定 整数、范围小
基数排序 O(n × k) O(n × k) O(n × k) O(n + k) 稳定 整数、多位数

3.2 使用示例

kotlin 复制代码
fun main() {
    val arr = intArrayOf(64, 34, 25, 12, 22, 11, 90)
    quickSort(arr)  // 可替换为任意上述排序函数
    println(arr.contentToString())  // [11, 12, 22, 25, 34, 64, 90]
}

四、工程实践与延伸

4.1 Kotlin 标准库

实际项目优先使用标准库,无需手写排序:

kotlin 复制代码
arr.sort()                              // 升序,原地
arr.sortDescending()                    // 降序,原地
val sorted = arr.sorted()                // 升序,返回新 List
val sortedDesc = arr.sortedDescending()  // 降序,返回新 List
arr.sortWith(compareBy { kotlin.math.abs(it) })           // 自定义比较
arr.sortWith(compareBy<DataClass>({ it.a }, { it.b }))    // 多关键字

Kotlin/JVM 的 sort() 多为 Dual-Pivot QuickSort,工程上足够好用。

4.2 边界与注意点

情况 说明
空数组 计数、基数中已做 isEmpty() 判断;其余算法循环不执行即安全。
单元素 各算法均可处理,归并/快排的 left >= right 已覆盖。
基数排序与负数 当前实现针对非负整数;若有负数需按符号分桶或整体偏移。
计数排序范围大 max - min 很大时需 O(n + k) 空间且常数大,不如快排/归并。

4.3 快排优化

  • 随机基准val pivot = arr[Random.nextInt(high - low + 1) + low](需 import kotlin.random.Random),减轻有序/近似有序时的 O(n²) 退化。
  • 三路划分:将等于基准的放中间,只对严格小于和严格大于的两段递归,重复多时更高效。

4.4 桶排序简介

将数据分到有限个桶中,每桶内再排序(如插入排序),最后按桶顺序输出。适合均匀分布的浮点数等。

  • 时间:平均 O(n + k),k 为桶数。
  • 空间:O(n)。
  • 稳定性:取决于桶内排序是否稳定。

4.5 正确性验证

  • 与标准库对比:先 val expected = arr.copyOf().also { it.sort() },再对副本 执行自实现排序,用 副本.contentEquals(expected) 比较。
  • 测试用例:随机、全相同、已排序、逆序、含重复元素等。
  • 可配合 JUnit 等做单元测试。

4.6 更多场景与算法

更多排序「场景」(问题类型):

场景 说明 常用思路
全量排序 整体有序 快排、归并、sort()
Top-K / 部分排序 前 K 大/小 堆、快速选择
多关键字排序 先 A 后 B,同 A 保持顺序 稳定排序或 sortWith(compareBy(...))
自定义规则 按绝对值、字段等 任意算法 + Comparator
链表排序 只能顺序访问 归并排序
外排序 数据在磁盘、内存不足 分块排序 + 多路归并
近似有序 大部分已有序 插入排序、Tim Sort

工程中常见其他算法

名称 说明
Tim Sort 归并 + 插入,稳定,Python/Java 默认对象排序。
Dual-Pivot QuickSort 双基准快排,Kotlin/JVM sort() 常用。
IntroSort 快排 + 堆 + 插入混合,C++ std::sort,避免快排最坏。
桶排序 见 4.4。

小结:九大算法是经典必学;工程中优先用标准库。全量排序、Top-K、多关键字、链表、外排序等均为常见场景,按需求选对应算法或 API 即可。

相关推荐
留声2 小时前
Vue3 动态路由实战:基于权限的动态路由管理与常见坑点解析
前端
许留山2 小时前
前端 PDF 导出:从文件流下载到自动分页
前端·react.js
蓝鲸有腿2 小时前
项目部署后->这样通知用户刷新
前端
少卿2 小时前
OpenClaw github 技能:让 GitHub 操作像聊天一样简单
前端
Ekehlaft2 小时前
同题画图大考,AiPy 章鱼适配性拉满,OpenClaw 龙虾全程 “哑火”
前端
掘金酱2 小时前
小册上新|玩🦐吗?ai 编程全栈指南了解一下?
前端·人工智能·ai编程
zd2005722 小时前
用摩斯密码「听」时间:一款安卓报时应用的诞生
android
小小小小宇2 小时前
富文本编辑器知识体系(一)
前端