🎬 博主名称 :月夜的风吹雨
🔥 个人专栏 : 《C语言》《基础数据结构》
⛺️任何一个伟大的思想,都有一个微不足道的开始!
✨各位读者朋友,提前说明:本文系统解析 C 语言常见排序算法(含插入、选择、交换、归并及非比较排序),涵盖每类算法的实现思路、核心代码与特性对比,篇幅稍长但干货密度较高 ------ 从基础算法到复杂度分析,每部分均搭配具体思考逻辑,适合用来梳理排序算法体系。建议大家可先浏览目录定位感兴趣的章节,或收藏后分时段消化,以便更好地理解不同算法的适用场景与实现细节~
引言
排序是数据处理中的基础操作,指将一组记录按关键字大小(递增或递减)排列的过程,广泛应用于购物筛选、院校排名、数据检索等场景。本文基于 C 语言,系统解析插入排序、选择排序、交换排序、归并排序及非比较排序等常见算法的实现逻辑,梳理每个算法的设计思路、关键代码与核心特性,为不同场景下的排序方案选择提供参考。


文章目录
- 引言
- 一、排序的基本概念与算法分类
-
- [1.1 核心概念](#1.1 核心概念)
- [1.2 算法分类](#1.2 算法分类)
- 二、插入排序类算法
-
- [2.1 直接插入排序](#2.1 直接插入排序)
-
- [2.1.1 基本思想](#2.1.1 基本思想)
- [2.1.2 实现思路与代码](#2.1.2 实现思路与代码)
- [2.1.3 特性总结](#2.1.3 特性总结)
- [2.2 希尔排序(缩小增量排序)](#2.2 希尔排序(缩小增量排序))
-
- [2.2.1 基本思想](#2.2.1 基本思想)
- [2.2.2 实现思路与代码](#2.2.2 实现思路与代码)
- [2.2.3 特性总结](#2.2.3 特性总结)
- 三、选择排序类算法
-
- [3.1 直接选择排序](#3.1 直接选择排序)
-
- [3.1.1 基本思想](#3.1.1 基本思想)
- [3.1.2 实现思路与代码](#3.1.2 实现思路与代码)
- [3.1.3 特性总结](#3.1.3 特性总结)
- [3.2 堆排序](#3.2 堆排序)
- 四、交换排序类算法
-
- [4.1 冒泡排序](#4.1 冒泡排序)
-
- [4.1.1 基本思想](#4.1.1 基本思想)
- [4.1.2 实现思路与代码](#4.1.2 实现思路与代码)
- [4.1.3 特性总结](#4.1.3 特性总结)
- [4.2 快速排序](#4.2 快速排序)
-
- [4.2.1 Hoare 版本(左右指针法)](#4.2.1 Hoare 版本(左右指针法))
- [4.2.2 挖坑法](#4.2.2 挖坑法)
- [4.2.3 前后指针法(Lomuto 版本)](#4.2.3 前后指针法(Lomuto 版本))
- [4.2.4 非递归版本(栈模拟递归)](#4.2.4 非递归版本(栈模拟递归))
- [4.2.5 特性总结](#4.2.5 特性总结)
- 五、归并排序
-
- [5.1 基本思想](#5.1 基本思想)
- [5.2 实现思路与代码](#5.2 实现思路与代码)
- [5.3 特性总结](#5.3 特性总结)
- 六、非比较排序:计数排序
-
- [6.1 基本思想](#6.1 基本思想)
- [6.2 实现思路与代码](#6.2 实现思路与代码)
- [6.3 特性总结](#6.3 特性总结)
- 七、排序算法复杂度与稳定性汇总
- 八、总结
一、排序的基本概念与算法分类
1.1 核心概念
- 稳定性:若待排序列中存在相同关键字的记录,排序后其相对次序保持不变,则算法稳定,否则不稳定;
- 时间复杂度:排序过程中关键字比较与数据移动的次数,反映算法效率;
- 空间复杂度:排序过程中额外占用的存储空间,反映资源消耗。
1.2 算法分类
根据排序逻辑的差异,常见排序算法可分为四类:
🔍在讲解排序算法之前,我们需要先理解 "稳定性" 的概念。稳定性指的是排序后相同元素的相对位置是否保持不变:若位置改变则为不稳定,保持不变则为稳定。
例如 ,给定数组:[ 5, 5, 3],排序后变为:[3, 5, 5]。可以看到原本排在5前面的5,排序后位置发生了改变,这就是不稳定的排序表现。
二、插入排序类算法
插入排序的核心思想是 "将待排元素插入已有序的子序列",适用于元素接近有序的场景。
2.1 直接插入排序
2.1.1 基本思想
将数组分为 "已有序段" (初始为第 1 个元素)和 "待插入段" ,依次将待插入段的元素(从第 2 个开始)插入已有序段的合适位置,使有序段逐渐扩展至整个数组。
2.1.2 实现思路与代码
- 插入第
i
个元素时,需先保存该元素(避免后续移动覆盖),再从已有序段的末尾(i-1
位置)向前比较:若有序段元素大于待插元素,则向后移动;直到找到小于等于待插元素的位置,将待插元素插入该位置后。
c
void InsertSort(int* a, int n)
{
// 遍历待插入段(从第2个元素开始,i是待插入段的前一个下标)
for (int i = 0; i < n - 1; i++)
{
int end = i; // 已有序段的末尾下标
int tmp = a[end + 1]; // 保存待插入元素(避免移动时覆盖)
// 从后向前比较,移动有序段元素
while (end >= 0)
{
if (a[end] > tmp)
{ //这里一定要用>,>=会改变稳定性
a[end + 1] = a[end]; // 元素后移
end--;
}
else
{
break; // 找到插入位置,退出循环
}
}
a[end + 1] = tmp; // 插入待插元素
}
}
2.1.3 特性总结
- 时间复杂度 :( O ( N 2 ) O(N^2) O(N2))(最坏 / 平均),( O ( N ) O(N) O(N))(最好,数组已有序);
- 空间复杂度 :( O ( 1 ) O(1) O(1))(原地排序);
- 稳定性:稳定;
- 适用场景:小规模数据或接近有序的数据。
2.2 希尔排序(缩小增量排序)
2.2.1 基本思想
对直接插入排序的优化 :通过 "增量gap
" 将数组分为若干组,每组内进行直接插入排序(预排序
);逐渐缩小gap
,重复预排序;当gap=1
时,数组已接近有序,执行最后一次直接插入排序 ,大幅减少移动次数。
2.2.2 实现思路与代码
- 如何选择gap?常用
gap = gap / 3 + 1
(确保最后gap=1);每组内的插入逻辑与直接插入一致,仅将 "相邻元素" 改为 "间隔gap的元素"。
c
void ShellSort(int* a, int n)
{
int gap = n;
// 缩小gap,直到gap=1
while (gap > 1)
{
gap = gap / 3 + 1; // 增量计算,保证最后gap=1
// 遍历每组的待插入元素
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap]; // 保存待插入元素(间隔gap)
// 组内从后向前比较移动
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp; // 组内插入
}
}
}
2.2.3 特性总结
- 时间复杂度 :难以精确计算,通常认为是( O ( N 1.3 ) ∼ O ( N 2 ) O(N^{1.3}) \sim O(N^2) O(N1.3)∼O(N2))(依赖
gap
序列); - 空间复杂度 :( O ( 1 ) O(1) O(1));
- 稳定性:不稳定(分组排序可能打乱相同元素次序);
- 适用场景:中大规模数据,比直接插入排序效率更高。
三、选择排序类算法
选择排序的核心思想是 "每次从待排序列中选出最值元素,放到指定位置",实现简单但效率较低。
3.1 直接选择排序
3.1.1 基本思想
同时查找待排序列的最大值
和最小值
,将最小值放到序列起始位置
、最大值放到末尾位置
,缩小待排范围;重复此过程,直到序列有序。
3.1.2 实现思路与代码
- 用
begin
和end
双指针限定待排范围 ,mini
和maxi
记录最值下标;需注意特殊情况 :若begin
是最大值下标(如begin=maxi
),交换mini
和begin
后,maxi
需更新为mini
(原最小值位置),避免后续交换错误。
c
// 交换辅助函数
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
// 待排范围缩小至begin >= end时结束
while (begin < end)
{
int mini = begin, maxi = begin;
// 遍历待排范围,找最值下标
for (int i = begin; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
// 处理maxi与begin重合的情况
if (begin == maxi)
{
maxi = mini;
}
// 最小值放begin,最大值放end
Swap(&a[mini], &a[begin]);
Swap(&a[maxi], &a[end]);
begin++;
end--;
}
}
3.1.3 特性总结
- 时间复杂度 :( O ( N 2 ) O(N^2) O(N2))(最坏 / 平均 / 最好,均需遍历找最值);
- 空间复杂度 :( O ( 1 ) O(1) O(1));
- 稳定性:不稳定(如序列[5,8,5,2],第一次交换后第一个 5 会到末尾);
- 适用场景:数据量小,对效率要求不高的场景。
3.2 堆排序
堆排序是选择排序的优化,基于堆(完全二叉树)
的特性:堆顶元素为最值。排升序需建大堆(每次选最大元素放末尾),排降序建小堆;核心操作是AdjustDown
(向下调整),具体实现可参考二叉树章节,此处简要总结特性:
二叉树详细讲解传送门 :👉《二叉树的人生:选择左还是右,这是个问题|我的 3 天二叉树小记》
- 时间复杂度 :( O ( N l o g N ) O(NlogN) O(NlogN))(建堆( O ( N ) O(N) O(N)),调整( O ( N l o g N ) O(NlogN) O(NlogN));
- 空间复杂度 :( O ( 1 ) O(1) O(1));
- 稳定性:不稳定;
- 适用场景:中大规模数据,需高效排序且空间有限。
四、交换排序类算法
交换排序的核心思想是 "通过比较交换,使关键字大的元素向后移动、小的向前移动",代表算法为冒泡排序和快速排序。
4.1 冒泡排序
4.1.1 基本思想
相邻元素两两比较,若逆序则交换,使较大元素逐渐 "沉底";引入exchange
标志,若某趟无交换,说明序列已有序,提前退出,优化效率。
4.1.2 实现思路与代码
- 外层循环控制排序趟数,内层循环比较相邻元素;每趟后最大元素已在末尾,内层循环范围可缩小(
j < n - i - 1
);exchange
标志避免无效循环。
c
void BubbleSort(int* a, int n)
{
int exchange = 0;
// 外层循环:最多n趟(实际可能更少)
for (int i = 0; i < n; i++)
{
exchange = 0;
// 内层循环:比较相邻元素,大的沉底
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
exchange = 1; // 标记有交换
}
}
if (exchange == 0)
{
break; // 无交换,序列有序,退出
}
}
}
4.1.3 特性总结
- 时间复杂度 :( O ( N 2 ) O(N^2) O(N2))(最坏 / 平均),( O ( N ) O(N) O(N))(最好,已有序);空间复杂度:( O ( 1 ) O(1) O(1));
- 稳定性:稳定(仅相邻元素交换,不打乱相同元素次序);
- 适用场景:小规模数据或已接近有序的数据。
4.2 快速排序
快速排序是 Hoare
于 1962 年提出的高效算法,基于分治法:选基准值分割序列为 "左小右大" 的子序列,递归处理子序列,直至有序。
4.2.1 Hoare 版本(左右指针法)
实现思路与代码
- 选左端点为基准值(
keyi
),left
(左指针)从左向右找比基准大的元素,right
(右指针)从右向左找比基准小的元素,交换二者;循环至left
>right
,最后将基准值与right
位置元素交换,使基准归位(right
位置元素≤基准);递归处理左(left ~ right-1
)右(right+1~right
)子序列。
c
void PartSort1(int* a, int left, int right)
{
assert(a);
if (left >= right) return; //递归返回条件
int begin = left, end = right;
int midi = FindMidi(a, left, (left + right) / 2, right);
Swap(&a[left], &a[midi]); //找到中间值与left交换元素
int key = left;
while (begin < end)
{
while (begin < end && a[end] >= a[key])
{
end--;
}
while (begin < end && a[begin] < a[key])
{
begin++;
}
if (begin < end)
{
Swap(&a[begin], &a[end]);
}
}
Swap(&a[begin], &a[key]);
PartSort1(a, left, begin - 1); //左边递归
PartSort1(a, begin + 1, right); //右边递归
}
该算法存在一个显著缺陷:当基准元素(
key
)恰好是子数组中的极值(最大值或最小值)时,每次划分只能将数组分成单个元素和剩余部分,导致时间复杂度退化至 O ( N 2 ) O(N^2) O(N2)。为解决这一问题,我们采用三数取中法 进行优化:在每次递归前,选取首元素、中间元素和末元素,将三者中的中间值与首元素交换。这种策略能有效避免因key
值选择不当造成的单侧递归问题。
有人可能会疑惑:当数组有序时,使用三数取中法是否会破坏原有的有序性?实际上,三数取中的交换操作只是临时性的调整。
假设对升序数组 [1,2,3,4,5]
进行快速排序,三数取中的步骤如下:
- 选基准:取左(1)、中(3)、右(5)的中位数(3),将其与最左元素(1)交换 → 数组变成
[3,2,1,4,5]
。(这里看似 "打乱" 了前三个元素,但目的是让基准3更接近中间值,避免划分时出现 "一边倒"。) - 划分(partition):将比3小的元素移到左边,比3大的移到右边 → 最终划分成
[1,2, | 3 |, 4,5]
。(此时3已回到正确位置,左右子数组分别是[1,2]和[4,5],都是有序的。)
c
//三数取中(防止key一直取当前递归的最小数:快排时间复杂度会变为O(N^2))
int FindMidi(int* a, int left, int midi, int right)
{
if (a[left] > a[midi])
{
if (a[right] > a[left]) return left;
else if (a[midi] > a[right]) return midi;
else return right;
}
else //a[left] <= a[midi]
{
if (a[right] < a[left]) return left;
else if (a[right] > a[midi]) return midi;
else return right;
}
}
我们已经解决了
key
取值极端导致时间复杂度退化的问题。接下来需要处理的是栈溢出问题 ,即递归深度过大时可能引发的风险。在学习二叉树时可以看到:最底层元素占总数的50%
,倒数第二层占25%
。因此,只要针对最后几层改用非递归方法,就能显著降低栈溢出的风险。

当递归处理的区间元素数量小于等于10时 ,我们可以改用更高效的排序方法直接完成排序。这时,插入排序就是最佳选择。
下面就是我们优化后的快排代码了:
c
//进行三数取中
int FindMidi(int* a, int left, int midi, int right)
{
if (a[left] > a[midi])
{
if (a[right] > a[left]) return left;
else if (a[midi] > a[right]) return midi;
else return right;
}
else //a[left] <= a[midi]
{
if (a[right] < a[left]) return left;
else if (a[right] > a[midi]) return midi;
else return right;
}
}
void PartSort1(int* a, int left, int right)
{
assert(a);
if (left >= right) return; //没有元素或一个元素时直接返回
//小区间优化(当递归数组元素个数小于等于10个时,直接用插入排序,防止过度递归)
if (right - left + 1 <= 10)
{
InsertSort(a + left, right - left + 1); //插入排序参数是排序的起始位置和元素个数
return;
}
//不优化
//if (left >= right) return;
int begin = left, end = right;
int midi = FindMidi(a, left, (left + right) / 2, right);
Swap(&a[left], &a[midi]); //找到中间值与left交换元素
int key = left;
while (begin < end)
{
while (begin < end && a[end] >= a[key])
{
end--;
}
while (begin < end && a[begin] < a[key])
{
begin++;
}
if (begin < end)
{
Swap(&a[begin], &a[end]);
}
}
Swap(&a[begin], &a[key]);
PartSort1(a, left, begin - 1); //左边递归
PartSort1(a, begin + 1, right); //右边递归
}
4.2.2 挖坑法
实现思路与代码
- 选左端点为基准值(
key
),形成 "坑位" (hole
);right
从右向左找比基准小的元素,填入坑位,right
成为新坑;left
从左向右找比基准大的元素,填入新坑,left
成为新坑;循环至left == right
,将基准值填入最终坑位,基准归位;逻辑比 Hoare 版更直观,避免指针相遇的复杂判断 。
c
//快排挖坑法
void PartSort2(int* a, int left, int right)
{
assert(a);
if (left >= right) return;
if (right - left + 1 >= 0)
{
InsertSort(a + left, right - left + 1);
return;
}
int begin = left, end = right;
int midi = FindMidi(a, left, (left + right) / 2, right);
Swap(&a[left], &a[midi]);
int key = a[left];
while (begin < end)
{
while (begin < end && a[end] >= key)
end--;
a[begin] = a[end];
while (begin < end && a[begin] < key)
begin++;
a[end] = a[begin];
}
a[begin] = key;
PartSort2(a, left, begin - 1);
PartSort2(a, end + 1, right);
}
4.2.3 前后指针法(Lomuto 版本)
实现思路与代码
prev
(前指针)指向序列起始,cur
(后指针)从prev+1
开始;cur
找比基准小的元素,若找到且++prev != cur
,交换a[prev]
和a[cur]
;cur
遍历结束后,交换基准值与a[prev]
,使基准左侧均为小于基准的元素,右侧均为大于基准的元素;逻辑简洁,易实现。

简单来说就是保证prev与cur之间全是比key大的元素,不是则交换(有些细节要自己去处理)
c
//快排双指针法
void QuickSort(int* a, int left, int right)
{
assert(a);
if (left >= right) return; //递归停止条件
//三数取中
int keyi = FindMidi(a, left, (left + right) / 2, right);
Swap(&a[left], &a[keyi]);
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[left] && ++prev != cur)
Swap(&a[cur], &a[prev]);
cur++;
}
Swap(&a[prev], &a[left]);
QuickSort(a, left, prev - 1); //递归左边
QuickSort(a, prev + 1, right); //递归右边
}
4.2.4 非递归版本(栈模拟递归)
实现思路与代码
- 递归的本质是调用栈保存区间 ,非递归用栈模拟:先将初始区间(
left, right
)压栈(注意先压右区间,再压左区间,保证左区间先处理
);循环弹出区间,分割后若子区间长度 > 1,继续压栈;直至栈空,排序完成。避免递归栈溢出(大规模数据递归深度过大)。
c
//快速排序 非递归形式
void QuickSortNonR(int* a, int left, int right)
{
assert(a);
Stack ST;
StackInit(&ST);
StackPush(&ST, right);
StackPush(&ST, left);
while (!StackEmpty(&ST))
{
int left = StackTop(&ST);
StackPop(&ST);
int right = StackTop(&ST);
StackPop(&ST);
int begin = left, end = right;
int midi = FindMidi(a, left, (left + right) / 2, right);
Swap(&a[left], &a[midi]); //找到中间值与left交换元素
int key = left;
while (begin < end)
{
while (begin < end && a[end] >= a[key])
{
end--;
}
while (begin < end && a[begin] < a[key])
{
begin++;
}
if (begin < end)
{
Swap(&a[begin], &a[end]);
}
}
Swap(&a[begin], &a[key]);
//区间优化
/*if (right - left + 1 >= 10)
{
InsertSort(a + left, right - left + 1);
continue;
}
StackPush(&ST, right);
StackPush(&ST, begin + 1);
StackPush(&ST, begin - 1);
StackPush(&ST, left);*/
//不进行区间优化
if (right > begin + 1)
{
StackPush(&ST, right);
StackPush(&ST, begin + 1);
}
if (begin - 1 > left)
{
StackPush(&ST, begin - 1);
StackPush(&ST, left);
}
}
StackDestory(&ST);
}
4.2.5 特性总结
- 时间复杂度 :( O ( N l o g N ) O(NlogN) O(NlogN))(平均 / 最好),( O ( N 2 ) O(N^2) O(N2))(最坏,序列已有序,基准选端点,还不进行三数取中);
- 空间复杂度 :( O ( l o g N ) O(logN) O(logN))(递归栈,非递归栈),最坏( O ( N ) O(N) O(N));
- 稳定性:不稳定(基准交换可能打乱相同元素次序);
- 适用场景:中大规模数据,是实际应用中效率最高的排序算法之一。
五、归并排序
归并排序是分治法的典型应用,核心是 "先分解、后合并 ",确保排序稳定但需额外空间。
5.1 基本思想
将序列分解为若干长度为 1 的子序列
(天然有序),然后两两合并
为有序子序列,重复合并过程,直至形成完整有序序列;合并时需借助临时数组,避免原位修改覆盖数据。

5.2 实现思路与代码
- 递归分解:将区间
[left, right]
分为[left, mid]
和[mid+1, right]
,递归处理子区间;合并有序子序列:用双指针begin1
(左子区间)和begin2
(右子区间),比较元素大小,依次存入临时数组tmp
,最后将tmp
中合并结果拷贝回原数组a。
c
//采用后序遍历
void _MergeSort(int* a,int left, int right, int* temp)
{
if (left >= right) return;
int midi = (left + right) / 2;
//[begin1, end1 = midi(上一个区间的midi)] [begin2 = midi + 1, end2]
_MergeSort(a,left, midi, temp);
_MergeSort(a, midi + 1, right, temp);
int begin1 = left, end1 = midi;
int begin2 = midi + 1, end2 = right;
int i = 0; //归并的每个小区间都是从temp[0]开始的
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
temp[i++] = a[begin1++];
else
temp[i++] = a[begin2++];
}
//处理两个区间未归并的元素
while (begin1 <= end1) temp[i++] = a[begin1++];
while (begin2 <= end2) temp[i++] = a[begin2++];
memcpy(a + left, temp, sizeof(int) * i); //将归并好的区间直接覆盖到原区间(临时空间里面的有效元素)
}
//归并排序 递归版
//稳定
void MergeSort(int* a, int n) //这里的n指的是最后一个元素的下标
{
assert(a);
int* temp = (int*)malloc(sizeof(int) * (n + 1));
_MergeSort(a, 0, n, temp);
free(temp);
temp = NULL;
}
5.3 特性总结
- 时间复杂度 :( O ( N l o g N ) O(NlogN) O(NlogN))(分解与合并均为( O ( N l o g N ) ) O(NlogN)) O(NlogN)));
- 空间复杂度 :( O ( N ) O(N) O(N))(临时数组tmp);
- 稳定性:稳定(合并时相等元素按左子区间优先,保持相对次序);
- 适用场景:需稳定排序、数据量较大的场景(如外部排序,数据无法一次性加载到内存)。
当然,这也有对应的非递归实现,感兴趣的话可以深入了解一下。
c
//归并排序 非递归版
void MergeSortNonR(int* a, int n) //n指的是最后一个元素的下标
{
assert(a);
int* temp = (int*)malloc(sizeof(int) * (n + 1));
if (temp == NULL)
{
perror("malloc fail:");
return;
}
int gap = 1;
while (gap <= n)
{
for (int i = 0; i <= n; i += gap * 2)
{
int begin1 = i, end1 = begin1 + gap - 1;
int begin2 = i + gap, end2 = begin2 + gap - 1;
//处理begin2或end1越界的情况
if (begin2 > n)
continue;
//处理end2越界的情况
if (end2 > n)
end2 = n;
int j = 0;
while (begin1 <= end1 && begin2 <= end2)
{
if (begin2 <= n && a[begin1] <= a[begin2])
temp[j++] = a[begin1++];
else
temp[j++] = a[begin2++];
}
//处理两个区间未归并的元素
while (begin1 <= end1) temp[j++] = a[begin1++];
while (begin2 <= end2) temp[j++] = a[begin2++];
//将归并的元素拷贝到原数组中
memcpy(a + i, temp, sizeof(int) * j);
}
gap *= 2;
}
free(temp);
temp = NULL;
}
六、非比较排序:计数排序
非比较排序不依赖关键字比较,基于 "鸽巢原理",适用于数据范围集中的场景。
6.1 基本思想
- 统计待排序列中每个元素的出现次数;
- 根据次数将元素 "回收" 到原数组,实现排序;
- 若数据范围过大(如
[100, 109]
),用max - min + 1
计算范围(range
),避免空间浪费。
6.2 实现思路与代码
- 先遍历数组找
max
和min
,计算range
;申请count
数组统计次数(count[a[i]-min]++
,将数据映射到[0, range-1]
);最后遍历count
数组,按次数将元素填回原数组(a[j++] = i + min
)。
c
#include <string.h> // 用于memset
void CountSort(int* a, int n)
{
if (n <= 1)
{
return; // 无需排序
}
// 1. 找max和min,确定数据范围
int min = a[0], max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;
// 2. 申请count数组,统计次数
int* count = (int*)malloc(sizeof(int) * range);
if (count == NULL)
{
perror("malloc fail");
return;
}
memset(count, 0, sizeof(int) * range); // 初始化count为0
for (int i = 0; i < n; i++)
{
count[a[i] - min]++; // 数据映射到count下标
}
// 3. 按次数回收元素到原数组
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j++] = i + min; // 映射回原数据
}
}
free(count);
}
6.3 特性总结
- 时间复杂度 :( O ( N + r a n g e ) O(N + range) O(N+range))(N为数据量,range为数据范围);
- 空间复杂度 :( O ( r a n g e ) O(range) O(range));
- 稳定性:稳定(按统计顺序回收,相同元素相对次序不变);
- 适用场景:数据范围小且集中的场景(如学生成绩、年龄排序)。
七、排序算法复杂度与稳定性汇总

八、总结
选择排序算法需结合数据规模、有序程度、稳定性需求综合判断:
- 小规模数据 / 接近有序:直接插入排序、冒泡排序;
- 中大规模数据:快速排序(优先)、堆排序、归并排序;
- 数据范围集中:计数排序;
- 需稳定排序:归并排序、计数排序、直接插入排序;
- 空间有限:堆排序、希尔排序(避免归并 / 计数的额外空间)。
掌握不同算法的核心逻辑与适用场景,才能在实际开发中选择最优方案,同时为后续复杂数据结构(如平衡二叉树、哈希表)的学习奠定基础。