C 语言排序算法专题总结
一、冒泡排序(Bubble Sort)
基本思想
比较相邻两个数的大小,每一趟将最大数"冒"至数组末尾。
优化策略
若某一趟没有进行交换,则说明已经有序,可以设置 flag 提前停止。
代码实现
c
void bubbleSort(int a[], int n){
for(int i = 0; i < n - 1; i++){
for(int j = 0; j < n - i - 1; j++){
if(a[j] > a[j + 1]){
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
}
}
时间复杂度
- 最好情况:O(n) - 已经有序
- 平均情况:O(n²)
- 最坏情况:O(n²)
空间复杂度
O(1)
稳定性
稳定排序
二、选择排序(Selection Sort)
基本思想
每一趟在未排序部分中选择最小的数,放在已排序部分的末尾。
代码实现
c
void selectionSort(int a[], int n){
for(int i = 0; i < n; i++){
int minIndex = i;
// 找到最小值的索引
for(int j = i + 1; j < n; j++){
if(a[minIndex] > a[j]){
minIndex = j;
}
}
// 交换
int tmp = a[i];
a[i] = a[minIndex];
a[minIndex] = tmp;
}
}
时间复杂度
- 最好、平均、最坏情况:O(n²)
空间复杂度
O(1)
稳定性
不稳定排序
三、插入排序(Insertion Sort)
基本思想
前 i 个数字是已排序的部分,将第 i+1 个数字插入到前面已排序序列的适当位置。
代码实现
c
void insertionSort(int a[], int n){
for(int i = 1; i < n; i++){
int key = a[i], j = i;
// 比 key 大的元素向后移动
while(j > 0 && key < a[j - 1]){
a[j] = a[j - 1];
j--;
}
a[j] = key;
}
}
时间复杂度
- 最好情况:O(n) - 已经有序
- 平均情况:O(n²)
- 最坏情况:O(n²)
空间复杂度
O(1)
稳定性
稳定排序
四、快速排序(Quick Sort)
基本思想
采用分治法,选定一个基准元素(pivot),将数组分为两部分:左侧都比基准小,右侧都比基准大,然后递归排序两部分。
代码实现
c
// 分区函数
int partition(int a[], int low, int high){
int key = a[low]; // 选择第一个元素作为基准
while(low < high){
// 从右向左找小于基准的元素
while(a[high] >= key && low < high) high--;
a[low] = a[high];
// 从左向右找大于基准的元素
while(a[low] <= key && low < high) low++;
a[high] = a[low];
}
a[low] = key;
return low;
}
// 快速排序主函数
void quickSort(int a[], int n, int low, int high){
if(low < high){
int pivotkey = partition(a, low, high);
quickSort(a, n, low, pivotkey - 1); // 左半部分
quickSort(a, n, pivotkey + 1, high); // 右半部分
}
}
使用库函数 qsort
c
#include <stdlib.h>
// 比较函数
int compare(const void *a, const void *b){
int num1 = *((int*)a);
int num2 = *((int*)b);
return num1 - num2; // 升序
}
// 调用示例
qsort(arr, n, sizeof(int), compare);
自定义排序规则示例
c
// 西电考题:按各位数字之和降序排列,和相等时按数字本身升序
int sumOfDigits(int num){
int sum = 0;
while(num > 0){
sum += num % 10;
num /= 10;
}
return sum;
}
int compare(const void *a, const void *b){
int num1 = *((int*)a);
int num2 = *((int*)b);
int sum1 = sumOfDigits(num1);
int sum2 = sumOfDigits(num2);
if(sum1 == sum2){
return num1 - num2; // 和相等时按数字升序
} else {
return sum2 - sum1; // 按数字和降序
}
}
时间复杂度
- 最好情况:O(n log n)
- 平均情况:O(n log n)
- 最坏情况:O(n²) - 已经有序时
空间复杂度
O(log n) - 递归栈
稳定性
不稳定排序
五、归并排序(Merge Sort)
基本思想
采用分治法,将数组分为两部分分别排序,然后合并两个有序序列。
代码实现
c
// 合并两个有序子序列
void merge(int a[], int low, int mid, int high){
int i = low, j = mid + 1, k = 0;
int *temp = (int*)malloc((high - low + 1) * sizeof(int));
// 比较两个子序列的元素,较小的放入临时数组
while(i <= mid && j <= high){
if(a[i] <= a[j]){
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
// 复制剩余元素
while(i <= mid) temp[k++] = a[i++];
while(j <= high) temp[k++] = a[j++];
// 将临时数组复制回原数组
for(i = 0; i < high - low + 1; i++){
a[low + i] = temp[i];
}
free(temp);
}
// 归并排序主函数
void mergeSort(int a[], int low, int high){
if(low < high){
int mid = low + (high - low) / 2;
mergeSort(a, low, mid); // 左半部分
mergeSort(a, mid + 1, high); // 右半部分
merge(a, low, mid, high); // 合并
}
}
时间复杂度
- 最好、平均、最坏情况:O(n log n)
空间复杂度
O(n) - 需要额外空间
稳定性
稳定排序
特点
常考稳定性,适合链表排序
六、堆排序(Heap Sort)
基本思想
基于堆数据结构(完全二叉树),构建大根堆/小根堆后进行排序。
代码实现
c
// 堆化函数:维护以 i 为根的子树满足大根堆性质
void heapify(int a[], int n, int i){
int largest = i;
int left = i * 2 + 1; // 左孩子
int right = i * 2 + 2; // 右孩子
// 找出父节点和两个孩子中的最大值
if(left < n && a[largest] < a[left]) largest = left;
if(right < n && a[largest] < a[right]) largest = right;
if(largest != i){
// 交换
int tmp = a[i];
a[i] = a[largest];
a[largest] = tmp;
// 递归调整被影响的子树
heapify(a, n, largest);
}
}
// 堆排序主函数
void heapSort(int a[], int n){
// 构建初始大根堆
for(int i = n / 2 - 1; i >= 0; i--){
heapify(a, n, i);
}
// 依次将堆顶元素与末尾元素交换
for(int i = n - 1; i > 0; i--){
int tmp = a[0];
a[0] = a[i];
a[i] = tmp;
// 重新调整堆
heapify(a, i, 0);
}
}
时间复杂度
- 最好、平均、最坏情况:O(n log n)
空间复杂度
O(1)
稳定性
不稳定排序
七、基数排序(Radix Sort)
基本思想
按照低位优先(LSD)或高位优先(MSD)的方式,逐位进行排序。通常配合计数排序使用。
代码实现
c
// 计数排序辅助函数:按指定位数排序
void countingSortForRadix(int arr[], int n, int exp){
int output[n], count[10] = {0};
// 统计当前位的基数
for(int i = 0; i < n; i++){
count[(arr[i] / exp) % 10]++;
}
// 累加基数
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];
}
}
// 基数排序主函数
void radixSort(int arr[], int n){
// 找出最大值确定位数
int max = arr[0];
for(int i = 1; i < n; i++){
if(arr[i] > max) max = arr[i];
}
// 按每一位进行计数排序
for(int exp = 1; max / exp > 0; exp *= 10){
countingSortForRadix(arr, n, exp);
}
}
时间复杂度
- O(d × (n + k)),d 为位数,k 为基数(通常为 10)
空间复杂度
O(n + k)
稳定性
稳定排序
八、双向冒泡排序(Cocktail Sort)
基本思想
冒泡排序的改进版,每一轮在两个方向上进行冒泡:先从左到右将最大值移到右端,再从右到左将最小值移到左端。
代码实现
c
void cocktailSort(int arr[], int n){
int start = 0, end = n - 1;
while(start < end){
// 从左到右,将最大值移到右端
for(int i = start; i < end; i++){
if(arr[i] > arr[i + 1]){
int tmp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = tmp;
}
}
end--;
// 从右到左,将最小值移到左端
for(int i = end; i > start; i--){
if(arr[i] < arr[i - 1]){
int tmp = arr[i];
arr[i] = arr[i - 1];
arr[i - 1] = tmp;
}
}
start++;
}
}
时间复杂度
- 最好情况:O(n)
- 平均情况:O(n²)
- 最坏情况:O(n²)
空间复杂度
O(1)
稳定性
稳定排序
九、统一测试代码
以下代码可以用来测试上述所有排序算法,只需替换调用的排序函数即可:
c
#include<stdio.h>
#include<stdlib.h>
// 函数声明
void bubbleSort(int a[], int n); // 冒泡排序
void selectionSort(int a[], int n); // 选择排序
void insertionSort(int a[], int n); // 插入排序
void quickSort(int a[], int n, int low, int high); // 快速排序
void mergeSort(int a[], int low, int high); // 归并排序
void heapSort(int a[], int n);
void radixSort(int arr[], int n);
void cocktailSort(int arr[], int n);
int main(){
int n;
printf("请输入数字的个数:");
scanf("%d", &n);
// 动态分配内存
int *arr = (int *)malloc(n * sizeof(int));
if(arr == NULL){
printf("内存分配失败\n");
return 0;
}
printf("请输入%d个数字:", n);
for(int i = 0; i < n; i++){
scanf("%d", &arr[i]);
}
// 对数组进行从小到大的排序
// 可以替换为以下任意排序函数:
// bubbleSort(arr, n); // 冒泡排序
// selectionSort(arr, n); // 选择排序
// insertionSort(arr, n); // 插入排序
// quickSort(arr, n, 0, n-1); // 快速排序
// mergeSort(arr, 0, n-1); // 归并排序
// heapSort(arr, n); // 堆排序
// radixSort(arr, n); // 基数排序
cocktailSort(arr, n); // 双向冒泡排序
// 输出排序结果
for(int i = 0; i < n; i++){
printf("%d ", arr[i]);
}
// 释放内存
free(arr);
return 0;
}
使用说明:
- 编译时需要将所有排序函数的实现代码一起编译
- 通过注释/取消注释不同的排序函数调用来测试不同算法
- 快速排序调用:
quickSort(arr, n, 0, n-1) - 归并排序调用:
mergeSort(arr, 0, n-1)
十、各种排序算法对比
| 排序算法 | 最好时间 | 平均时间 | 最坏时间 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 数据量小、基本有序 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 | 数据量小、对稳定性无要求 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 数据量小、基本有序 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 不稳定 | 大规模数据、一般情况首选 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | 要求稳定性、链表排序 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | 要求最坏情况也是 O(n log n) |
| 基数排序 | O(d×n) | O(d×n) | O(d×n) | O(n+k) | 稳定 | 整数排序、位数固定 |
| 双向冒泡 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 数据量小、基本有序 |
十一、排序算法选择建议
1. 小规模数据(n ≤ 50)
- 推荐:插入排序、冒泡排序
- 理由:简单易懂,常数因子小
2. 大规模数据
- 一般情况:快速排序(效率最高)
- 要求稳定性:归并排序
- 要求最坏情况好:堆排序
3. 特殊场景
- 基本有序:插入排序、冒泡排序
- 整数排序且范围已知:基数排序、计数排序
- 链表排序:归并排序
- 内存受限:堆排序、原地排序
4. 考试重点
- 稳定性判断:归并、插入、冒泡、基数是稳定的
- 时间复杂度分析:重点掌握快排、归并、堆排序
- 实际应用:qsort 库函数的使用
十二、常见问题
1. 如何判断排序算法的稳定性?
答 :如果排序后相同元素的相对位置不变,则为稳定排序。例如:[3a, 1, 3b, 2] 排序后为 [1, 2, 3a, 3b],3a 仍在 3b 前面。
2. 快速排序为什么不稳定?
答:因为在分区过程中,可能会跨越多个位置交换元素,导致相同元素的相对位置改变。
3. 归并排序为什么稳定?
答:因为在合并时,当两个元素相等时,优先选择左边子序列的元素,保持了原有顺序。
4. 如何选择基准元素(快速排序)?
答:
- 固定位置(如第一个、最后一个)
- 随机选择
- 三数取中(首、中、尾的中位数)
5. 堆排序中建堆的时间复杂度?
答:O(n),不是 O(n log n)。因为大部分节点的高度较小。