【C语言实战(39)】C语言排序算法实战:冒泡、选择与插入的对决
目录
- 一、冒泡排序(Bubble Sort)
- 1.1 冒泡排序基本原理
- 1.2 冒泡排序代码实现
- 1.3 冒泡排序实战:对整型数组排序,输出排序前后结果对比
- 二、选择排序(Selection Sort)
- 2.1 选择排序基本原理
- 2.2 选择排序代码实现
- 2.3 选择排序实战:对结构体数组(学生成绩)按成绩排序
- 三、插入排序(Insertion Sort)
- 3.1 插入排序基本原理
- 3.2 插入排序代码实现
- 四、排序算法对比实战:测试三种排序算法对同一数组的执行效率
- 4.1 统计比较次数与交换次数
- 4.2 分析结果得出结论
一、冒泡排序(Bubble Sort)
1.1 冒泡排序基本原理
冒泡排序是一种简单的交换排序算法 ,它的基本思想是通过对待排序序列从前向后(或从后向前),依次比较相邻元素的排序码,若发现逆序则交换,使排序码较小(或较大)的元素逐渐从后部(或前部)移向前部(或后部),就像水底下的气泡一样逐渐向上冒,所以叫冒泡排序。
具体来说,它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从 Z 到 A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢 “浮” 到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名 “冒泡排序”。
例如,对于数组[5, 3, 8, 2, 9],冒泡排序的第一轮比较过程如下:
- 比较5和3,因为5 > 3,所以交换它们,数组变为[3, 5, 8, 2, 9];
- 比较5和8,因为5 < 8,所以不交换,数组仍为[3, 5, 8, 2, 9];
- 比较8和2,因为8 > 2,所以交换它们,数组变为[3, 5, 2, 8, 9];
- 比较8和9,因为8 < 9,所以不交换,数组仍为[3, 5, 2, 8, 9]。
第一轮结束后,最大的数9已经 “冒泡” 到了数组的末尾。接下来进行第二轮、第三轮…… 直到整个数组有序。
1.2 冒泡排序代码实现
// 基础版冒泡排序
void bubbleSort(int arr[], int n) {int i, j;for (i = 0; i < n - 1; i++) { // 外层循环控制排序轮数,n个元素需要n-1轮for (j = 0; j < n - i - 1; j++) { // 内层循环控制每一轮比较次数,每一轮会把当前最大的数“冒泡”到末尾,所以下一轮比较次数减1if (arr[j] > arr[j + 1]) { // 如果前一个元素大于后一个元素,则交换int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}
上述基础版冒泡排序代码,无论数组初始状态如何,都需要完整执行n-1轮比较。但如果数组本身已经有序,这些比较就是多余的。为了避免这种无效循环,可以添加一个标志位来优化。
// 优化版冒泡排序
void optimizedBubbleSort(int arr[], int n) {int i, j;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]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;swapped = 1; // 标记发生了交换}}if (!swapped) break; // 如果一轮中没有发生交换,说明数组已排序,提前结束}
}
在优化版中,添加了swapped标志位。每一轮开始时,将swapped置为0。在内层循环中,如果发生了元素交换,就将swapped置为1。当一轮结束后,如果swapped仍然为0,说明这一轮没有发生交换,数组已经有序,直接跳出外层循环,从而避免了不必要的比较。
1.3 冒泡排序实战:对整型数组排序,输出排序前后结果对比
#include <stdio.h>// 优化版冒泡排序
void optimizedBubbleSort(int arr[], int n) {int i, j;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]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;swapped = 1;}}if (!swapped) break;}
}// 打印数组函数
void printArray(int arr[], int n) {int i;for (i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");
}int main() {int arr[] = {64, 34, 25, 12, 22, 11, 90};int n = sizeof(arr) / sizeof(arr[0]);printf("排序前的数组: ");printArray(arr, n);optimizedBubbleSort(arr, n);printf("排序后的数组: ");printArray(arr, n);return 0;
}
运行上述代码,输出结果如下:
排序前的数组: 64 34 25 12 22 11 90
排序后的数组: 11 12 22 25 34 64 90
从结果可以看出,经过冒泡排序后,数组已经按照升序排列。通过实际的代码运行,验证了冒泡排序算法对整型数组排序的有效性。
二、选择排序(Selection Sort)
2.1 选择排序基本原理
选择排序是一种简单直观的排序算法 ,它的工作原理是将数组分为已排序和未排序两部分。初始时,已排序部分为空,未排序部分是整个数组。每一轮选择排序会在未排序部分中找到最小(升序排序时)或最大(降序排序时)的元素,然后将其与未排序部分的第一个元素交换位置,这样已排序部分就增加了一个元素,未排序部分减少一个元素。不断重复这个过程,直到未排序部分为空,整个数组就完成了排序。
例如,对于数组[5, 3, 8, 2, 9],升序选择排序的第一轮过程如下:
- 在整个数组[5, 3, 8, 2, 9]中找到最小的元素2;
- 将2与数组的第一个元素5交换位置,数组变为[2, 3, 8, 5, 9],此时已排序部分为[2],未排序部分为[3, 8, 5, 9]。
接下来,在未排序部分[3, 8, 5, 9]中找到最小元素3,将其与未排序部分的第一个元素3(这里就是它本身,无需交换),已排序部分变为[2, 3],未排序部分变为[8, 5, 9]。以此类推,直到整个数组有序。
2.2 选择排序代码实现
// 选择排序升序版本
void selectionSortAscending(int arr[], int n) {int i, j, minIndex;for (i = 0; i < n - 1; i++) {minIndex = i; // 假设当前位置的元素是最小的for (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;}}
}// 选择排序降序版本
void selectionSortDescending(int arr[], int n) {int i, j, maxIndex;for (i = 0; i < n - 1; i++) {maxIndex = i; // 假设当前位置的元素是最大的for (j = i + 1; j < n; j++) {if (arr[j] > arr[maxIndex]) {maxIndex = j; // 更新最大元素的索引}}if (maxIndex != i) { // 如果最大元素不是当前位置的元素,则交换int temp = arr[i];arr[i] = arr[maxIndex];arr[maxIndex] = temp;}}
}
在升序版本中,外层循环控制已排序部分的边界,每一轮确定一个最小元素并将其放置到正确位置。内层循环用于在未排序部分中寻找最小元素的索引。如果找到的最小元素索引与当前假设的最小元素索引不同,就交换这两个元素。降序版本与升序版本类似,只是在寻找最大元素时进行比较和交换。
2.3 选择排序实战:对结构体数组(学生成绩)按成绩排序
#include <stdio.h>// 定义学生结构体
typedef struct {char name[50];int score;
} Student;// 选择排序升序版本,对学生结构体数组按成绩排序
void selectionSortStudentAscending(Student arr[], int n) {int i, j, minIndex;for (i = 0; i < n - 1; i++) {minIndex = i;for (j = i + 1; j < n; j++) {if (arr[j].score < arr[minIndex].score) {minIndex = j;}}if (minIndex != i) {Student temp = arr[i];arr[i] = arr[minIndex];arr[minIndex] = temp;}}
}// 打印学生结构体数组
void printStudentArray(Student arr[], int n) {int i;for (i = 0; i < n; i++) {printf("姓名: %s, 成绩: %d\n", arr[i].name, arr[i].score);}
}int main() {Student students[] = {{"张三", 85},{"李四", 90},{"王五", 78},{"赵六", 88}};int n = sizeof(students) / sizeof(students[0]);printf("排序前的学生成绩:\n");printStudentArray(students, n);selectionSortStudentAscending(students, n);printf("\n排序后的学生成绩:\n");printStudentArray(students, n);return 0;
}
上述代码定义了一个Student结构体,包含学生姓名和成绩。selectionSortStudentAscending函数实现了对Student结构体数组按成绩升序排序的选择排序算法。printStudentArray函数用于打印学生结构体数组的内容。在main函数中,创建了一个学生结构体数组,并调用排序和打印函数,展示排序前后的结果。
三、插入排序(Insertion Sort)
3.1 插入排序基本原理
插入排序是一种简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入 。插入排序就像我们平时玩扑克牌时的理牌过程,假设手中已经有一部分牌是按顺序排列好的(已排序区间),每次从剩下的牌中拿起一张(未排序元素),然后将这张牌插入到已排序好的牌中的合适位置,使得插入后手中的牌仍然是有序的。
具体来说,插入排序将数组分为已排序和未排序两部分。初始时,已排序部分只有第一个元素,未排序部分是从第二个元素开始的剩余元素。然后,依次从未排序部分取出元素,将其插入到已排序部分的正确位置。在插入过程中,从已排序部分的最后一个元素开始向前比较,如果已排序元素大于待插入元素,则将已排序元素向后移动一位,为待插入元素腾出位置,直到找到一个已排序元素小于或等于待插入元素,此时将待插入元素插入到该位置。重复这个过程,直到未排序部分为空,整个数组就完成了排序。
例如,对于数组[5, 3, 8, 2, 9],插入排序的过程如下:
- 初始时,已排序部分为[5],未排序部分为[3, 8, 2, 9]。
- 取出未排序部分的第一个元素3,与已排序部分的5比较,因为3 < 5,所以将5向后移动一位,把3插入到5原来的位置,此时已排序部分变为[3, 5],未排序部分变为[8, 2, 9]。
- 取出未排序部分的第一个元素8,与已排序部分的5比较,因为8 > 5,所以直接将8插入到5的后面,已排序部分变为[3, 5, 8],未排序部分变为[2, 9]。
- 取出未排序部分的第一个元素2,依次与已排序部分的8、5、3比较,因为2都小于它们,所以将3、5、8依次向后移动一位,把2插入到最前面,已排序部分变为[2, 3, 5, 8],未排序部分变为[9]。
- 取出未排序部分的第一个元素9,与已排序部分的8比较,因为9 > 8,所以直接将9插入到8的后面,已排序部分变为[2, 3, 5, 8, 9],未排序部分为空,排序完成。
3.2 插入排序代码实现
// 直接插入排序
void insertionSort(int arr[], int n) {int i, j, key;for (i = 1; i < n; i++) {key = arr[i]; // 待插入的元素j = i - 1; // 已排序部分的最后一个元素的索引// 将已排序部分中大于key的元素向后移动一位while (j >= 0 && arr[j] > key) {arr[j + 1] = arr[j];j = j - 1;}arr[j + 1] = key; // 将key插入到正确的位置}
}
上述代码中,外层for循环从第二个元素开始,依次将未排序部分的元素插入到已排序部分。内层while循环用于在已排序部分中寻找插入位置,将大于key的元素向后移动。当找到合适位置或已遍历完已排序部分时,将key插入到j + 1的位置。
四、排序算法对比实战:测试三种排序算法对同一数组的执行效率
4.1 统计比较次数与交换次数
为了对比冒泡排序、选择排序和插入排序这三种排序算法的执行效率,我们在代码中添加统计比较次数与交换次数的逻辑。下面是实现这一功能的完整代码:
#include <stdio.h>
#include <time.h>// 冒泡排序
void bubbleSort(int arr[], int n, int *comparisons, int *swaps) {int i, j;*comparisons = 0;*swaps = 0;for (i = 0; i < n - 1; i++) {for (j = 0; j < n - i - 1; j++) {(*comparisons)++;if (arr[j] > arr[j + 1]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;(*swaps)++;}}}
}// 选择排序
void selectionSort(int arr[], int n, int *comparisons, int *swaps) {int i, j, minIndex;*comparisons = 0;*swaps = 0;for (i = 0; i < n - 1; i++) {minIndex = i;for (j = i + 1; j < n; j++) {(*comparisons)++;if (arr[j] < arr[minIndex]) {minIndex = j;}}if (minIndex != i) {int temp = arr[i];arr[i] = arr[minIndex];arr[minIndex] = temp;(*swaps)++;}}
}// 插入排序
void insertionSort(int arr[], int n, int *comparisons, int *swaps) {int i, j, key;*comparisons = 0;*swaps = 0;for (i = 1; i < n; i++) {key = arr[i];j = i - 1;while (j >= 0) {(*comparisons)++;if (arr[j] > key) {arr[j + 1] = arr[j];j = j - 1;(*swaps)++;} else {break;}}arr[j + 1] = key;}
}// 打印数组函数
void printArray(int arr[], int n) {int i;for (i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");
}int main() {int arr[] = {64, 34, 25, 12, 22, 11, 90};int n = sizeof(arr) / sizeof(arr[0]);int comparisons, swaps;clock_t start, end;double cpu_time_used;// 测试冒泡排序start = clock();bubbleSort(arr, n, &comparisons, &swaps);end = clock();cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;printf("冒泡排序结果: ");printArray(arr, n);printf("冒泡排序比较次数: %d\n", comparisons);printf("冒泡排序交换次数: %d\n", swaps);printf("冒泡排序运行时间: %f 秒\n\n", cpu_time_used);// 恢复原始数组int originalArr[] = {64, 34, 25, 12, 22, 11, 90};for (int i = 0; i < n; i++) {arr[i] = originalArr[i];}// 测试选择排序start = clock();selectionSort(arr, n, &comparisons, &swaps);end = clock();cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;printf("选择排序结果: ");printArray(arr, n);printf("选择排序比较次数: %d\n", comparisons);printf("选择排序交换次数: %d\n", swaps);printf("选择排序运行时间: %f 秒\n\n", cpu_time_used);// 恢复原始数组for (int i = 0; i < n; i++) {arr[i] = originalArr[i];}// 测试插入排序start = clock();insertionSort(arr, n, &comparisons, &swaps);end = clock();cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;printf("插入排序结果: ");printArray(arr, n);printf("插入排序比较次数: %d\n", comparisons);printf("插入排序交换次数: %d\n", swaps);printf("插入排序运行时间: %f 秒\n\n", cpu_time_used);return 0;
}
在上述代码中:
- 每种排序算法函数都添加了comparisons和swaps指针参数,用于统计比较次数和交换次数。在函数内部,每次进行比较或交换时,相应的计数器就会增加。
- 在main函数中,使用clock()函数记录每种排序算法的开始和结束时间,通过计算时间差并除以CLOCKS_PER_SEC得到排序算法的运行时间。
- 每次测试完一种排序算法后,将数组恢复为原始状态,以便对下一种排序算法进行公平的测试。
4.2 分析结果得出结论
运行上述代码,得到的输出结果类似如下:
冒泡排序结果: 11 12 22 25 34 64 90
冒泡排序比较次数: 21
冒泡排序交换次数: 12
冒泡排序运行时间: 0.000000 秒选择排序结果: 11 12 22 25 34 64 90
选择排序比较次数: 21
选择排序交换次数: 6
选择排序运行时间: 0.000000 秒插入排序结果: 11 12 22 25 34 64 90
插入排序比较次数: 18
插入排序交换次数: 12
插入排序运行时间: 0.000000 秒
从结果可以看出:
- 比较次数:在这个特定的数组上,冒泡排序和选择排序的比较次数相同,都是n*(n - 1)/2次(n为数组元素个数),因为它们都需要遍历整个数组进行比较。插入排序的比较次数相对较少,这是因为插入排序在数组部分有序时性能较好,它会在找到合适位置后提前结束内层循环。
- 交换次数:冒泡排序和插入排序的交换次数相同,这是因为它们在处理逆序对时都会进行交换操作。选择排序的交换次数最少,因为它每次只选择最小元素并交换一次,而不是像冒泡和插入排序那样频繁交换。
- 运行时间:在这个小规模数组上,三种排序算法的运行时间都非常短且几乎相同,这是因为小规模数据下,算法的时间复杂度差异不明显。但从理论上来说,冒泡排序和选择排序的时间复杂度均为O(n²),插入排序在最好情况下时间复杂度为O(n)(数组已经有序时),平均和最坏情况下为O(n²)。当数组规模增大时,插入排序在部分有序的情况下会比冒泡排序和选择排序更高效 ,而冒泡排序和选择排序在大规模数据下性能会明显下降。
综上所述,在选择排序算法时,需要根据数据规模、初始数据的有序程度等因素综合考虑,选择最适合的排序算法以提高程序的执行效率。