
🎬 博主名称 :键盘敲碎了雾霭
🔥 个人专栏 : 《C语言》《数据结构》
⛺️指尖敲代码,雾霭皆可破

文章目录
-
- 引言
- 一、插入排序
-
- [1.1 基本思想](#1.1 基本思想)
- [1.2 代码实现](#1.2 代码实现)
- [1.3 特性总结](#1.3 特性总结)
- 二、希尔排序
-
- [2.1 基本思想](#2.1 基本思想)
- [2.2 代码实现](#2.2 代码实现)
- [2.3 特性总结](#2.3 特性总结)
- 三、选择排序
-
- [3.1 基本思想](#3.1 基本思想)
- [3.2 代码实现](#3.2 代码实现)
- [3.3 特性总结](#3.3 特性总结)
- 四、堆排序
-
- [4.1 基本思想](#4.1 基本思想)
- [4.2 代码实现](#4.2 代码实现)
- [4.3 特性总结](#4.3 特性总结)
- 五、冒泡排序
-
- [5.1 基本思想](#5.1 基本思想)
- [5.2 代码实现](#5.2 代码实现)
- [5.3 特性总结](#5.3 特性总结)
- 六、快速排序
- 七、归并排序
- 八、计数排序
-
- [8.1 基本思想](#8.1 基本思想)
- [8.2 代码实现](#8.2 代码实现)
- [8.3 特性总结](#8.3 特性总结)
- 总结与对比
引言
排序是计算机科学中最基础的操作之一,几乎所有的应用程序都会涉及数据的排序。本文将详细介绍八种经典的排序算法:插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序 。每种算法都包含基本思想、C语言代码实现、特性总结(时间复杂度、空间复杂度、稳定性),并辅以必要的文字说明,帮助读者深入理解其原理与适用场景。
文章中的所有代码均经过测试,可直接在C语言环境中运行。
一、插入排序
1.1 基本思想
插入排序的工作方式类似于我们整理扑克牌:将待排序的元素逐个插入到已经有序的序列中的适当位置,直到全部插入完毕。
初始时,认为第一个元素已经有序,然后取下一个元素,在已排序序列中从后向前扫描,找到相应位置并插入。

1.2 代码实现
c
void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
1.3 特性总结
- 元素集合越接近有序,直接插入排序的时间效率越高。
- 时间复杂度:最坏情况(逆序)O(N²),最好情况(有序)O(N)。
- 空间复杂度:O(1),原地排序。
- 稳定性:稳定(因为当待插入元素与有序序列中的元素相等时,插入到其后面,相对顺序不变)。
二、希尔排序
2.1 基本思想
希尔排序是对插入排序的改进,也称"缩小增量排序"。它通过将待排序数组按下标的一定增量分组,对每组使用插入排序;随着增量逐渐减少,每组包含的元素越来越多,当增量减至1时,整个数组基本有序,最后再进行一次插入排序。
2.2 代码实现
这里给出两种常见的写法,本质上都是对每组进行插入排序,只是循环层数不同。
版本一(三层循环,按组处理)
c
void ShellSort(int* arr, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1; // 保证最后 gap=1
for (int j = 0; j < gap; j++) // 对每组进行插入排序
{
for (int i = j; i < n - gap; i += gap)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
}
版本二(两层循环,多组交替进行)
c
void ShellSort(int* arr, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++) // 多组交替进行插入排序
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}
2.3 特性总结
- 希尔排序是对直接插入排序的优化,通过预排序使数组接近有序,最后一步插入排序效率很高。
- 时间复杂度 :依赖于增量序列的选取,一般可认为在 O(N^1.3) ~ O(N^1.5) 之间。
- 空间复杂度:O(1)。
- 稳定性:不稳定(因为相同元素可能分到不同组,导致相对顺序改变)。
三、选择排序
3.1 基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
3.2 代码实现
版本一(每次选一个最小值)
c
void SelectSort(int* arr, int n)
{
for (int j = 0; j < n; j++)
{
int min = j;
for (int i = j + 1; i < n; i++)
{
if (arr[min] > arr[i])
{
min = i;
}
}
int tmp = arr[min];
arr[min] = arr[j];
arr[j] = tmp;
}
}
版本二(同时选出最小值和最大值,减少循环次数)
c
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int min = begin;
int max = end;
for (int i = begin + 1; i <= end; i++)
{
if (arr[min] > arr[i])
{
min = i;
}
if (arr[max] < arr[i])
{
max = i;
}
}
Swap(&arr[begin], &arr[min]);
// 如果最大值在 begin 位置,修正 max
if (max == begin)
max = min;
Swap(&arr[end], &arr[max]);
begin++;
end--;
}
}
3.3 特性总结
- 选择排序的思路非常简单,但效率较低,实际中很少使用。
- 时间复杂度:始终为 O(N²)。
- 空间复杂度:O(1)。
- 稳定性:不稳定(例如,交换时可能把前面的相同元素换到后面)。
四、堆排序
4.1 基本思想
堆排序是利用堆这种数据结构设计的排序算法,属于选择排序的一种。它通过建堆(升序建大堆,降序建小堆)将最大(最小)元素放到堆顶,然后与堆尾元素交换,再对剩余元素向下调整,重复此过程。
4.2 代码实现
c
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void AdjustDown(int* arr, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && arr[child] < arr[child + 1])
{
child++;
}
if (arr[child] > arr[parent])
{
Swap(&arr[child], &arr[parent]);
}
parent = child;
child = parent * 2 + 1;
}
}
void HeapSort(int* arr, int n)
{
// 建大堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
4.3 特性总结
- 堆排序使用堆来选数,效率较高。
- 时间复杂度:O(N log N)。
- 空间复杂度:O(1)。
- 稳定性:不稳定(向下调整可能破坏相同元素的相对顺序)。
五、冒泡排序
5.1 基本思想
冒泡排序通过重复遍历待排序序列,比较相邻元素,如果顺序错误就交换,直到没有需要交换的元素为止。每一趟都会将最大(最小)元素"冒泡"到末尾。

5.2 代码实现
c
void BubbleSort(int* arr, int n)
{
for (int j = 0; j < n; j++)
{
for (int i = 0; i < n - 1 - j; i++)
{
if (arr[i] > arr[i + 1])
{
Swap(&arr[i], &arr[i + 1]);
}
}
}
}
5.3 特性总结
- 冒泡排序非常容易理解和实现。
- 时间复杂度:最坏 O(N²),最好 O(N)(优化后可检测是否已经有序)。
- 空间复杂度:O(1)。
- 稳定性:稳定(相邻元素相等时不交换)。
六、快速排序
6.1 基本思想
快速排序采用分治思想:选择一个基准值(key),将待排序序列分成两部分,左部分所有元素 ≤ 基准值,右部分所有元素 ≥ 基准值,然后递归地对左右两部分进行快速排序。
6.2 三种常见分区方法
hoare版本

c
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int keyi = begin;
while (begin < end)
{
while (begin < end && arr[end] >= arr[keyi])
end--;
while (begin < end && arr[begin] <= arr[keyi])
begin++;
Swap(&arr[begin], &arr[end]);
}
Swap(&arr[keyi], &arr[begin]);
keyi = begin;
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
挖坑法

c
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int tmp = arr[left];
int hole = left;
while (begin < end)
{
while (begin < end && arr[end] >= tmp)
end--;
arr[hole] = arr[end];
hole = end;
while (begin < end && arr[begin] <= tmp)
begin++;
arr[hole] = arr[begin];
hole = begin;
}
arr[hole] = tmp;
QuickSort(arr, left, hole - 1);
QuickSort(arr, hole + 1, right);
}
前后指针法

c
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
return;
int keyi = left;
int prev = keyi;
int cur = prev + 1;
while (cur <= right)
{
if (arr[cur] <= arr[keyi])
{
prev++;
Swap(&arr[prev], &arr[cur]);
}
cur++;
}
Swap(&arr[prev], &arr[keyi]);
keyi = prev;
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
6.3 快速排序优化
三数取中法选key
为了避免最坏情况(例如数组已经有序),可以使用三数取中法选取基准值。
c
int Mid(int* arr, int left, int right)
{
int mid = (left + right) / 2;
if (arr[left] < arr[right])
{
if (arr[right] < arr[mid]) return right;
else if (arr[mid] > arr[left]) return mid;
else return left;
}
else
{
if (arr[right] > arr[mid]) return right;
else if (arr[left] > arr[mid]) return mid;
else return left;
}
}
在分区前,将选出的中间值与最左边交换即可。
小区间优化
递归到小的子区间(例如元素个数 ≤ 10)时,使用插入排序代替递归,可以减少递归深度,提高效率。
c
if ((right - left + 1) <= 10)
{
InsertSort(arr + left, right - left + 1);
return;
}
6.4 快速排序非递归实现(借助栈)
c
int PartSort(int* arr, int left, int right)
{
int begin = left, end = right;
int keyi = begin;
while (begin < end)
{
while (begin < end && arr[end] >= arr[keyi]) end--;
while (begin < end && arr[begin] <= arr[keyi]) begin++;
Swap(&arr[begin], &arr[end]);
}
Swap(&arr[keyi], &arr[begin]);
return begin;
}
void QuickSortNonR(int* arr, int left, int right)
{
ST st; // 假设栈已实现
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
int left1 = STTop(&st); STPop(&st);
int right1 = STTop(&st); STPop(&st);
int keyi = PartSort(arr, left1, right1);
if (keyi + 1 <= right1)
{
STPush(&st, right1);
STPush(&st, keyi + 1);
}
if (left1 <= keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, left1);
}
}
STDestroy(&st);
}
6.5 特性总结
- 快速排序综合性能很好,是实际中最常用的排序算法之一。
- 时间复杂度:平均 O(N log N),最坏 O(N²)(可通过三数取中优化)。
- 空间复杂度:递归栈平均 O(log N),最坏 O(N)。
- 稳定性:不稳定(分区过程中可能交换相同元素)。
七、归并排序
7.1 基本思想
归并排序采用经典的分治策略:将序列分成两个子序列,分别排序后,再将两个有序子序列合并成一个有序序列。通常使用递归实现,也可以用非递归(迭代)实现。

7.2 代码实现
递归版本
c
void _MergeSort(int* arr, int* tmp, int left, int right)
{
if (left == right)
return;
int mid = (left + right) / 2;
_MergeSort(arr, tmp, left, mid);
_MergeSort(arr, tmp, mid + 1, right);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
tmp[i++] = arr[begin1++];
else
tmp[i++] = arr[begin2++];
}
while (begin1 <= end1)
tmp[i++] = arr[begin1++];
while (begin2 <= end2)
tmp[i++] = arr[begin2++];
memcpy(arr + left, tmp + left, sizeof(int) * (right - left + 1));
}
void MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(arr, tmp, 0, n - 1);
free(tmp);
}
非递归(迭代)版本
c
void MergeNonR(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = i;
if (begin2 >= n) // 第二组不存在,无需合并
break;
if (end2 >= n) // 第二组右边界越界,修正
end2 = n - 1;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
tmp[j++] = arr[begin1++];
else
tmp[j++] = arr[begin2++];
}
while (begin1 <= end1)
tmp[j++] = arr[begin1++];
while (begin2 <= end2)
tmp[j++] = arr[begin2++];
memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
}
7.3 特性总结
- 归并排序需要额外的空间,但它是稳定的,适合外部排序。
- 时间复杂度:始终为 O(N log N)。
- 空间复杂度:O(N)。
- 稳定性:稳定(合并时若相等,先取左半部分,保证顺序不变)。
八、计数排序
8.1 基本思想
计数排序是一种非比较排序,适用于数据范围集中的情况。它通过统计每个元素出现的次数,然后根据统计结果将元素放回原数组。
步骤:
- 找出待排序数组的最大值和最小值,确定计数数组的范围。
- 统计每个值出现的次数。
- 根据次数,将元素依次放回原数组。
8.2 代码实现
c
void CountSort(int* arr, int n)
{
int min = arr[0], max = arr[0];
for (int i = 0; i < n; i++)
{
if (min > arr[i]) min = arr[i];
if (max < arr[i]) max = arr[i];
}
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("calloc");
return;
}
// 统计次数
for (int i = 0; i < n; i++)
{
count[arr[i] - min]++;
}
// 回写
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
arr[j++] = i + min;
}
}
free(count);
}
8.3 特性总结
- 计数排序在数据范围集中时效率极高,但适用范围有限(只能处理整数或可映射的数据)。
- 时间复杂度:O(MAX(N, range)),range为数据范围。
- 空间复杂度:O(range)。
- 稳定性:稳定(但上述实现是稳定的吗?实际上,如果从后向前遍历原数组并放入结果数组可以保持稳定,但上面的实现是直接按次数顺序填充,对于相同元素,相对顺序无法保证------但因为计数排序通常用于整数,稳定性意义不大;严格来说,上述写法不稳定,若要稳定需采用累加位置并反向填充的方法)。
总结与对比
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 插入排序 | O(N²) | O(N²) | O(1) | 稳定 |
| 希尔排序 | O(N^1.3) | O(N²) | O(1) | 不稳定 |
| 选择排序 | O(N²) | O(N²) | O(1) | 不稳定 |
| 堆排序 | O(N log N) | O(N log N) | O(1) | 不稳定 |
| 冒泡排序 | O(N²) | O(N²) | O(1) | 稳定 |
| 快速排序 | O(N log N) | O(N²) | O(log N)~O(N) | 不稳定 |
| 归并排序 | O(N log N) | O(N log N) | O(N) | 稳定 |
| 计数排序 | O(N+range) | O(N+range) | O(range) | 稳定* |
选择建议:
- 数据量小、基本有序:插入排序。
- 一般情况、对稳定性无要求:快速排序(最快)。
- 要求稳定、内存充足:归并排序。
- 数据范围小、整数:计数排序。
- 内存紧张、需稳定:冒泡排序(但效率低)或归并排序(但需要额外内存),实际上内存紧张时可采用堆排序或希尔排序。
希望本文能帮助你全面理解这八大排序算法。如果有任何疑问或建议,欢迎在评论区留言讨论!
(完)