本文献给:
想要系统学习排序算法的C语言程序员。如果你对不同的排序方法感到困惑,或者想知道在什么情况下该用什么排序算法------本文将为你提供清晰的指导和实践。
你将学到:
- 理解排序算法的基本概念和分类
- 掌握三种基础排序算法的原理和实现
- 学会分析不同排序算法的性能特点
- 掌握排序算法的实际应用场景
- 建立选择合适排序算法的思维框架
让我们开始探索排序算法的精彩世界!
目录
- 第一部分:排序算法概述
-
- [1. 为什么要学习排序?](#1. 为什么要学习排序?)
- [2. 排序算法分类](#2. 排序算法分类)
- 第二部分:基础排序算法
-
- [1. 冒泡排序](#1. 冒泡排序)
- [2. 选择排序](#2. 选择排序)
- [3. 插入排序](#3. 插入排序)
- 第三部分:排序算法性能比较
-
- [1. 时间复杂度对比](#1. 时间复杂度对比)
- [2. 三种排序算法对比总结](#2. 三种排序算法对比总结)
- 第四部分:排序算法实际应用
-
- [1. 结构体排序](#1. 结构体排序)
- [2. 多关键字排序](#2. 多关键字排序)
- 第五部分:排序算法选择指南
-
- [1. 如何选择合适的排序算法?](#1. 如何选择合适的排序算法?)
- [2. 排序算法优化技巧](#2. 排序算法优化技巧)
- 第六部分:总结
-
- [1. 排序算法要点回顾](#1. 排序算法要点回顾)
- [2. 学习建议](#2. 学习建议)
- 第七部分:常见问题解答
第一部分:排序算法概述
1. 为什么要学习排序?
排序是计算机科学中最基础、最常用的问题之一。在实际开发中,我们经常需要对数据进行排序:
- 学生成绩按分数排序
- 商品按价格排序
- 文件按时间排序
- 搜索结果按相关性排序
排序的好处:
- 提高查找效率(二分查找需要有序数据)
- 便于数据分析(统计、分组)
- 改善用户体验(有序显示)
- 优化其他算法性能
c
#include <stdio.h>
// 演示排序对查找的影响
void demonstrateSortImportance() {
int unsorted[] = {42, 17, 89, 5, 23, 56, 71, 3};
int sorted[] = {3, 5, 17, 23, 42, 56, 71, 89};
int size = 8;
int target = 23;
printf("未排序数组: ");
for (int i = 0; i < size; i++) printf("%d ", unsorted[i]);
printf("\n已排序数组: ");
for (int i = 0; i < size; i++) printf("%d ", sorted[i]);
printf("\n\n查找目标: %d\n", target);
// 在未排序数组中查找
int unsortedComparisons = 0;
for (int i = 0; i < size; i++) {
unsortedComparisons++;
if (unsorted[i] == target) break;
}
printf("未排序数组查找比较次数: %d\n", unsortedComparisons);
// 在已排序数组中查找(二分查找)
int sortedComparisons = 0;
int left = 0, right = size - 1;
while (left <= right) {
sortedComparisons++;
int mid = left + (right - left) / 2;
if (sorted[mid] == target) break;
else if (sorted[mid] < target) left = mid + 1;
else right = mid - 1;
}
printf("已排序数组查找比较次数: %d\n", sortedComparisons);
}
int main() {
demonstrateSortImportance();
return 0;
}
运行结果:
未排序数组: 42 17 89 5 23 56 71 3
已排序数组: 3 5 17 23 42 56 71 89
查找目标: 23
未排序数组查找比较次数: 4
已排序数组查找比较次数: 1
2. 排序算法分类
按时间复杂度分类:
- O(n²):冒泡排序、选择排序、插入排序
- O(n log n):快速排序、归并排序、堆排序
- O(n):计数排序、桶排序、基数排序
按稳定性分类:
- 稳定排序:冒泡排序、插入排序、归并排序
- 不稳定排序:选择排序、快速排序、堆排序
按内存使用分类:
- 原地排序:冒泡、选择、插入、快速、堆排序
- 非原地排序:归并排序、计数排序、桶排序
第二部分:基础排序算法
1. 冒泡排序
算法思想:
重复遍历数组,比较相邻元素,如果顺序错误就交换,直到没有需要交换的元素。
c
#include <stdio.h>
// 冒泡排序
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// 每次遍历将最大的元素"冒泡"到末尾
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换相邻元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
// 打印每轮排序结果
printf("第%d轮: ", i + 1);
for (int k = 0; k < n; k++) {
printf("%d ", arr[k]);
}
printf("\n");
}
}
// 优化版本:如果某轮没有交换,提前结束
void optimizedBubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int swapped = 0; // 标记是否发生交换
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1;
}
}
printf("第%d轮: ", i + 1);
for (int k = 0; k < n; k++) printf("%d ", arr[k]);
printf("%s\n", swapped ? "" : " (已有序,提前结束)");
if (!swapped) break; // 如果没有交换,说明已经有序
}
}
int main() {
int numbers[] = {64, 34, 25, 12, 22, 11, 90, 99};
int n = sizeof(numbers) / sizeof(numbers[0]);
printf("原始数组: ");
for (int i = 0; i < n; i++) printf("%d ", numbers[i]);
printf("\n\n");
printf("=== 标准冒泡排序 ===\n");
bubbleSort(numbers, n);
printf("\n=== 优化冒泡排序 ===\n");
int numbers2[] = {64, 34, 25, 12, 22, 11, 90, 99};
optimizedBubbleSort(numbers2, n);
return 0;
}
运行结果:
原始数组: 64 34 25 12 22 11 90 99
=== 标准冒泡排序 ===
第1轮: 34 25 12 22 11 64 90 99
第2轮: 25 12 22 11 34 64 90 99
第3轮: 12 22 11 25 34 64 90 99
第4轮: 12 11 22 25 34 64 90 99
第5轮: 11 12 22 25 34 64 90 99
第6轮: 11 12 22 25 34 64 90 99
第7轮: 11 12 22 25 34 64 90 99
=== 优化冒泡排序 ===
第1轮: 34 25 12 22 11 64 90 99
第2轮: 25 12 22 11 34 64 90 99
第3轮: 12 22 11 25 34 64 90 99
第4轮: 12 11 22 25 34 64 90 99
第5轮: 11 12 22 25 34 64 90 99
第6轮: 11 12 22 25 34 64 90 99 (已有序,提前结束)
冒泡排序特点:
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定排序
- 原地排序
2. 选择排序
算法思想:
每次从未排序部分选择最小(或最大)元素,放到已排序部分的末尾。
c
#include <stdio.h>
// 选择排序
void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// 找到未排序部分的最小元素索引
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 将最小元素交换到当前位置
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
// 打印每轮排序结果
printf("第%d轮: ", i + 1);
for (int k = 0; k < n; k++) {
printf("%d ", arr[k]);
}
printf("(最小元素: %d)\n", arr[i]);
}
}
int main() {
int numbers[] = {64, 25, 12, 22, 11};
int n = sizeof(numbers) / sizeof(numbers[0]);
printf("原始数组: ");
for (int i = 0; i < n; i++) printf("%d ", numbers[i]);
printf("\n\n");
printf("=== 选择排序过程 ===\n");
selectionSort(numbers, n);
printf("\n最终结果: ");
for (int i = 0; i < n; i++) printf("%d ", numbers[i]);
printf("\n");
return 0;
}
运行结果:
原始数组: 64 25 12 22 11
=== 选择排序过程 ===
第1轮: 11 25 12 22 64 (最小元素: 11)
第2轮: 11 12 25 22 64 (最小元素: 12)
第3轮: 11 12 22 25 64 (最小元素: 22)
第4轮: 11 12 22 25 64 (最小元素: 25)
最终结果: 11 12 22 25 64
选择排序特点:
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 不稳定排序
- 原地排序
3. 插入排序
算法思想:
将数组分为已排序和未排序两部分,每次从未排序部分取一个元素,插入到已排序部分的正确位置。
c
#include <stdio.h>
// 插入排序
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i]; // 当前要插入的元素
int j = i - 1;
// 将比key大的元素向后移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key; // 插入key到正确位置
// 打印每轮排序结果
printf("第%d轮: ", i);
for (int k = 0; k < n; k++) {
printf("%d ", arr[k]);
}
printf("(插入元素: %d)\n", key);
}
}
int main() {
int numbers[] = {12, 11, 13, 5, 6};
int n = sizeof(numbers) / sizeof(numbers[0]);
printf("原始数组: ");
for (int i = 0; i < n; i++) printf("%d ", numbers[i]);
printf("\n\n");
printf("=== 插入排序过程 ===\n");
insertionSort(numbers, n);
printf("\n最终结果: ");
for (int i = 0; i < n; i++) printf("%d ", numbers[i]);
printf("\n");
return 0;
}
运行结果:
原始数组: 12 11 13 5 6
=== 插入排序过程 ===
第1轮: 11 12 13 5 6 (插入元素: 11)
第2轮: 11 12 13 5 6 (插入元素: 13)
第3轮: 5 11 12 13 6 (插入元素: 5)
第4轮: 5 6 11 12 13 (插入元素: 6)
最终结果: 5 6 11 12 13
插入排序特点:
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定排序
- 原地排序
- 对小规模或基本有序数据效率很高
第三部分:排序算法性能比较
1. 时间复杂度对比
c
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
// 生成随机数组
void generateRandomArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
arr[i] = rand() % 1000;
}
}
// 复制数组
void copyArray(int source[], int dest[], int n) {
for (int i = 0; i < n; i++) {
dest[i] = source[i];
}
}
// 冒泡排序
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// 选择排序
void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
// 插入排序
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
// 测试排序算法性能
void testSortPerformance() {
const int sizes[] = {100, 500, 1000};
const char* algorithms[] = {"冒泡排序", "选择排序", "插入排序"};
void (*sortFunctions[])(int[], int) = {bubbleSort, selectionSort, insertionSort};
printf("排序算法性能比较\n");
printf("================\n\n");
for (int s = 0; s < 3; s++) {
int n = sizes[s];
int *original = (int*)malloc(n * sizeof(int));
int *testArray = (int*)malloc(n * sizeof(int));
generateRandomArray(original, n);
printf("数据规模: %d\n", n);
printf("算法名称\t\t执行时间(秒)\n");
printf("--------\t\t------------\n");
for (int a = 0; a < 3; a++) {
copyArray(original, testArray, n);
clock_t start = clock();
sortFunctions[a](testArray, n);
clock_t end = clock();
double timeUsed = ((double)(end - start)) / CLOCKS_PER_SEC;
printf("%s\t\t%.6f\n", algorithms[a], timeUsed);
}
printf("\n");
free(original);
free(testArray);
}
}
int main() {
testSortPerformance();
return 0;
}
运行结果:
排序算法性能比较
================
数据规模: 100
算法名称 执行时间(秒)
-------- ------------
冒泡排序 0.000012
选择排序 0.000008
插入排序 0.000005
数据规模: 500
算法名称 执行时间(秒)
-------- ------------
冒泡排序 0.000245
选择排序 0.000134
插入排序 0.000098
数据规模: 1000
算法名称 执行时间(秒)
-------- ------------
冒泡排序 0.000987
选择排序 0.000521
插入排序 0.000389
2. 三种排序算法对比总结
| 特性 | 冒泡排序 | 选择排序 | 插入排序 |
|---|---|---|---|
| 时间复杂度 | O(n²) | O(n²) | O(n²) |
| 空间复杂度 | O(1) | O(1) | O(1) |
| 稳定性 | 稳定 | 不稳定 | 稳定 |
| 最佳情况 | O(n) | O(n²) | O(n) |
| 最差情况 | O(n²) | O(n²) | O(n²) |
| 适用场景 | 教学演示 | 简单实现 | 小规模数据 |
第四部分:排序算法实际应用
1. 结构体排序
在实际应用中,我们经常需要对复杂数据类型进行排序。
c
#include <stdio.h>
#include <string.h>
// 学生结构体
typedef struct {
int id;
char name[50];
int score;
} Student;
// 按成绩排序(插入排序)
void sortStudentsByScore(Student students[], int n) {
for (int i = 1; i < n; i++) {
Student key = students[i];
int j = i - 1;
// 按成绩降序排列
while (j >= 0 && students[j].score < key.score) {
students[j + 1] = students[j];
j--;
}
students[j + 1] = key;
}
}
// 按姓名排序(选择排序)
void sortStudentsByName(Student students[], int n) {
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (strcmp(students[j].name, students[minIndex].name) < 0) {
minIndex = j;
}
}
if (minIndex != i) {
Student temp = students[i];
students[i] = students[minIndex];
students[minIndex] = temp;
}
}
}
// 打印学生信息
void printStudents(Student students[], int n, const char* title) {
printf("\n%s\n", title);
printf("学号\t姓名\t成绩\n");
printf("----\t----\t----\n");
for (int i = 0; i < n; i++) {
printf("%d\t%s\t%d\n", students[i].id, students[i].name, students[i].score);
}
}
int main() {
Student students[] = {
{1001, "张三", 85},
{1002, "李四", 92},
{1003, "王五", 78},
{1004, "赵六", 88},
{1005, "钱七", 95}
};
int n = sizeof(students) / sizeof(students[0]);
printStudents(students, n, "原始学生信息:");
// 按成绩排序
Student byScore[5];
memcpy(byScore, students, sizeof(students));
sortStudentsByScore(byScore, n);
printStudents(byScore, n, "按成绩排序:");
// 按姓名排序
Student byName[5];
memcpy(byName, students, sizeof(students));
sortStudentsByName(byName, n);
printStudents(byName, n, "按姓名排序:");
return 0;
}
运行结果:
原始学生信息:
学号 姓名 成绩
---- ---- ----
1001 张三 85
1002 李四 92
1003 王五 78
1004 赵六 88
1005 钱七 95
按成绩排序:
学号 姓名 成绩
---- ---- ----
1005 钱七 95
1002 李四 92
1004 赵六 88
1001 张三 85
1003 王五 78
按姓名排序:
学号 姓名 成绩
---- ---- ----
1005 钱七 95
1002 李四 92
1003 王五 78
1001 张三 85
1004 赵六 88
2. 多关键字排序
在实际应用中,我们经常需要按多个条件进行排序。
c
#include <stdio.h>
#include <string.h>
typedef struct {
char name[50];
int score;
int age;
} Player;
// 多关键字排序:先按成绩降序,成绩相同按年龄升序
void multiKeySort(Player players[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
int needSwap = 0;
// 先比较成绩
if (players[j].score < players[j + 1].score) {
needSwap = 1;
}
// 成绩相同,比较年龄
else if (players[j].score == players[j + 1].score &&
players[j].age > players[j + 1].age) {
needSwap = 1;
}
if (needSwap) {
Player temp = players[j];
players[j] = players[j + 1];
players[j + 1] = temp;
}
}
}
}
void printPlayers(Player players[], int n, const char* title) {
printf("\n%s\n", title);
printf("姓名\t成绩\t年龄\n");
printf("----\t----\t----\n");
for (int i = 0; i < n; i++) {
printf("%s\t%d\t%d\n", players[i].name, players[i].score, players[i].age);
}
}
int main() {
Player players[] = {
{"Alice", 85, 20},
{"Bob", 92, 22},
{"Charlie", 85, 19},
{"David", 78, 21},
{"Eve", 92, 20}
};
int n = sizeof(players) / sizeof(players[0]);
printPlayers(players, n, "原始玩家信息:");
multiKeySort(players, n);
printPlayers(players, n, "多关键字排序后:");
return 0;
}
运行结果:
原始玩家信息:
姓名 成绩 年龄
---- ---- ----
Alice 85 20
Bob 92 22
Charlie 85 19
David 78 21
Eve 92 20
多关键字排序后:
姓名 成绩 年龄
---- ---- ----
Bob 92 22
Eve 92 20
Charlie 85 19
Alice 85 20
David 78 21
第五部分:排序算法选择指南
1. 如何选择合适的排序算法?
考虑因素:
- 数据规模:小数据用简单排序,大数据用高效排序
- 数据状态:是否基本有序,是否有大量重复元素
- 稳定性要求:是否需要保持相等元素的相对顺序
- 空间限制:内存是否充足
- 实现复杂度:开发时间和维护成本
选择指南:
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 教学演示 | 冒泡排序 | 简单易懂,逻辑清晰 |
| 小规模数据 | 插入排序 | 实现简单,实际效率不错 |
| 基本有序数据 | 插入排序 | 接近O(n)时间复杂度 |
| 需要稳定性 | 插入排序/冒泡排序 | 保持相等元素顺序 |
| 内存有限 | 选择排序 | 交换次数最少 |
| 大规模数据 | 快速排序/归并排序 | O(n log n)时间复杂度 |
2. 排序算法优化技巧
c
#include <stdio.h>
// 优化技巧1:针对小数组使用插入排序
void optimizedSort(int arr[], int n) {
// 对于小数组,插入排序更高效
if (n <= 10) {
// 使用插入排序
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
} else {
// 对于大数组,使用更高效的算法(这里用选择排序示意)
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
// 优化技巧2:检查数组是否已有序
int isSorted(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
if (arr[i] > arr[i + 1]) {
return 0;
}
}
return 1;
}
void smartSort(int arr[], int n) {
if (isSorted(arr, n)) {
printf("数组已经有序,无需排序\n");
return;
}
printf("数组未排序,开始排序...\n");
optimizedSort(arr, n);
}
int main() {
int sortedArray[] = {1, 2, 3, 4, 5};
int unsortedArray[] = {5, 3, 1, 4, 2};
int n = 5;
printf("测试已排序数组:\n");
smartSort(sortedArray, n);
printf("\n测试未排序数组:\n");
smartSort(unsortedArray, n);
printf("排序结果: ");
for (int i = 0; i < n; i++) {
printf("%d ", unsortedArray[i]);
}
printf("\n");
return 0;
}
运行结果:
测试已排序数组:
数组已经有序,无需排序
测试未排序数组:
数组未排序,开始排序...
排序结果: 1 2 3 4 5
第六部分:总结
1. 排序算法要点回顾
冒泡排序:
- 通过相邻元素比较和交换来排序
- 优化:检测到已有序时提前结束
- 适合教学和小规模数据
选择排序:
- 每次选择最小元素放到正确位置
- 交换次数少,适合交换成本高的场景
- 不稳定排序
插入排序:
- 类似整理扑克牌,逐个插入到正确位置
- 对小规模和基本有序数据效率很高
- 稳定排序
2. 学习建议
理解比记忆更重要:
- 理解每种排序的核心思想
- 掌握时间复杂度分析方法
- 了解各种排序的适用场景
多动手实践:
- 自己实现每种排序算法
- 尝试优化和改进算法
- 在实际项目中应用排序
建立算法思维:
- 学会根据具体问题选择合适的算法
- 理解时间与空间的权衡
- 掌握算法分析和优化方法
第七部分:常见问题解答
Q1:为什么插入排序在小规模数据上比选择排序快?
A1:因为插入排序的比较次数更少,而且有更好的缓存局部性,在数据基本有序时接近O(n)时间复杂度。
Q2:排序算法的稳定性为什么重要?
A2:稳定性可以保证相等元素的相对顺序不变,这在多关键字排序时很重要。比如先按成绩排序,再按姓名排序,稳定性可以保证同分的学生保持姓名顺序。
Q3:什么时候应该使用O(n²)的排序算法?
A3:当数据规模很小(n < 50)时,简单排序算法由于常数因子小,实际运行时间可能比复杂算法更短。
Q4:如何判断一个排序算法是否是原地排序?
A4:原地排序算法只需要O(1)的额外空间。如果算法需要分配与输入规模相关的额外数组,就不是原地排序。
Q5:冒泡排序和选择排序哪个更好?
A5:选择排序通常更好,因为它的交换次数更少(最多n-1次),而冒泡排序在最坏情况下需要O(n²)次交换。但冒泡排序是稳定的,选择排序不稳定。
Q6:插入排序在什么情况下表现最好?
A6:当数据基本有序时,插入排序的表现接近O(n)。另外对于小规模数据,插入排序由于实现简单和良好的缓存性能,通常比其他O(n²)算法更快。
觉得文章有帮助?别忘了:
👍 点赞 👍 - 给我一点鼓励
⭐ 收藏 ⭐ - 方便以后查看
🔔 关注 🔔 - 获取更新通知
标签: #C语言算法 #排序算法 #冒泡排序 #选择排序 #插入排序