一、基础概念
1.1 复杂度
- 时间复杂度:衡量随数据规模 n 增长,操作次数的量级。常用平均、最坏、最好三种。
- 空间复杂度:除输入外,额外占用内存的量级(如递归栈、辅助数组)。
本文用 O 表示渐进上界,如 O(n²)、O(n log n)、O(n + k) 等。下文中各算法的复杂度表统一为:平均时间、最坏时间、最好时间、空间、稳定性。
1.2 稳定性
稳定排序:相等元素在排序前后相对顺序不变。
- 为何重要:多关键字排序时(先按分数再按学号),稳定排序能保证同分时学号顺序不变。
- 典型场景:先按日期再按金额,希望同一天内金额顺序保持。
本文中的稳定算法:冒泡、插入、归并、计数、基数。
二、九大排序算法
以下各算法均对 IntArray 原地排序(计数与基数会写回原数组),结构为:思想 → 步骤 → 复杂度 → 代码。
2.1 简单比较排序 O(n²)
1. 冒泡排序 (Bubble Sort)
思想:多轮扫描数组,每轮只比较相邻两项,若逆序则交换,使较大值像「气泡」一样逐步移到右侧;若某轮未发生交换则提前结束。
步骤:
- i 从 0 到 n-2(共 n-1 轮),每轮设
swapped = false。 - j 从 0 到 n-2-i:若
arr[j] > arr[j+1]则交换并设swapped = true。 - 若未发生交换则
break;否则进入下一轮。
| 平均时间 | 最坏时间 | 最好时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| O(n²) | O(n²) | O(n) | O(1) | 稳定 |
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)
思想:将数组视为「已确定的前段 + 未排序的后段」。每轮在后段中选出最小值,与当前待放位置交换,从而把最小元依次放到前段末尾。
步骤:
- i 从 0 到 n-2,表示当前待放位置;令
minIdx = i。 - j 从 i+1 到 n-1:若
arr[j] < arr[minIdx]则minIdx = j。 - 若
minIdx != i则交换arr[i]与arr[minIdx];i 由外层循环自增。
| 平均时间 | 最坏时间 | 最好时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
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 放入空位。
步骤:
- i 从 1 到 n-1,令
key = arr[i],j = i - 1。 - 当
j >= 0且arr[j] > key时:arr[j+1] = arr[j],j--。 - 将 key 放入
arr[j+1](j 可能为 -1,则插入到 0);i 由外层循环自增。
| 平均时间 | 最坏时间 | 最好时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| O(n²) | O(n²) | O(n) | O(1) | 稳定 |
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,使数据先大致有序再细调,减少移动次数。
步骤:
- 令
gap = n / 2;当gap > 0时重复以下步骤。 - 对 i 从 gap 到 n-1:令
key = arr[i],j = i;当j >= gap且arr[j-gap] > key时执行arr[j] = arr[j-gap]、j -= gap;最后arr[j] = key。 - 本轮结束后
gap /= 2,直到 gap 为 0。
| 平均时间 | 最坏时间 | 最好时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| O(n^1.3) | O(n²) | O(n) | O(1) | 不稳定 |
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)
思想:分治。将当前区间一分为二,先递归使左右各自有序,再通过双指针合并两个有序段到原数组;合并时取较小者写回并移动指针,保证稳定。
步骤:
- 若
left >= right直接返回。 mid = left + (right - left) / 2,递归排序[left, mid]与[mid+1, right]。- merge:左半、右半分别复制到
leftArr、rightArr;双指针 i、j 从 0 开始,k 从 left 开始,每次取两段中较小者写入arr[k]并移动对应指针;任一指针扫完后,将另一段剩余元素依次写回。
| 平均时间 | 最坏时间 | 最好时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
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 将「小于等于基准」的放左侧、「大于」的放右侧,基准放到分界位置,再对左右两段递归排序。
步骤:
- 若
low >= high直接返回。 - partition:
pivot = 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。 - 递归排序
[low, pivotIdx-1]与[pivotIdx+1, high]。
| 平均时间 | 最坏时间 | 最好时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| O(n log n) | O(n²) | O(n log n) | O(log n) | 不稳定 |
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),使剩余部分仍为大顶堆,从而得到升序。
步骤:
- 建堆:i 从 n/2-1 到 0,对当前节点 i 做 heapify(下沉),得到大顶堆。
- i 从 n-1 到 1:交换
arr[0]与arr[i],再对长度 i、根下标 0 做 heapify;i 减 1 重复。 - 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) | 不稳定 |
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,保证相同值相对顺序不变(稳定)。
步骤:
- 若数组为空则返回;求 min、max,
range = max - min + 1,开辟count长度为 range。 - 遍历原数组:对每个值 num 执行
count[num - min]++。 - 前缀和:i 从 1 到 range-1,
count[i] += count[i-1]。 - 从后往前遍历原数组:令
v = arr[idx] - min,将arr[idx]写入output[count[v]-1],然后count[v]--。 - 将 output 拷贝回 arr。
| 平均时间 | 最坏时间 | 最好时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| O(n + k) | O(n + k) | O(n + k) | O(n + k) | 稳定 |
k = 数据范围(max - min + 1)
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)
思想:不直接比较大小,而是按「位」排序。从个位到最高位,每次按当前位做一次稳定排序(如按该位的计数排序),这样低位有序后,再按高位排一次即得到整体有序(当前实现仅适用于非负整数)。
步骤:
- 若数组为空则返回;求 max,
exp = 1。 - 当
max / exp > 0时:按当前位(值/exp) % 10调用 countingSortByDigit(arr, exp),然后exp *= 10,直到最高位处理完。 - countingSortByDigit(arr, exp):统计该位 0 到 9 的个数到 count;做前缀和;从后往前按该位写入 output,再拷贝回 arr。
| 平均时间 | 最坏时间 | 最好时间 | 空间 | 稳定性 |
|---|---|---|---|---|
| O(n × k) | O(n × k) | O(n × k) | O(n + k) | 稳定 |
k = 位数
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 使用示例
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 标准库
实际项目优先使用标准库,无需手写排序:
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 即可。