排序算法技术文档
目录
- 一、插入排序(Insertion Sort)
- 二、希尔排序(Shell Sort)
- 三、归并排序(Merge Sort)
- 四、快速排序(Quick Sort)
- 五、堆排序(Heap Sort)
- 六、排序算法对比总结
一、插入排序(Insertion Sort)
1. 基本思想
- 将数组划分为两个逻辑区域:
- 已排序区
- 未排序区
- 默认第一个元素在已排序区
- 每次从未排序区取出一个元素
- 从已排序区从后向前比较
- 找到合适位置插入
2. 核心特征
- 类似“打扑克牌插牌”
- 数据基本有序时效率极高
- 属于稳定排序
- 原地排序(不需要额外数组)
3. 时间 & 空间复杂度
| 情况 | 复杂度 |
|---|---|
| 最好情况(已排序) | O(n) |
| 平均情况 | O(n²) |
| 最坏情况(逆序) | O(n²) |
| 空间复杂度 | O(1) |
| 稳定性 | 稳定 |
4. 适用场景
- 数据量较小
- 数据基本有序
- 作为其他排序(如希尔排序)的基础
5. 代码实现
class 插入排序
{
//时间复杂度:O(n²)
//空间复杂度:O(1) 所有操作都是在原数组里完成的,没有开辟新的数组或额外存储。
//潜在隐患:在数据量较大时,算法会非常慢,可能导致程序卡顿甚至无法在合理时间内完成。如果在安全关键场景(如实时系统、金融交易系统)使用,会造成延迟风险
//应用场景:数据量较小 数据基本有序
class Program
{
static void Main(string[] args)
{
#region 知识点一 插入排序的基本原理
//871542639
//两个区域
// 排序区
// 未排序区
// 用一个索引值做分水岭
// 未排序区元素
// 与排序区元素比较
//插入到合适位置
// 直到未排序区清空
#endregion
#region 知识点二 代码实现
//实现升序 把 大的 放在最后面
int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
//int[] arr = new int[] { 12, 5, 33, 2, 6, 9, 5, 4, 6, 6, 7, 6 };
//前提规则
//排序开始前
//首先认为第一个元素在排序区中
//其它所有元素在未排序区中
//排序开始后
//每次将未排序区第一个元素取出用于和
//排序区中元素比较(从后往前)
//满足条件(较大或者较小)
//则排序区中元素往后移动一个位置。
//注意
//所有数字都在一个数组中
//所谓的两个区域是一个分水岭索引
//第一步
//能取出未排序区的所有元素进行比较
//i=1的原因:默认第一个元素就在排序区
for (int i = 1; i < arr.Length; i++)
{
//第二步
//每一轮
//1.取出排序区的最后一个元素索引
int sortIndex = i - 1;
//2.取出未排序区的第一个元素
int noSortNum = arr[i];
//第三步
//在未排序区进行比较
//移动位置
//确定插入索引
while (sortIndex >= 0 && arr[sortIndex] > noSortNum)
{
//只要进了这个while循环 证明满足条件
//排序区中的元素 就应该往后退一格
arr[sortIndex + 1] = arr[sortIndex];
//移动到排序区的前一个位置 准备继续比较
sortIndex--;
}
//最终插入数字
//循环中只是在确定位置 和找最终的插入位置
//最终插入对应位置 应该循环结束后
arr[sortIndex + 1] = noSortNum;
}
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
#endregion
#region 知识点三 总结
//为什么有两层循环
//第一层循环:一次取出未排序区的元素进行排序
//第二层循环:找到想要插入的位置
//为什么第一层循环从1开始遍历
//插入排序的关键是分两个区域
//已派序区和未排序区
//默认第一个元素在已派序区
//为什么使用while循环
//满足条件才比较
//否则证明插入位置已确定
//不需要继续循环
//为什么可以直接往后移位置
//每轮未排序数已记录
//最后一个位置不怕丢
//为什么确定位置后 是放在sortIndex + 1的位置
//当循环停止时 插入位置应该是停止循环的索引加1处
//基本原理
//两个区域
//用索引值来区分
//未排序区与排序区
//元素不停比较
//找到合适位置
//插入当前元素
//套路写法
//两层循环
//一层获取未排序区元素
//一层找到合适插入位置
//注意事项
//默认开头已排序
//第二层循环外插入
#endregion
}
}
}
二、希尔排序(Shell Sort)
1. 基本思想
希尔排序是 插入排序的升级版
- 引入 步长(gap)
- 将原数组按步长拆分为多个子序列
- 对每个子序列执行插入排序
- 步长逐渐缩小,最终为 1
2. 排序流程
- 初始步长:
gap = n / 2 - 对 gap 个子序列分别做插入排序
- gap /= 2
- 重复直到 gap = 1
3. 时间 & 空间复杂度
时间复杂度依赖步长序列
| 项目 | 说明 |
|---|---|
| 最坏情况 | O(n²) |
| 平均情况 | 介于 O(n) ~ O(n²) |
| 实际表现 | 明显优于插入排序 |
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定 |
4. 适用场景
- 中等规模数据
- 内存受限环境
- 插入排序性能不足时的改进方案
5. 代码实现
namespace 希尔排序
{
//时间复杂度:取决于步长序列 O(n²) 在实际应用中,通常接近 O(n log² n),比插入排序快很多
//空间复杂度:O(1) - 原地排序,所有操作都在原数组里完成,没有额外存储
//潜在隐患:步长序列选择不当时,性能提升有限,甚至退化为插入排序 实现时要注意索引边界,避免数组越界
//应用场景:中等规模数据排序(比插入排序更适合大数据)内存受限环境(空间复杂度 O(1)) 教学和算法研究中常用,作为插入排序的升级版
class Program
{
static void Main(string[] args)
{
Console.WriteLine("希尔排序");
#region 知识点一 希尔排序的基本原理
//希尔排底是
//插入排序的升级版
//必须先掌握插入排序
//希尔排序的原理
//将整个待排序序列
//分割成为若干子序列
//分别进行插入排序
//总而言之
//希尔排序对插入排序的升级主要就是加入了一个步长的概念
//通过步长每次可以把原序列分为多个子序列
//对子序列进行插入排序
//在极限情况下可以有效降低普通插入排序的时间复杂度
//提升算法效率
#endregion
#region 知识点二 代码实现
int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
//学习希尔排序的前提条件
//先掌握插入排序
//第一步:实现插入排序
//第二步:确定步长
//基本规则:每次步长变化都是/2
//一开始步长 就是数组的长度/2
//之后每一次 都是在上一次的步长基础上/2
//结束条件是 步长 <=0
for (int step = arr.Length / 2; step > 0; step /= 2)
{
//第三步:执行插入排序
//i=1代码 相当于 代表取出来的排序区的第一个元素
//for(int i=1;i<arr.Length;i++)
//i=step 相当于 代表取出来的排序区的第一个元素
for (int i = step; i < arr.Length; i++)
{
//得出未排序区的元素
int noSortNum = arr[i];
//得出排序区中最后一个元素索引
//int sortIndex = i-1;
//i-step 代表和子序列中 已排序区元素一一比较
int sortIndex = i - step;
//进入条件
//首先排序区中还有可以比较的 >= 0
//排序区中元素 满足交换条件 升序就是排序区中元素要大于未排序区中元素
while (sortIndex >= 0 && arr[sortIndex] > noSortNum)
{
arr[sortIndex + step] = arr[sortIndex];
sortIndex -= step;
}
//找到位置过后 真正的插入 值
arr[sortIndex + step] = noSortNum;
}
}
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
#endregion
#region 知识点三 总结
//基本原理
//设置步长
//步长不停缩小
//到1排序后结束
//具体排序方式
//插入排序原理
//套路写法
//三层循环
//-层获取步长
//一层获取未排序区元素
//一层找到合适位置插入
//注意事项
//步长确定后
//会将所有子序列进行插入排序
#endregion
}
}
}
三、归并排序(Merge Sort)
1. 基本思想
分治思想 + 递归
- 不断将数组二分
- 直到子数组长度为 1
- 逐层向上合并
- 合并过程保证有序
2. 核心流程
- 递归拆分数组
- 左右子数组分别排序
- 合并两个有序数组
3. 时间 & 空间复杂度
| 项目 | 复杂度 |
|---|---|
| 最好 / 平均 / 最坏 | O(n log n) |
| 空间复杂度 | O(n) |
| 稳定性 | 稳定 |
4. 特点与应用
-
性能稳定
-
适合处理大数据量
-
常用于:
- 外部排序
- 链表排序
-
缺点:额外内存消耗
5. 代码实现
namespace 归并排序
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("归并排序");
#region 归并排序基本原理
//归并=递归 +合并
//数组分左右
//左右元素相比较
//满足条件放入新数组
//-侧用完放对面
//递归不停分
//分完再排序
//排序结束往上走
//边走边合并
//走到头顶出结果
//归并排序分成两部分
//1.基本排序规则
//2.递归平分数组
//递归平分数组:
//不停进行分割
// 长度小于停止
//开始比较
// 一层一层向上比
//基本排序规则:
//左右元素进行比较
//依次放入新数组中
//一侧没有了另一侧直接放入新数组
#endregion
int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
arr = Merge(arr);
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
}
#region 代码实现
//第一步:
//基本排序规则
//左右元素相比较
//满足条件放进去
//一侧用完直接放
public static int[] Sort(int[] left, int[] right)
{
//先准备一个新数组
int[] array = new int[left.Length + right.Length];
int leftIndex = 0;//左数组索引
int rightIndex = 0;//右数组索引
//最终目的是要填满这个新数组
//不会出现两侧都放完了 还会进入循环 因为新数组长度是根据左右两个数组计算来的
for (int i = 0; i < array.Length; i++)
{
//左侧放完了 放对面
if (leftIndex >= left.Length)
{
array[i] = right[rightIndex];
//已经放入了右侧元素进入新数组
//所以 标识应该指向下一个
rightIndex++;
}
//右侧放完了 放对面
else if (rightIndex >= right.Length)
{
array[i] = left[leftIndex];
//已经放入了左侧元素进入新数组
//所以 标识应该指向下一个
leftIndex++;
}
else if (left[leftIndex] < right[rightIndex])
{
array[i] = left[leftIndex];
//已经放入了左侧元素进入新数组
//所以 标识应该指向下一个
leftIndex++;
}
else
{
array[i] = right[rightIndex];
//已经放入了右侧元素进入新数组
//所以 标识应该指向下一个
rightIndex++;
}
}
//得到了新数组 直接返回出去
return array;
}
//第二步:
//递归评分数组
//结束条件为长度小于2
public static int[] Merge(int[] array)
{
//递归结束条件
if (array.Length < 2)
{
return array;
}
//1.数组分两段
int mid = array.Length / 2;
//2.初始化左右数组
//左数组
int[] left = new int[mid];
//右数组
int[] right = new int[array.Length - mid];
//左右初始化内容
for (int i = 0; i < array.Length; i++)
{
if (i < mid)
{
left[i] = array[i];
}
else
{
right[i - mid] = array[i];
}
}
//递归再分再排序 先调用Merge
return Sort(Merge(left), Merge(right));
}
#endregion
}
}
四、快速排序(Quick Sort)
1. 基本思想
- 选择一个 基准值(pivot)
- 小于基准的放左边
- 大于基准的放右边
- 递归处理左右区间
2. 关键步骤
- 选择基准
- 左右指针向中间移动
- 交换不符合条件的元素
- 基准归位
- 递归左右区间
3. 时间 & 空间复杂度
| 情况 | 复杂度 |
|---|---|
| 平均情况 | O(n log n) |
| 最坏情况 | O(n²) |
| 空间复杂度 | O(log n)(递归栈) |
| 稳定性 | 不稳定 |
4. 工程实践说明
-
实际项目中常用
-
通常会:
- 随机选基准
- 三数取中
-
避免退化为 O(n²)
5. 代码实现
namespace 快速排序
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("快速排序");
int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
QuickSort(arr, 0, arr.Length - 1);
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
}
#region 知识点一 快速排序基本原理
//选取基准
//产生左右标识
//左右比基准
//满足则换位
//排完一次
//基准定位
//左右递归
//直到有序
#endregion
#region 知识点二 代码实现
//第一步
//申明用于快速排序的函数
public static void QuickSort(int[] array, int left, int right)
{
//第七步:
//递归函数结束条件
if (left >= right)
{
return;
}
//第二步:
int tempLeft, tempRight, temp;
//记录基准值
temp = array[left];
//左游标
tempLeft = left;
//右游标
tempRight = right;
//第三步:
//核心交换逻辑
//左右游标会不同变化 要不相同时才能继续变化
while (tempLeft != tempRight)
{
//第四步:比较位置交换
//首先从右边开始 比较 看值有没有资格放到标识的右侧
while (tempLeft < tempRight && array[tempRight] > temp)
{
tempRight--;
}
//移动结束证明可以换位置
array[tempLeft] = array[tempRight];
//上面是移动右侧游标
//接着移动完右侧游标 就要来移动左侧游标
while (tempLeft < tempRight && array[tempLeft] < temp)
{
tempLeft++;
}
//移动结束证明可以换位置
array[tempRight] = array[tempLeft];
}
//第五步:放置基准值
//跳出循环后 把基准值放在中间位置
//此时tempRight和tempLeft一定是相等的
array[tempLeft] = temp;
//第六步:
//递归继续
QuickSort(array, left, tempLeft - 1);
QuickSort(array, tempRight + 1, right);
}
#endregion
}
}
五、堆排序(Heap Sort)
1. 基本思想
- 利用 完全二叉树
- 构建 大顶堆
- 堆顶元素与末尾交换
- 调整堆结构
- 重复直到有序
2. 核心规则
- 父节点
i - 左子节点
2i + 1 - 右子节点
2i + 2 - 最大非叶子节点:
n / 2 - 1
3. 时间 & 空间复杂度
| 项目 | 复杂度 |
|---|---|
| 最好 / 平均 / 最坏 | O(n log n) |
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定 |
4. 特点
- 原地排序
- 性能稳定
- 不依赖递归
- 常用于对空间要求严格的系统
5. 代码实现
using System;
using System.Reflection;
namespace 堆排序
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("堆排序");
int[] arr = new int[] { 8, 7, 1, 5, 4, 2, 6, 3, 9 };
HeapSort(arr);
for (int i = 0; i < arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
}
#region 知识点一堆排序基本原理
//构建二叉树
//大堆顶调整
//堆顶往后方
//不停变堆顶
//关键规则
//最大非叶子节点:
//数组长度/2-1
//父节点和叶子节点:
//父节点为i
//左节点21+1
//右节点2i+2
#endregion
#region 知识点二 代码实现
//第一步:实现父节点和左右节点比较
/// <summary>
///
/// </summary>
/// <param name="array">需要排序的数组</param>
/// <param name="nowIndex">当前作为根节点的索引</param>
/// <param name="arrayLength">哪些位置没有确定</param>
static void HeapCompare(int[] array, int nowIndex, int arrayLength)
{
//通过传入的索引 得到它对应的左右叶子节点的索引
//可能算出来的会溢出数组的索引 我们一会再判断
int left = 2 * nowIndex + 1;
int right = 2 * nowIndex + 2;
//用于记录较大数的索引
int biggerIndex = nowIndex;
//先比左 再比右
//不能溢出
if (left < arrayLength && array[left] > array[biggerIndex])
{
//认为目前最大的是左节点 记录索引
biggerIndex = left;
}
if (right < arrayLength && array[right] > array[biggerIndex])
{
//认为目前最大的是右节点 记录索引
biggerIndex = right;
}
if (biggerIndex != nowIndex)
{
int temp = array[nowIndex];
array[nowIndex] = array[biggerIndex];
array[biggerIndex] = temp;
//通过递归 看是否影响了叶子节点他们的三角关系
HeapCompare(array, biggerIndex, arrayLength);
}
}
//第二步:构建大堆顶
static void BuildBigHeap(int[] array)
{
//从最大的非叶子节点索引 开始 不停的往前 去构建大堆顶
for (int i = array.Length / 2 - 1; i >= 0; i--)
{
HeapCompare(array, i, array.Length);
}
}
//第三步:结合大堆顶和节点比较 实现堆排序 把堆顶不停往后移动
static void HeapSort(int[] array)
{
//构建大堆顶
BuildBigHeap(array);
//执行过后
//最大的数肯定就在最上层
//往屁股后面放 得到 屁股后面最后一个索引
for (int i = array.Length - 1; i >= 0; i--)
{
int temp = array[0];
array[0] = array[i];
array[i] = temp;
//重新进行大堆顶调整
HeapCompare(array, 0, i);
}
}
#endregion
#region 知识点三 总结
//基本原理
//构建二叉树
//大堆顶调整
//堆顶往后方
//不停变堆顶
//套路写法
//3个函数
//1个堆顶比较
//1个构建大堆顶
//1个堆排序
//重要规则
//最大非叶子节点索引:
//数组长度/2-1
//父节点和叶子节点索引:
//父节点为i
//左节点2i+1
//右节点2i+2
//注意:
//堆是一类特殊的树
//堆的通用特点就是父节点会大于或小于所有子节点
//我们并没有真正的把数组变成堆
//只是利用了堆的特点来解决排序问题
#endregion
}
}
六、排序算法对比总结
| 算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 特点 |
|---|---|---|---|---|
| 插入排序 | O(n²) | O(1) | 稳定 | 小数据、基本有序 |
| 希尔排序 | ~O(n²) | O(1) | 不稳定 | 插入排序优化 |
| 归并排序 | O(n log n) | O(n) | 稳定 | 大数据、性能稳定 |
| 快速排序 | 平均 O(n log n) | O(log n) | 不稳定 | 实际最快 |
| 堆排序 | O(n log n) | O(1) | 不稳定 | 空间友好 |
下面是一份可直接追加到你文档末尾的“概念补充说明”章节。
我按面试 / 教学 / 自学三者都通用的标准表述来写,语言与你正文风格保持一致,可原样使用。
七、对比
| 算法 | 平均复杂度 | 最坏复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|
| 插入排序 | O(n²) | O(n²) | O(1) | 是 | 小规模数据,几乎有序数据 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 是 | 需要稳定排序,大数据外部排序 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 否 | 大规模随机数据,平均性能优先 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 否 | Top K、优先队列、内存有限、最坏性能稳定 |
八、常见排序相关概念补充说明
1️⃣ 什么是排序算法的「稳定性」
定义
稳定排序:
若待排序序列中存在值相等的元素,排序后它们的相对顺序保持不变,则称该排序算法是稳定的。
举例说明
原始数据(元素含编号):
(5a), (3), (5b), (2)
排序后如果是:
(2), (3), (5a), (5b)
→ 稳定
如果是:
(2), (3), (5b), (5a)
→ 不稳定
稳定性的意义
- 多关键字排序(先按 A 排,再按 B 排)
- 数据库排序
- 业务数据(时间、ID 等隐含顺序)
常见排序算法稳定性
| 算法 | 稳定性 |
|---|---|
| 插入排序 | 稳定 |
| 希尔排序 | 不稳定 |
| 归并排序 | 稳定 |
| 快速排序 | 不稳定 |
| 堆排序 | 不稳定 |
2️⃣ 什么是外部排序(External Sorting)
定义
外部排序:
当待排序数据量大到无法一次性全部加载进内存时,
需要借助 磁盘 / SSD 等外部存储 完成的排序过程。
核心特点
- 数据主要存放在外存
- 排序过程以顺序读写为主
- 极少随机访问
典型实现方式
外部排序几乎都采用:
分块排序 + 多路归并
即:
- 分批读入内存排序
- 将多个有序文件进行归并
👉 归并排序是外部排序的核心算法
实际应用
- 数据库
ORDER BY - 海量日志排序
- 搜索引擎索引构建
3️⃣ 什么是链表排序(Linked List Sorting)
定义
链表排序:
针对 链式存储结构(单链表 / 双链表) 进行的排序。
链表的限制
- ❌ 无法随机访问
- ❌ 无法通过下标直接定位
- ✅ 只能顺序遍历
- ✅ 修改指针成本低
为什么常用归并排序
- 归并排序只需要顺序访问
- 合并过程只改变
next指针 - 时间复杂度稳定
O(n log n)
👉 链表排序的最优通用解法:归并排序
4️⃣ 为什么“传入的数组不算空间复杂度”
空间复杂度的定义
空间复杂度衡量的是:
算法运行过程中,额外申请的辅助空间大小
不计入空间复杂度的内容
- 函数参数
- 原始输入数组
- 固定数量的局部变量
举例
void Sort(int[] arr)
arr是调用者提供的- 排序算法并未新申请
- 👉 不计入空间复杂度
什么时候才算
int[] temp = new int[n];
- 新申请了与 n 成正比的空间
- 👉 空间复杂度至少
O(n)
5️⃣ 为什么递归层数常是 log n
以归并排序 / 快速排序(理想情况)为例:
- 每一层递归把问题规模减半
- 递归层数满足:
n → n/2 → n/4 → ... → 1
这是一个 对数关系
递归深度 ≈
log₂ n
👉 因此:
- 递归栈空间复杂度通常为
O(log n) - 不是每次递归都是 log n,而是递归“层数”是 log n
6️⃣ 为什么“排序所有元素”的复杂度是 n
在每一层排序过程中:
- 每个元素都至少被:
- 比较一次
- 或移动一次
因此:
单层处理成本是
O(n)
结合递归:
- 每层
O(n) - 层数
O(log n)
得到:
总时间复杂度 = O(n log n)
这是归并排序、理想快排的核心推导逻辑。
7️⃣ 什么是栈递归(递归栈)
定义
栈递归指:
函数在尚未返回时再次调用自身(或同类函数),
导致多个函数调用状态同时存在于调用栈(Stack)中。
为什么需要栈
每一层递归都需要保存:
- 参数
- 局部变量
- 返回地址
这些信息只能存放在栈内存中。
哪些排序有栈递归
| 算法 | 是否产生递归栈 |
|---|---|
| 插入 / 希尔 | 否 |
| 归并排序 | 是 |
| 快速排序 | 是 |
| 堆排序 | 否(本质迭代) |
8️⃣ 一句话总总结(建议记忆)
- 稳定性:相等元素顺序是否保持
- 外部排序:数据放不进内存,核心是归并
- 链表排序:无法随机访问,首选归并
- 原数组不算空间复杂度
- 递归层数通常是
log n - 每层处理所有元素是
n - 栈递归来自“未完成函数调用的等待”

浙公网安备 33010602011771号