C 语言中常见排序算法
常见排序算法(C 语言实现)—— 原理、代码与复杂度分析
文档说明
本文档整理了冒泡排序、插入排序、选择排序、希尔排序、快速排序、桶排序、基数排序七种经典排序算法的原理说明、C 语言实现代码、复杂度分析及核心特性,便于学习和查阅。
目录
1. 冒泡排序
1.1 原理说明
冒泡排序是最基础的交换排序算法,核心思想是:
-
将数组分为无序区和有序区,重复遍历无序区,每次比较相邻两个元素,若顺序错误则交换;
-
每一轮遍历会将无序区中最大的元素 “冒泡” 到无序区的末尾(有序区的开头);
-
优化:若某一轮遍历未发生任何交换,说明数组已完全有序,可提前终止。
1.2 C 语言实现代码
#include <stdio.h>
void bubbleSort(int arr[], int n) {
int i, j, temp;
int swapped; // 标记是否发生交换,用于优化
for (i = 0; i < n-1; i++) {
swapped = 0;
for (j = 0; j < n-i-1; j++) {
if (arr[j] > arr[j+1]) {
// 交换相邻元素
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
swapped = 1;
}
}
// 无交换则提前退出
if (swapped == 0)
break;
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr)/sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
bubbleSort(arr, n);
printf("\n排序后的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}
1.3 复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 最好:O (n) | 数组已有序,仅需 1 轮遍历 |
| 最坏:O (n²) | 数组逆序,需 n-1 轮遍历 | |
| 平均:O (n²) | 适用于小规模数据 | |
| 空间复杂度 | O(1) | 原地排序,仅需常数级空间 |
1.4 关键特性
-
稳定排序(不改变相同元素的相对顺序);
-
实现简单,效率低,仅适用于入门学习或极小规模数据。
2. 插入排序
2.1 原理说明
插入排序的核心思想是 “逐步构建有序序列”:
-
将数组分为 “已排序区”(初始为第一个元素)和 “未排序区”;
-
每次从未排序区取出第一个元素,插入到已排序区的合适位置;
-
插入过程中,已排序区中比当前元素大的元素依次后移,腾出位置完成插入。
2.2 C 语言实现代码
#include <stdio.h>
void insertionSort(int arr[], int n) {
int i, key, j;
for (i = 1; i < n; i++) {
key = arr[i]; // 取出未排序区的第一个元素
j = i - 1; // 指向已排序区最后一个元素
// 已排序区元素后移,找到插入位置
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key; // 插入到正确位置
}
}
int main() {
int arr[] = {12, 11, 13, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
insertionSort(arr, n);
printf("\n排序后的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}
2.3 复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 最好:O (n) | 数组已有序,仅需比较无需移动 |
| 最坏:O (n²) | 数组逆序,每个元素需移动全部已排序区 | |
| 平均:O (n²) | 适合小规模 / 基本有序数据 | |
| 空间复杂度 | O(1) | 原地排序,仅需临时变量存储当前元素 |
2.4 关键特性
-
稳定排序;
-
在线算法(可边接收数据边排序);
-
实际应用中比冒泡排序高效(移动次数少于交换次数)。
3. 选择排序
3.1 原理说明
选择排序的核心思想是 “选择最小值”:
-
将数组分为 “已排序区” 和 “未排序区”;
-
每次遍历未排序区,找到最小元素的索引,将其与未排序区的第一个元素交换;
-
重复上述过程,直到未排序区为空。
3.2 C 语言实现代码
#include <stdio.h>
void selectionSort(int arr[], int n) {
int i, j, min_idx, temp;
for (i = 0; i < n-1; i++) {
// 找到未排序区的最小元素索引
min_idx = i;
for (j = i+1; j < n; j++) {
if (arr[j] < arr[min_idx])
min_idx = j;
}
// 交换最小元素与未排序区第一个元素
if (min_idx != i) {
temp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = temp;
}
}
}
int main() {
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr)/sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
selectionSort(arr, n);
printf("\n排序后的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}
3.3 复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 最好 / 最坏 / 平均:O (n²) | 无论数组是否有序,都需遍历未排序区找最小值 |
| 空间复杂度 | O(1) | 原地排序,仅需临时变量交换元素 |
3.4 关键特性
-
不稳定排序(交换可能打乱相同元素的相对顺序);
-
交换次数最少(最多 n-1 次),适合 “写操作代价高” 的场景(如磁盘排序);
-
效率低于插入排序(即使数据有序,仍需遍历)。
4. 希尔排序
4.1 原理说明
希尔排序是插入排序的改进版(又称 “缩小增量排序”),核心思想是 “分组插入排序”:
-
选择一个递减的增量序列(如 n/2, n/4, ..., 1);
-
按增量将数组分为多个子序列(相隔增量的元素为一组);
-
对每个子序列进行插入排序,使数组 “基本有序”;
-
逐步缩小增量,重复分组排序,直到增量为 1;
-
增量为 1 时,数组已基本有序,最后一次插入排序效率极高。
4.2 C 语言实现代码
#include <stdio.h>
void shellSort(int arr[], int n) {
int gap, i, j, temp;
// 初始增量为数组长度的一半,逐步减半
for (gap = n/2; gap > 0; gap /= 2) {
// 对每个增量进行插入排序
for (i = gap; i < n; i++) {
temp = arr[i];
// 插入排序:比较相隔gap的元素
for (j = i; j >= gap && arr[j-gap] > temp; j -= gap) {
arr[j] = arr[j-gap];
}
arr[j] = temp;
}
}
}
int main() {
int arr[] = {12, 34, 54, 2, 3};
int n = sizeof(arr)/sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
shellSort(arr, n);
printf("\n排序后的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}
4.3 复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 最好:O (n) | 数组已有序,增量为 1 时接近插入排序 |
| 最坏:O (n²)(Shell 增量)/ O (n^(3/2))(Hibbard 增量) | 取决于增量序列选择 | |
| 平均:O (n log n) ~ O (n²) | 增量序列越优,复杂度越低 | |
| 空间复杂度 | O(1) | 原地排序,仅需常数级空间 |
4.4 关键特性
-
不稳定排序(分组排序会打乱相同元素的相对顺序);
-
增量序列是性能关键(常见:Shell 增量、Hibbard 增量、Knuth 增量);
-
适用于中等规模数据,性能优于冒泡 / 插入 / 选择排序。
5. 快速排序
5.1 原理说明
快速排序是基于分治思想的高效排序算法,核心思想是 “基准分区 + 递归排序”:
-
选择基准:从数组中选取一个元素作为 “基准(pivot)”(常用策略:选尾元素、中间元素、随机元素,随机基准可避免最坏情况);
-
分区操作:遍历数组,将小于基准的元素移到基准左侧,大于基准的元素移到基准右侧,最终基准处于 “排序后的正确位置”;
-
递归排序:分别对基准左侧和右侧的子数组重复 “选基准 + 分区” 操作,直到子数组长度≤1(天然有序)。
核心分区逻辑:
-
定义左右指针(
low指向子数组起点,high指向终点); -
以尾元素为基准,左指针向右找 “大于基准” 的元素,右指针向左找 “小于基准” 的元素;
-
找到后交换左右指针指向的元素,直到左右指针相遇;
-
将基准与左指针元素交换,完成分区(基准左侧全小、右侧全大)。
5.2 C 语言实现代码
#include <stdio.h>
// 分区函数:返回基准元素的最终位置
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选尾元素作为基准
int i = (low - 1); // 小于基准区域的右边界
for (int j = low; j <= high - 1; j++) {
// 若当前元素小于等于基准,划入左侧区域
if (arr[j] <= pivot) {
i++; // 扩大左侧区域
// 交换元素
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将基准放到正确位置(左侧区域的下一位)
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return (i + 1);
}
// 快速排序递归函数
void quickSort(int arr[], int low, int high) {
if (low < high) {
// 获取基准位置
int pi = partition(arr, low, high);
// 递归排序基准左侧子数组
quickSort(arr, low, pi - 1);
// 递归排序基准右侧子数组
quickSort(arr, pi + 1, high);
}
}
// 辅助打印函数
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
printArray(arr, n);
quickSort(arr, 0, n - 1);
printf("排序后的数组:\n");
printArray(arr, n);
return 0;
}
5.3 复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 最好:O (n log n) | 基准选得均衡,每次分区将数组拆分为两个等长子数组 |
| 最坏:O (n²) | 数组已有序 / 逆序,且选首尾为基准(分区为 1 和 n-1) | |
| 平均:O (n log n) | 实际应用中最接近理论最优的复杂度 | |
| 空间复杂度 | 最好:O (log n) | 递归栈深度(均衡分区) |
| 最坏:O (n) | 递归栈深度(极端分区) | |
| 平均:O (log n) | 仅需递归栈的额外空间 |
5.4 关键特性
-
不稳定排序:分区交换会打乱相同元素的相对顺序;
-
优化方向:
-
基准选择:改用 “三数取中”(首、中、尾取中间值)或随机基准,避免最坏情况;
-
小规模子数组:递归到子数组长度 < 10 时,改用插入排序(减少递归开销);
-
非递归实现:用栈模拟递归,降低栈溢出风险;
-
适用场景:大规模数据排序(实际工程中最常用的排序算法,效率高于希尔排序);
-
原地排序:仅需常数级额外空间(递归栈除外)。
6. 桶排序
6.1 原理说明
桶排序是分布式排序算法,核心思想是 “空间换时间”:
-
确定数据范围,创建若干个 “桶”(每个桶对应一个区间);
-
将元素按值映射到对应的桶中;
-
对每个非空桶内的元素进行排序(可复用插入 / 快速排序);
-
按桶的顺序依次收集元素,形成有序数组。
6.2 C 语言实现代码
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点(存储桶内元素)
typedef struct Node {
int data;
struct Node* next;
} Node;
// 插入节点到链表(保持升序)
void insert(Node** head_ref, int value) {
Node* new_node = (Node*)malloc(sizeof(Node));
new_node->data = value;
new_node->next = NULL;
if (*head_ref == NULL || (*head_ref)->data >= value) {
new_node->next = *head_ref;
*head_ref = new_node;
return;
}
Node* current = *head_ref;
while (current->next != NULL && current->next->data < value) {
current = current->next;
}
new_node->next = current->next;
current->next = new_node;
}
// 桶排序主函数
void bucketSort(int arr[], int n, int max) {
// 创建max+1个桶(假设数据范围0\~max)
Node** buckets = (Node**)calloc(max + 1, sizeof(Node*));
if (buckets == NULL) {
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}
// 将元素分配到桶中
for (int i = 0; i < n; i++) {
insert(\&buckets[arr[i]], arr[i]);
}
// 从桶中收集元素回原数组
int index = 0;
for (int i = 0; i <= max; i++) {
Node* current = buckets[i];
while (current != NULL) {
arr[index++] = current->data;
Node* temp = current;
current = current->next;
free(temp); // 释放节点内存
}
}
free(buckets); // 释放桶数组
}
int main() {
int arr[] = {4, 1, 5, 2, 3, 5};
int n = sizeof(arr) / sizeof(arr[0]);
int max = 5; // 已知数据最大值
printf("排序前的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
bucketSort(arr, n, max);
printf("\n排序后的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}
6.3 复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 最好 / 平均:O (n + k) | k 为桶数,数据均匀分布时,桶内排序代价极低 |
| 最坏:O (n²) | 所有元素集中在一个桶,退化为桶内排序的复杂度 | |
| 空间复杂度 | O(n + k) | 需存储桶数组和桶内元素(链表 / 数组) |
6.4 关键特性
-
稳定性:取决于桶内排序算法(用插入排序则稳定);
-
数据敏感性:仅适用于数据范围已知、分布均匀的场景(如浮点数、年龄排序);
-
可并行化:每个桶的排序可独立执行。
7. 基数排序
7.1 原理说明
基数排序是非比较型排序,核心思想是 “逐位排序”:
-
将整数按位数分解(个位、十位、百位...);
-
从最低位(LSD)或最高位(MSD)开始,逐位对元素进行稳定排序(通常用计数排序 / 桶排序);
-
每一轮排序保证当前位有序,最终所有位排序完成后,数组整体有序。
LSD(最低位优先):最常用,从个位→十位→百位依次排序,适合整数排序;
MSD(最高位优先):从百位→十位→个位依次排序,适合字符串 / 字典序排序。
7.2 C 语言实现代码(LSD)
#include <stdio.h>
#include <stdlib.h>
// 获取数组最大值(确定最大位数)
int getMax(int arr[], int n) {
int max = arr[0];
for (int i = 1; i < n; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
// 按指定位数(exp)进行计数排序(稳定)
void countSort(int arr[], int n, int exp) {
int* output = (int*)malloc(n * sizeof(int));
int count[10] = {0};
// 统计当前位的数字出现次数
for (int i = 0; i < n; i++)
count[(arr[i] / exp) % 10]++;
// 计算累积次数(确定元素在output中的位置)
for (int i = 1; i < 10; i++)
count[i] += count[i - 1];
// 构建输出数组(从后往前,保证稳定性)
for (int i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
// 复制回原数组
for (int i = 0; i < n; i++)
arr[i] = output[i];
free(output);
}
// 基数排序主函数
void radixSort(int arr[], int n) {
if (n <= 0) return;
int max = getMax(arr, n);
// 逐位排序(个位exp=1,十位exp=10,百位exp=100...)
for (int exp = 1; max / exp > 0; exp *= 10)
countSort(arr, n, exp);
}
int main() {
int arr[] = {170, 45, 75, 90, 802, 24, 2, 66};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
radixSort(arr, n);
printf("\n排序后的数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}
7.3 复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 最好 / 平均 / 最坏:O (d×(n + k)) | d 为最大位数,k 为基数(十进制 k=10),每轮计数排序 O (n+k) |
| 空间复杂度 | O(n + k) | 需存储输出数组和计数数组 |
7.4 关键特性
-
稳定排序;
-
仅适用于 “可分解位” 的数据(整数、字符串、可转换为整数的浮点数);
-
处理负数:需将所有数加偏移量转为非负,排序后恢复;
-
大规模数据下效率高(线性时间复杂度),但空间开销较大。
总结
| 排序算法 | 时间复杂度(平均) | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(1) | 稳定 | 入门学习、极小规模数据 |
| 插入排序 | O(n²) | O(1) | 稳定 | 小规模 / 基本有序数据 |
| 选择排序 | O(n²) | O(1) | 不稳定 | 写操作代价高的场景 |
| 希尔排序 | O(n log n) ~ O(n²) | O(1) | 不稳定 | 中等规模数据 |
| 快速排序 | O(n log n) | O(log n) | 不稳定 | 大规模数据(工程首选) |
| 桶排序 | O(n + k) | O(n + k) | 稳定 | 数据范围已知、分布均匀 |
| 基数排序 | O(d×(n + k)) | O(n + k) | 稳定 | 大规模整数 / 字符串排序 |

浙公网安备 33010602011771号