🎯 适合人群:C语言学习者、数据结构初学者、面试准备者
📦 特点:完整可运行代码 + 逐行注释 + 复杂度分析
📋 前置准备:通用工具函数
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// 交换两个整数
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 打印数组
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// 生成随机数组
void generateRandomArray(int arr[], int n, int min, int max) {
srand(time(NULL));
for (int i = 0; i < n; i++) {
arr[i] = rand() % (max - min + 1) + min;
}
}
// 比较函数(便于统计比较次数,可选)
int compareCount = 0;
int swapCount = 0;
1️⃣ 冒泡排序 (Bubble Sort)
🔍 算法原理
arduino
重复遍历数组,比较相邻元素,如果顺序错误就交换
每轮遍历后,最大元素会"冒泡"到末尾
优化:如果某轮没有发生交换,说明已有序,提前结束
💻 C语言实现
c
/**
* 冒泡排序 - 优化版本
* @param arr 待排序数组
* @param n 数组长度
* 时间复杂度: O(n²) 最好: O(n) 空间复杂度: O(1) 稳定: ✓
*/
void bubbleSort(int arr[], int n) {
if (n <= 1) return;
for (int i = 0; i < n - 1; i++) {
int exchanged = 1; // 标记本轮是否发生交换
// 每轮将最大值"冒泡"到末尾,所以范围逐渐缩小
for (int j = 0; j < n - 1 - i; j++) {
compareCount++;
if (arr[j] > arr[j + 1]) { // 升序:前面的大于后面的就交换
swap(&arr[j], &arr[j + 1]);
swapCount++;
exchanged = 0; // 发生了交换
}
}
// 优化:如果本轮没交换,说明已经有序
if (exchanged) break;
}
}
🧪 测试代码
c
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr)/sizeof(arr[0]);
printf("排序前: ");
printArray(arr, n);
bubbleSort(arr, n);
printf("排序后: ");
printArray(arr, n);
return 0;
}
2️⃣ 选择排序 (Selection Sort)
🔍 算法原理
每轮从未排序部分找到最小元素,放到已排序部分末尾
类似:从一堆牌中每次挑出最小的放左边
💻 C语言实现
c
/**
* 选择排序
* @param arr 待排序数组
* @param n 数组长度
* 时间复杂度: O(n²) 空间复杂度: O(1) 稳定: ✗
*/
void selectionSort(int arr[], int n) {
if (n <= 1) return;
// i表示当前要确定第i小的元素位置
for (int i = 0; i < n - 1; i++) {
int minIdx = i; // 假设当前位置是最小值
// 在剩余未排序部分找真正的最小值
for (int j = i + 1; j < n; j++) {
compareCount++;
if (arr[j] < arr[minIdx]) {
minIdx = j; // 更新最小值索引
}
}
// 将最小值交换到当前位置
if (minIdx != i) {
swap(&arr[i], &arr[minIdx]);
swapCount++;
}
}
}
3️⃣ 堆排序 (Heap Sort)
🔍 算法原理
markdown
1. 建堆:将数组构建成大顶堆(父节点 >= 子节点)
2. 排序:将堆顶(最大值)与末尾交换,堆大小-1,重新调整堆
3. 重复步骤2直到堆大小为1
完全二叉树索引关系:
- 父节点i的左子: 2*i+1, 右子: 2*i+2
- 子节点i的父节点: (i-1)/2
💻 C语言实现
c
/**
* 下沉操作:维护堆的性质
* @param arr 数组
* @param n 堆的大小
* @param i 当前节点索引
*/
void siftDown(int arr[], int n, int i) {
int largest = i; // 假设当前节点最大
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 找三个节点中的最大值
if (left < n) {
compareCount++;
if (arr[left] > arr[largest]) {
largest = left;
}
}
if (right < n) {
compareCount++;
if (arr[right] > arr[largest]) {
largest = right;
}
}
// 如果最大值不是当前节点,交换并继续下沉
if (largest != i) {
swap(&arr[i], &arr[largest]);
swapCount++;
siftDown(arr, n, largest); // 递归下沉
}
}
/**
* 堆排序
* @param arr 待排序数组
* @param n 数组长度
* 时间复杂度: O(nlogn) 空间复杂度: O(1) 稳定: ✗
*/
void heapSort(int arr[], int n) {
if (n <= 1) return;
// 1. 建堆:从最后一个非叶子节点开始向上调整
// 最后一个非叶子节点索引: n/2 - 1
for (int i = n / 2 - 1; i >= 0; i--) {
siftDown(arr, n, i);
}
// 2. 排序:逐个将堆顶元素放到末尾
for (int i = n - 1; i > 0; i--) {
swap(&arr[0], &arr[i]); // 堆顶(最大)换到末尾
swapCount++;
siftDown(arr, i, 0); // 对剩余i个元素重新建堆
}
}
4️⃣ 插入排序 (Insertion Sort)
🔍 算法原理
类似整理扑克牌:每次拿一张新牌,插入到已排好序的牌堆中合适位置
从后向前扫描已排序部分,找到插入位置,元素后移腾出空间
💻 C语言实现
c
/**
* 插入排序
* @param arr 待排序数组
* @param n 数组长度
* 时间复杂度: O(n²) 最好: O(n) 空间复杂度: O(1) 稳定: ✓
*/
void insertionSort(int arr[], int n) {
if (n <= 1) return;
// i表示当前要插入的元素索引(从第2个元素开始)
for (int i = 1; i < n; i++) {
int key = arr[i]; // 待插入的元素
int j = i - 1;
// 从后向前扫描,大于key的元素后移
while (j >= 0) {
compareCount++;
if (arr[j] > key) {
arr[j + 1] = arr[j]; // 元素后移
j--;
} else {
break; // 找到插入位置
}
}
arr[j + 1] = key; // 插入到正确位置
}
}
5️⃣ 归并排序 (Merge Sort)
🔍 算法原理
markdown
分治法经典应用:
1. 分解:将数组递归分成两半,直到只剩1个元素
2. 合并:将两个有序子数组合并成一个有序数组
合并过程:用临时数组,双指针比较两个子数组元素,小的先放入
💻 C语言实现
c
/**
* 合并两个有序子数组
* @param arr 原数组
* @param left 左边界
* @param mid 中间位置(左子数组结束位置)
* @param right 右边界(不包含)
*/
void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left; // 左子数组长度
int n2 = right - mid; // 右子数组长度
// 创建临时数组存储左子数组(右子数组可直接用原数组)
int *leftArr = (int*)malloc(n1 * sizeof(int));
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
int i = 0, j = 0, k = left; // i:左数组指针, j:右数组指针, k:原数组指针
// 合并两个子数组
while (i < n1 && j < n2) {
compareCount++;
if (leftArr[i] <= arr[mid + j]) {
arr[k++] = leftArr[i++];
} else {
arr[k++] = arr[mid + (j++)];
}
}
// 复制剩余元素(只需复制左数组剩余,右数组已在原位置)
while (i < n1) {
arr[k++] = leftArr[i++];
}
free(leftArr); // 释放临时数组
}
/**
* 归并排序递归函数
* @param arr 待排序数组
* @param left 左边界
* @param right 右边界(不包含)
*/
void mergeSortRecursive(int arr[], int left, int right) {
if (right - left <= 1) return; // 只有一个元素,无需排序
int mid = left + (right - left) / 2; // 防止溢出的中点计算
mergeSortRecursive(arr, left, mid); // 排序左半部分
mergeSortRecursive(arr, mid, right); // 排序右半部分
merge(arr, left, mid, right); // 合并两部分
}
/**
* 归并排序入口函数
* @param arr 待排序数组
* @param n 数组长度
* 时间复杂度: O(nlogn) 空间复杂度: O(n) 稳定: ✓
*/
void mergeSort(int arr[], int n) {
if (n <= 1) return;
mergeSortRecursive(arr, 0, n);
}
6️⃣ 快速排序 (Quick Sort)
🔍 算法原理
markdown
分治法 + 分区操作:
1. 选基准:随机选一个元素作为基准(pivot)
2. 分区:小于pivot的放左边,大于的放右边
3. 递归:对左右子数组递归执行1-2步
优化:随机选基准避免最坏情况 O(n²)
💻 C语言实现
c
/**
* 分区函数:将数组按pivot分成两部分
* @param arr 数组
* @param low 起始索引
* @param high 结束索引
* @return pivot最终位置
*/
int partition(int arr[], int low, int high) {
// 随机选基准,避免最坏情况
int randomIdx = low + rand() % (high - low + 1);
swap(&arr[low], &arr[randomIdx]); // 基准换到开头
swapCount++;
int pivot = arr[low]; // 基准值
int i = low + 1; // 从左向右找大于pivot的
int j = high; // 从右向左找小于pivot的
while (i <= j) {
// 从左找第一个大于pivot的
while (i <= j) {
compareCount++;
if (arr[i] > pivot) break;
i++;
}
// 从右找第一个小于pivot的
while (i <= j) {
compareCount++;
if (arr[j] < pivot) break;
j--;
}
// 交换并移动指针
if (i < j) {
swap(&arr[i], &arr[j]);
swapCount++;
i++;
j--;
}
}
// 将pivot放到正确位置(j的位置)
swap(&arr[low], &arr[j]);
swapCount++;
return j; // 返回pivot的最终位置
}
/**
* 快速排序递归函数
* @param arr 数组
* @param low 起始索引
* @param high 结束索引
*/
void quickSortRecursive(int arr[], int low, int high) {
if (low >= high) return; // 子数组长度<=1
int pivotIdx = partition(arr, low, high); // 分区并获取pivot位置
quickSortRecursive(arr, low, pivotIdx - 1); // 排序左半部分
quickSortRecursive(arr, pivotIdx + 1, high); // 排序右半部分
}
/**
* 快速排序入口函数
* @param arr 待排序数组
* @param n 数组长度
* 时间复杂度: O(nlogn) 最坏: O(n²) 空间复杂度: O(logn) 稳定: ✗
*/
void quickSort(int arr[], int n) {
if (n <= 1) return;
srand(time(NULL)); // 初始化随机种子
quickSortRecursive(arr, 0, n - 1);
}
7️⃣ 希尔排序 (Shell Sort)
🔍 算法原理
markdown
插入排序的优化版本:
1. 选择步长序列(如: n/2, n/4, ..., 1)
2. 按步长分组,对每组进行插入排序
3. 步长逐渐减小,最后步长为1时就是普通插入排序
优势:前期大步长让元素快速移动到大致位置,后期小步长精细调整
💻 C语言实现
c
/**
* 希尔排序
* @param arr 待排序数组
* @param n 数组长度
* 时间复杂度: O(n^1.3)~O(n²) 取决于步长序列 空间复杂度: O(1) 稳定: ✗
*/
void shellSort(int arr[], int n) {
if (n <= 1) return;
// Shell原始步长序列: n/2, n/4, ..., 1
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个gap,进行gap组插入排序
// 每组:起始位置为0,1,2,...,gap-1
for (int start = 0; start < gap; start++) {
// 对start, start+gap, start+2gap, ... 这组进行插入排序
for (int i = start + gap; i < n; i += gap) {
int key = arr[i];
int j = i - gap;
// 组内插入排序
while (j >= start) {
compareCount++;
if (arr[j] > key) {
arr[j + gap] = arr[j];
j -= gap;
} else {
break;
}
}
arr[j + gap] = key;
}
}
}
}
/*
* 进阶:使用Sedgewick步长序列(更高效)
* 序列: 1, 5, 19, 41, 109, ...
* 可根据需要替换上面的gap循环
*/
8️⃣ 计数排序 (Counting Sort)
🔍 算法原理
markdown
非比较排序,适用于整数且范围不大的情况:
1. 找出最大值max和最小值min
2. 创建计数数组count,统计每个值出现次数
3. 对count做前缀和,得到每个值的最终位置
4. 从后向前遍历原数组,按count位置放入结果数组
优势:时间复杂度O(n+k),k为数据范围
限制:数据范围不能太大,否则空间浪费
💻 C语言实现
c
/**
* 计数排序
* @param arr 待排序数组
* @param n 数组长度
* @param min 数据最小值
* @param max 数据最大值
* 时间复杂度: O(n+k) k=max-min+1 空间复杂度: O(k) 稳定: ✓
*/
void countingSort(int arr[], int n, int min, int max) {
if (n <= 1) return;
int range = max - min + 1; // 数据范围
int *count = (int*)calloc(range, sizeof(int)); // 计数数组
int *output = (int*)malloc(n * sizeof(int)); // 输出数组
// 1. 统计每个元素出现次数
for (int i = 0; i < n; i++) {
count[arr[i] - min]++;
}
// 2. 计算前缀和:count[i]表示<=i的元素个数
for (int i = 1; i < range; i++) {
count[i] += count[i - 1];
}
// 3. 从后向前遍历,稳定排序的关键!
for (int i = n - 1; i >= 0; i--) {
int idx = arr[i] - min;
output[count[idx] - 1] = arr[i]; // 放到正确位置
count[idx]--; // 该值的可用位置-1
}
// 4. 复制回原数组
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
free(count);
free(output);
}
// 简化版:当min=0时
void countingSortSimple(int arr[], int n, int max) {
countingSort(arr, n, 0, max);
}
9️⃣ 基数排序 (Radix Sort)
🔍 算法原理
markdown
按位排序:从最低位到最高位,每位用计数排序
1. 找出最大数,确定最大位数d
2. 从个位开始,对每位进行计数排序(稳定排序)
3. 重复直到最高位
优势:时间复杂度O(d*(n+k)),d为位数,适合位数少的整数
💻 C语言实现
c
/**
* 获取数字的第digit位(从右往左,0表示个位)
*/
int getDigit(int num, int digit) {
for (int i = 0; i < digit; i++) {
num /= 10;
}
return num % 10;
}
/**
* 获取最大值的位数
*/
int getMaxDigits(int arr[], int n) {
int max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > max) max = arr[i];
}
int digits = 0;
while (max > 0) {
digits++;
max /= 10;
}
return digits;
}
/**
* 基数排序(LSD:最低位优先)
* @param arr 待排序数组
* @param n 数组长度
* 时间复杂度: O(d*(n+k)) d:位数 k:进制(10) 空间复杂度: O(n+k) 稳定: ✓
*/
void radixSort(int arr[], int n) {
if (n <= 1) return;
int maxDigits = getMaxDigits(arr, n); // 最大位数
int *output = (int*)malloc(n * sizeof(int));
// 从个位到最高位,逐位排序
for (int digit = 0; digit < maxDigits; digit++) {
int count[10] = {0}; // 0-9共10个桶
// 1. 统计当前位每个数字出现次数
for (int i = 0; i < n; i++) {
int d = getDigit(arr[i], digit);
count[d]++;
}
// 2. 计算前缀和
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 3. 从后向前放置(保证稳定性)
for (int i = n - 1; i >= 0; i--) {
int d = getDigit(arr[i], digit);
output[count[d] - 1] = arr[i];
count[d]--;
}
// 4. 复制回原数组
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
}
free(output);
}
🔟 桶排序 (Bucket Sort)
🔍 算法原理
markdown
分桶思想:
1. 根据数据范围创建若干个桶
2. 将元素均匀分配到对应桶中
3. 对每个桶内排序(可用任意排序算法)
4. 按顺序合并所有桶
适用:数据均匀分布,如[0,1)之间的浮点数
💻 C语言实现
c
/**
* 桶排序(简化版:适用于0~max的整数)
* @param arr 待排序数组
* @param n 数组长度
* @param max 数据最大值
* @param bucketCount 桶的数量
* 时间复杂度: O(n+k) 平均 空间复杂度: O(n+k) 稳定: 取决于桶内排序
*/
void bucketSort(int arr[], int n, int max, int bucketCount) {
if (n <= 1) return;
// 1. 创建桶(用链表数组模拟)
typedef struct Node {
int val;
struct Node *next;
} Node;
Node **buckets = (Node**)calloc(bucketCount, sizeof(Node*));
// 2. 分配元素到桶中
for (int i = 0; i < n; i++) {
int bucketIdx = (arr[i] * bucketCount) / (max + 1); // 均匀映射
Node *newNode = (Node*)malloc(sizeof(Node));
newNode->val = arr[i];
newNode->next = buckets[bucketIdx]; // 头插法
buckets[bucketIdx] = newNode;
}
// 3. 对每个桶排序 + 收集结果
int idx = 0;
for (int i = 0; i < bucketCount; i++) {
// 收集桶内元素到临时数组
Node *curr = buckets[i];
int bucketSize = 0;
while (curr) {
bucketSize++;
curr = curr->next;
}
if (bucketSize == 0) continue;
int *bucketArr = (int*)malloc(bucketSize * sizeof(int));
curr = buckets[i];
for (int j = 0; j < bucketSize; j++) {
bucketArr[j] = curr->val;
curr = curr->next;
}
// 桶内用插入排序(小数组效率高)
for (int j = 1; j < bucketSize; j++) {
int key = bucketArr[j];
int k = j - 1;
while (k >= 0 && bucketArr[k] > key) {
bucketArr[k + 1] = bucketArr[k];
k--;
}
bucketArr[k + 1] = key;
}
// 收集到原数组
for (int j = 0; j < bucketSize; j++) {
arr[idx++] = bucketArr[j];
}
free(bucketArr);
// 释放链表
curr = buckets[i];
while (curr) {
Node *temp = curr;
curr = curr->next;
free(temp);
}
}
free(buckets);
}
// 简化版:每个值一个桶(退化为计数排序)
void bucketSortSimple(int arr[], int n, int max) {
int *bucket = (int*)calloc(max + 1, sizeof(int));
// 统计每个值出现次数
for (int i = 0; i < n; i++) {
bucket[arr[i]]++;
}
// 按顺序输出
int idx = 0;
for (int i = 0; i <= max; i++) {
while (bucket[i] > 0) {
arr[idx++] = i;
bucket[i]--;
}
}
free(bucket);
}
📊 算法复杂度对比表
| 算法 | 平均时间 | 最好时间 | 最坏时间 | 空间 | 稳定 | 适用场景 |
|---|---|---|---|---|---|---|
| 冒泡排序 | 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^1.3) | O(nlogn) | O(n²) | O(1) | ✗ | 中等数据 |
| 归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | ✓ | 大数据、要求稳定 |
| 快速排序 | O(nlogn) | O(nlogn) | O(n²) | O(logn) | ✗ | 大数据、通用首选 |
| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | ✗ | 大数据、空间敏感 |
| 计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | ✓ | 整数、范围小 |
| 基数排序 | O(d(n+k)) | O(d(n+k)) | O(d(n+k)) | O(n+k) | ✓ | 整数、位数少 |
| 桶排序 | O(n+k) | O(n+k) | O(n²) | O(n+k) | ✓ | 均匀分布数据 |
💡 k: 数据范围, d: 最大位数, n: 数据量
🎯 使用建议(面试/实战)
c
// 1. 小数据量 (<50): 插入排序最简单高效
if (n < 50) {
insertionSort(arr, n);
}
// 2. 通用场景: 快速排序(注意随机化避免最坏情况)
else {
quickSort(arr, n);
}
// 3. 要求稳定排序: 归并排序
// 4. 空间敏感: 堆排序
// 5. 整数且范围小: 计数排序
// 6. 整数位数少: 基数排序
// 7. 数据均匀分布: 桶排序
🔧 完整测试框架
c
// 测试函数:验证排序正确性
int isSorted(int arr[], int n) {
for (int i = 1; i < n; i++) {
if (arr[i-1] > arr[i]) return 0;
}
return 1;
}
// 性能测试模板
void testSort(void (*sortFunc)(int[], int), char *name, int arr[], int n) {
// 复制数组(避免原数组被修改影响其他测试)
int *testArr = (int*)malloc(n * sizeof(int));
memcpy(testArr, arr, n * sizeof(int));
clock_t start = clock();
sortFunc(testArr, n);
clock_t end = clock();
printf("%-12s: 耗时%.3fms, 正确:%s\n",
name,
(double)(end - start) / CLOCKS_PER_SEC * 1000,
isSorted(testArr, n) ? "✓" : "✗");
free(testArr);
}
// 主函数示例
int main() {
const int N = 1000;
int arr[N];
generateRandomArray(arr, N, 0, 10000);
printf("=== 排序算法性能测试 (n=%d) ===\n", N);
testSort(bubbleSort, "Bubble", arr, N);
testSort(selectionSort, "Selection", arr, N);
testSort(insertionSort, "Insertion", arr, N);
testSort(mergeSort, "Merge", arr, N);
testSort(quickSort, "Quick", arr, N);
testSort(heapSort, "Heap", arr, N);
testSort(shellSort, "Shell", arr, N);
return 0;
}
📝 学习建议
- 先理解原理:看动图 + 手动模拟小数组
- 再写代码:从插入/冒泡开始,逐步挑战快排/归并
- 调试技巧:加打印语句观察每轮变化
- 对比记忆:画表格对比各算法特点
- 实战应用:根据数据特点选择合适算法
✨ 记住:没有最好的算法,只有最适合的算法!
🎁 Bonus: 所有代码已整理成单个.c文件,可直接编译运行:
bashgcc -o sort sort.c -Wall -O2 ./sort
有任何问题欢迎随时提问!🚀