目录
[三、插入排序(Insertion Sort)](#三、插入排序(Insertion Sort))
[1 插入排序的核心思想](#1 插入排序的核心思想)
[2 插入排序过程](#2 插入排序过程)
[3 插入排序代码](#3 插入排序代码)
[六、希尔排序(Shell Sort)](#六、希尔排序(Shell Sort))
[1 为什么需要希尔排序](#1 为什么需要希尔排序)
[2 希尔排序的核心思想](#2 希尔排序的核心思想)
[3 gap(间隔)](#3 gap(间隔))
[九、为什么 gap 要越来越小](#九、为什么 gap 要越来越小)
[1 为什么 i 从 1 开始](#1 为什么 i 从 1 开始)
[2 为什么要保存 tmp](#2 为什么要保存 tmp)
[3 为什么 j -= gap](#3 为什么 j -= gap)
[4 为什么最后必须 gap=1](#4 为什么最后必须 gap=1)
Author 👉**:TomGo**
排序专题是数据结构中非常基础但非常重要的一部分。
一开始学的时候,总觉得排序就是"会写代码"。
后来慢慢发现:
会写 ≠ 理解真正重要的是理解:
算法的思想
为什么这样写
复杂度从哪里来
容易踩的坑
这篇博客主要整理:
插入排序
希尔排序
以及我学习过程中踩过的坑和思考。
一、排序问题的本质
排序其实就是:
重新排列数据,使其满足某种顺序关系
例如:
原数组
5 3 8 1 2
排序后
1 2 3 5 8
排序算法通常关注三个重要指标:
| 指标 | 含义 |
|---|---|
| 时间复杂度 | 排序需要多少计算量 |
| 空间复杂度 | 是否需要额外空间 |
| 稳定性 | 相同元素的相对顺序是否改变 |
二、稳定排序
假设数组中有两个值相同的元素:
(2,A) (2,B)
如果排序后仍然是:
(2,A) (2,B)
说明:
排序是稳定的
如果顺序变成:
(2,B) (2,A)
说明:
排序不稳定
三、插入排序(Insertion Sort)
1 插入排序的核心思想
插入排序其实非常符合人的直觉。
想象你在整理扑克牌:
-
左边的牌已经排好序
-
右手拿一张新牌
-
把它插入到正确的位置
数组中的逻辑也是一样:
左边:已排序
右边:未排序
2 插入排序过程
数组:
5 3 8 1 2
初始:
[5] | 3 8 1 2
默认第一元素有序。
插入3
5 | 3 8 1 2
比较:
3 < 5
移动:
3 5 | 8 1 2
插入8
3 5 | 8 1 2
比较:
8 > 5
不需要移动。
插入1
3 5 8 | 1 2
向左比较:
1 < 8
1 < 5
1 < 3
移动:
1 3 5 8 | 2
3 插入排序代码
cpp
void InsertSort(int* a, int n)
{
for(int i = 1; i < n; i++)
{
int tmp = a[i];
int j = i - 1;
while(j >= 0 && a[j] > tmp)
{
a[j + 1] = a[j];
j--;
}
a[j + 1] = tmp;
}
}
核心动作:
移动元素
插入元素
四、复杂度分析
最坏情况
数组完全逆序:
9 8 7 6 5
移动次数:
cpp
1 + 2 + 3 + ... + n
等差数列
时间复杂度:
O(n²)
最好情况
数组已经有序:
1 2 3 4 5
每次只比较一次:
O(n)
五、插入排序的重要特点
插入排序有一个非常重要的特性:
对接近有序的数组非常快
这一点非常关键,因为很多高级排序算法都会利用这一特性。
六、希尔排序(Shell Sort)
1 为什么需要希尔排序
插入排序最大的问题是:
元素移动距离太远
例如:
9 1 2 3 4 5 6
插入排序:
9需要移动6次
这就是效率低的原因。
2 希尔排序的核心思想
希尔排序的思路是:
先让远距离元素提前移动
也可以理解为:
先粗排
再细排
3 gap(间隔)
希尔排序引入一个变量:
gap
例如:
gap = 3
数组:
9 8 3 7 5 6 4 1
分组:
组1:0 3 6
组2:1 4 7
组3:2 5
每一组做:
插入排序
七、希尔排序代码
cpp
void ShellSort(int* a, int n)
{
int gap = n;
while(gap > 1)
{
gap /= 2;
for(int i = gap; i < n; i++)
{
int tmp = a[i];
int j = i - gap;
while(j >= 0 && a[j] > tmp)
{
a[j + gap] = a[j];
j -= gap;
}
a[j + gap] = tmp;
}
}
}
八、为什么代码没有"分组循环"
很多人第一次看希尔排序会疑惑:
代码没有显式分组
怎么处理每一组?
关键在这句:
for (i = gap; i < n; i++)
例如:
gap = 3
数组:
0 1 2 3 4 5 6 7
i 的变化:
3 4 5 6 7
实际上对应:
i=3 → 处理组 0 3 6
i=4 → 处理组 1 4 7
i=5 → 处理组 2 5
所以:
所有组是交替处理的
希尔排序并不会单独处理每一组,
而是通过一个线性循环依次处理所有元素,
每个元素都会在自己的 gap 组内进行插入调整,
因此所有组会被"交替地"完成排序。
九、为什么 gap 要越来越小
希尔排序的核心策略:
大步调整
小步微调
例如:
gap = 4
gap = 2
gap = 1
当:
gap = 1
其实就是:
一次普通插入排序
但此时数组:
已经接近有序
所以效率非常高。
十、复杂度
希尔排序复杂度比较特殊。
取决于:
gap序列
一般认为:
O(n^1.3 ~ n^1.5)
明显优于:
O(n²)
十一、稳定性
插入排序:
稳定
因为只移动:
大于 tmp 的元素
希尔排序:
不稳定
因为:
跨 gap 移动
可能改变相同元素顺序。
十二、学习过程中踩过的坑
1 为什么 i 从 1 开始
因为:
a[0] 默认有序
2 为什么要保存 tmp
因为移动时:
a[i] 会被覆盖
3 为什么 j -= gap
因为需要在:
同一组
里移动。
4 为什么最后必须 gap=1
否则只能保证:
局部有序
而不能保证:
整体有序
十三、面试常问问题
插入排序什么时候最快?
数组接近有序
复杂度:
O(n)
希尔排序稳定吗?
不稳定
原因:
跨gap移动
希尔排序为什么比插入排序快?
核心原因:
减少元素移动距离
十四、我的理解总结
学习排序之后,我最大的感受是:
算法不是记代码
而是理解结构
例如:
插入排序的本质是:
维护一个有序区
而希尔排序只是:
让元素提前接近正确位置
当理解这一点之后,代码就变得非常清晰。
结语
这是我排序专题的第一篇总结。
后续我还会整理:
-
冒泡排序
-
选择排序
-
快速排序
-
归并排序
-
堆排序
希望未来复习时,一眼就能想起这些算法的本质。
------ TomGo