插入排序(Insertion Sort)
插入排序(Insertion Sort)
一、核心思想
插入排序的灵感来自日常生活中的"整理扑克牌":当你拿到一张新牌时,会把它插到手里已有牌的正确位置,保持手里的牌始终有序。
在算法层面,我们把数组分为两部分——左侧已排序区和右侧未排序区。每一轮从右侧取出一个元素,向左逐个比较,找到合适的位置插入,使得左侧有序区不断扩大。
初始: [5, 3, 1, 4, 2]
第1轮: 取3 → [3, 5 | 1, 4, 2] ← 3比5小,插入5前面
第2轮: 取1 → [1, 3, 5 | 4, 2] ← 1比3和5都小,插入最前
第3轮: 取4 → [1, 3, 4, 5 | 2] ← 4比5小比3大,插入3和5之间
第4轮: 取2 → [1, 2, 3, 4, 5] ← 2比3小比1大,插入1和3之间
二、算法步骤
- 从第2个元素(索引1)开始,将其视为"待插入元素 key"
- 从 key 的前一个位置向左扫描,将所有大于 key 的元素右移一位
- 把 key 放到腾出的空位上
- 重复步骤 1~3,直到遍历完整个数组
三、Go 语言实现
基础版
package main
import "fmt"
// 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 // 插入到正确位置
}
}
func main() {
data := []int{5, 3, 1, 4, 2, 8, 6, 7}
fmt.Println("排序前:", data)
InsertionSort(data)
fmt.Println("排序后:", data)
}
二分查找优化版(Binary Insertion Sort)
在寻找插入位置时,左侧已排序区可以用二分查找加速,将比较次数从 O(n) 降到 O(log n)。不过元素移动仍然是 O(n),所以整体时间复杂度不变,只是减少了比较操作。
package main
import "fmt"
// BinaryInsertionSort 使用二分查找确定插入位置的插入排序
func BinaryInsertionSort(arr []int) {
n := len(arr)
for i := 1; i < n; i++ {
key := arr[i]
// 二分查找插入位置
lo, hi := 0, i-1
for lo <= hi {
mid := lo + (hi-lo)/2
if arr[mid] > key {
hi = mid - 1
} else {
lo = mid + 1
}
}
// lo 就是 key 应插入的位置
// 将 [lo, i-1] 区间的元素右移一位
for j := i - 1; j >= lo; j-- {
arr[j+1] = arr[j]
}
arr[lo] = key
}
}
func main() {
data := []int{9, 7, 5, 3, 1, 2, 4, 6, 8}
fmt.Println("排序前:", data)
BinaryInsertionSort(data)
fmt.Println("排序后:", data)
}
四、复杂度分析
| 情况 | 时间复杂度 | 说明 |
|---|---|---|
| 最好 | O(n) | 数组已经有序,内层循环不执行任何移动 |
| 最坏 | O(n²) | 数组完全逆序,每个元素都要移动到最前面 |
| 平均 | O(n²) | 随机数据,约一半元素需要移动 |
- 空间复杂度:O(1),原地排序,仅用了一个 key 变量
- 稳定性:✅ 稳定排序——因为内层循环只在
arr[j] > key时才移动,相等元素不会跨越彼此
五、核心特性与适用场景
优势
- 近乎有序的数据极快:当数据基本有序时,插入排序接近 O(n),这是它的杀手级优势
- 小规模数据优选:元素少于20~50个时,插入排序往往比快排和归并排序更快(没有递归开销)
- 在线算法:可以边接收数据边排序,不需要等全部数据到手
不足
- 大规模随机数据效率低,O(n²) 增长太快
- 每次插入都要移动多个元素,数据量大时移动成本高
实际应用
- Go 标准库
sort.Sort在小区间(≤12元素)时会切换为插入排序作为快排的"收尾" - 很多混合排序算法(如 TimSort)都把插入排序作为处理小段有序区的核心组件
六、与其他排序对比
| 算法 | 最好 | 平均 | 最坏 | 空间 | 稳定性 | 特点 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 交换式,常数级优化空间小 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 | 交换次数少,但无法利用有序性 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 近乎有序时极快,小数据优选 |
关键区别:冒泡和选择都是"交换排序",通过两两交换来移动元素;插入排序是"移动排序",通过整体右移腾出空位再插入,移动操作在内存中是连续的,通常比随机位置交换更高效。
七、今日练习
package main
import "fmt"
// 练习1: 实现基础插入排序并验证稳定性
func InsertionSort(arr []int) {
n := len(arr)
for i := 1; i < n; i++ {
key := arr[i]
j := i - 1
for j >= 0 && arr[j] > key { // 严格大于,保证稳定性
arr[j+1] = arr[j]
j--
}
arr[j+1] = key
}
}
// 练习2: 实现降序插入排序(修改比较方向即可)
func InsertionSortDesc(arr []int) {
n := len(arr)
for i := 1; i < n; i++ {
key := arr[i]
j := i - 1
for j >= 0 && arr[j] < key { // 改为小于
arr[j+1] = arr[j]
j--
}
arr[j+1] = key
}
}
// 练习3: 对字符串切片按字典序排序
func InsertionSortString(arr []string) {
n := len(arr)
for i := 1; i < n; i++ {
key := arr[i]
j := i - 1
for j >= 0 && arr[j] > key {
arr[j+1] = arr[j]
j--
}
arr[j+1] = key
}
}
func main() {
// 验证基础版
data := []int{64, 25, 12, 22, 11}
fmt.Println("升序排序前:", data)
InsertionSort(data)
fmt.Println("升序排序后:", data)
// 验证降序版
data2 := []int{64, 25, 12, 22, 11}
fmt.Println("降序排序前:", data2)
InsertionSortDesc(data2)
fmt.Println("降序排序后:", data2)
// 验证字符串版
words := []string{"banana", "apple", "cherry", "date"}
fmt.Println("字典序排序前:", words)
InsertionSortString(words)
fmt.Println("字典序排序后:", words)
// 验证稳定性:使用带标记的元素
type Item struct {
val int
tag string
}
items := []Item{
{3, "a"}, {1, "x"}, {3, "b"}, {2, "y"}, {3, "c"},
}
fmt.Println("\n稳定性验证:")
fmt.Println("排序前:", items)
// 按val排序,观察相同val的tag顺序是否保持 a→b→c
for i := 1; i < len(items); i++ {
key := items[i]
j := i - 1
for j >= 0 && items[j].val > key.val {
items[j+1] = items[j]
j--
}
items[j+1] = key
}
fmt.Println("排序后:", items)
// 期望: val=3的三个元素tag顺序仍为 a→b→c
}
运行结果:
升序排序前: [64 25 12 22 11]
升序排序后: [11 12 22 25 64]
降序排序前: [64 25 12 22 11]
降序排序后: [64 25 22 12 11]
字典序排序前: [banana apple cherry date]
字典序排序后: [apple banana cherry date]
稳定性验证:
排序前: [{3 a} {1 x} {3 b} {2 y} {3 c}]
排序后: [{1 x} {2 y} {3 a} {3 b} {3 c}]
val=3 的三个元素 tag 保持了 a→b→c 的原始顺序,验证了插入排序的稳定性。

浙公网安备 33010602011771号