目录
一、排序算法概述
本文介绍了八种常见排序算法的原理和实现方法,分为稳定排序、非稳定排序和非比较排序三类。稳定排序包括冒泡排序(通过相邻元素比较交换)、插入排序(构建有序序列)和归并排序(分治合并);非稳定排序包括选择排序(选择最小/最大元素)、希尔排序(基于插入排序的优化)、快速排序(分治分区)和堆排序(基于堆结构);非比较排序包括计数排序(统计元素出现次数)。每种算法均附有C语言实现代码,并针对性能优化提出了改进方法,如快速排序的三数取中和堆排序的堆调整操作。
二、稳定排序
(一)冒泡排序

原理思路:
冒泡排序是一种简单的排序算法,通过重复遍历数组,比较相邻元素的大小,并在必要时交换它们的位置。
每次遍历后,最大的元素会“冒泡”到数组的末尾。随着遍历次数的增加,数组逐渐变得有序。
为了优化性能,可以引入一个标志变量 swapped,用于记录某一轮遍历中是否发生了交换操作。如果没有发生交换,则说明数组已经完全有序,可以提前终止排序。
代码实现:
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) { // 外层循环:控制排序轮数
int swapped = 0; // 标志变量,用于优化
for (int j = 0; j < n - 1 - i; j++) { // 内层循环:比较相邻元素
if (arr[j] > arr[j + 1]) { // 如果前一个元素大于后一个元素
int temp = arr[j]; // 交换它们的位置
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1; // 发生了交换操作
}
}
if (swapped == 0) { // 如果某轮排序中没有发生交换操作
break; // 提前终止排序
}
}
}
(二)插入排序

原理思路:
插入排序是一种类似于整理扑克牌的排序算法。它通过构建一个有序序列,逐步将未排序部分的元素插入到已排序序列的合适位置。
从数组的第二个元素开始,将其与前面的已排序部分进行比较,找到合适的位置后插入。
该算法的核心在于维护一个有序序列,并逐步将剩余元素插入到该序列中。
代码实现:
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) { // 从第二个元素开始
int temp = arr[i]; // 保存当前元素
int end = i - 1; // 设置已排序部分的末尾索引
// 在已排序部分中找到合适的插入位置
while (end >= 0 && arr[end] > temp) {
arr[end + 1] = arr[end]; // 将较大的元素向后移动
end--;
}
arr[end + 1] = temp; // 插入当前元素
}
}
(三)归并排序

原理思路:
归并排序是一种高效的分治排序算法。它通过递归地将数组分成两半,分别对这两半进行排序,然后将两个有序子数组合并为一个有序数组。
分治思想的核心在于“分而治之”:将大问题分解为小问题,递归解决小问题,最后将结果合并。
合并操作是归并排序的关键步骤,通过双指针技术将两个有序子数组合并为一个有序数组。
递归版本代码实现:
void _mergeSort(int *a, int begin, int end, int *tmp) {
if (begin == end) return; // 如果只有一个元素,直接返回
int mid = (begin + end) / 2; // 找到中间位置
_mergeSort(a, begin, mid, tmp); // 递归排序左半部分
_mergeSort(a, mid + 1, end, tmp); // 递归排序右半部分
int begin1 = begin, end1 = mid; // 左半部分的起始和结束位置
int begin2 = mid + 1, end2 = end; // 右半部分的起始和结束位置
int i = begin; // 合并后的数组的起始位置
// 合并两个有序子数组
while (begin1 <= end1 && begin2 <= end2) {
if (a[begin1] < a[begin2]) tmp[i++] = a[begin1++];
else tmp[i++] = a[begin2++];
}
// 处理剩余的左半部分
while (begin1 <= end1) tmp[i++] = a[begin1++];
// 处理剩余的右半部分
while (begin2 <= end2) tmp[i++] = a[begin2++];
// 将合并后的数组复制回原数组
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void mergeSort(int *a, int n) {
int *tmp = (int *)malloc(sizeof(int) * n); // 分配临时数组
_mergeSort(a, 0, n - 1, tmp); // 调用递归排序函数
free(tmp); // 释放临时数组
}
非递归版本代码实现:
void mergeSortNonR(int *a, int n) {
int *tmp = (int *)malloc(sizeof(int) * n); // 分配临时数组
int gap = 1; // 初始化子数组的大小
while (gap < n) { // 当子数组大小小于数组长度时
int j = 0; // 临时数组的索引
for (int i = 0; i < n; i += 2 * gap) { // 遍历数组,每次处理两个子数组
int begin1 = i, end1 = i + gap - 1; // 左子数组的范围
int begin2 = i + gap, end2 = i + 2 * gap - 1; // 右子数组的范围
// 修正区间,防止越界
if (end1 >= n) end1 = n - 1, begin2 = n, end2 = n - 1;
if (begin2 >= n) begin2 = n, end2 = n - 1;
if (end2 >= n) end2 = n - 1;
// 合并两个子数组
while (begin1 <= end1 && begin2 <= end2) {
if (a[begin1] <= a[begin2]) tmp[j++] = a[begin1++];
else tmp[j++] = a[begin2++];
}
// 处理剩余的左子数组
while (begin1 <= end1) tmp[j++] = a[begin1++];
// 处理剩余的右子数组
while (begin2 <= end2) tmp[j++] = a[begin2++];
}
memcpy(a, tmp, sizeof(int) * n); // 将临时数组复制回原数组
gap *= 2; // 子数组大小翻倍
}
free(tmp); // 释放临时数组
}
三、非稳定排序
(一)选择排序

原理思路:
选择排序是一种简单的排序算法,它通过不断选择未排序部分的最小(或最大)元素,并将其放到已排序部分的末尾。
每次遍历未排序部分,找到最小(或最大)元素的索引,然后将其与未排序部分的第一个元素交换。
该算法的核心在于每次选择一个最小(或最大)元素,逐步构建有序序列。
代码实现:
void selectionSort(int arr[], int n) {
int begin = 0, end = n - 1; // 设置未排序部分的起始和结束位置
while (begin < end) {
int mini = begin, maxi = begin; // 初始化最小值和最大值的索引
for (int j = begin; j <= end; j++) { // 遍历未排序部分
if (arr[j] < arr[mini]) mini = j; // 更新最小值索引
if (arr[j] > arr[maxi]) maxi = j; // 更新最大值索引
}
swap(&arr[begin], &arr[mini]); // 将最小值交换到未排序部分的开头
if (begin == maxi) maxi = mini; // 如果最大值被交换到开头,更新最大值索引
swap(&arr[end], &arr[maxi]); // 将最大值交换到未排序部分的末尾
++begin, --end; // 缩小未排序部分的范围
}
}
(二)希尔排序

原理思路:
希尔排序是一种基于插入排序的优化算法,它通过先对数组进行“预排序”,使得数组接近有序,从而提高插入排序的效率。
算法的核心在于选择合适的“增量序列”,逐步缩小增量,最终在增量为 1 时,执行一次普通的插入排序。
通过这种方式,希尔排序可以快速地将数组中的元素移动到接近最终位置,从而提高排序效率。
代码实现:
void shellSort(int arr[], int n) {
for (int gap = n / 2; gap > 0; gap /= 2) { // 逐步缩小增量
for (int i = 0; i < n - gap; i++) { // 遍历每个子序列的起始位置
int end = i; // 当前比较的起始位置
int temp = arr[end + gap]; // 当前需要插入的元素
// 在间隔为 gap 的子序列中找到插入位置
while (end >= 0 && arr[end] > temp) {
arr[end + gap] = arr[end]; // 将较大的元素向后移动
end -= gap; // 向左移动
}
arr[end + gap] = temp; // 插入当前元素
}
}
}
(三)快速排序

原理思路:
快速排序是一种高效的分治排序算法。它通过选择一个“基准值(pivot)”,将数组分为两部分:一部分包含所有小于基准值的元素,另一部分包含所有大于基准值的元素。
然后递归地对这两部分进行相同的操作,直到整个数组有序。
快速排序的核心在于“分区操作”,通过分区操作将数组分为两部分,并递归地对这两部分进行排序。
优化方法:
三数取中:选择数组的首、中、尾三个数的中值作为基准值,以减少基准值选择的随机性。
小区间优化:当数组规模较小时,改用插入排序,提高排序效率。
三路划分:针对大量数据重复的场景,将元素划分成三个区间 [left] [key] [right],分别存储小于、等于和大于基准值的元素。
代码实现:
Hoare 版本:
int partSort(int *a, int begin, int end) {
int keyi = begin; // 选择第一个元素作为基准值
while (begin < end) { // 右边找小,左边找大
while (begin < end && a[end] >= a[keyi]) end--; // 在右边找小于基准值的元素
while (begin < end && a[begin] <= a[keyi]) begin++; // 在左边找大于基准值的元素
swap(&a[begin], &a[end]); // 交换找到的两个元素
}
swap(&a[begin], &a[keyi]); // 将基准值放到中间位置
return begin; // 返回基准值的最终位置
}
void quickSort(int *a, int begin, int end) {
if (begin >= end) return; // 如果区间无效,直接返回
int key = partSort(a, begin, end); // 分区操作
quickSort(a, begin, key - 1); // 递归排序左半部分
quickSort(a, key + 1, end); // 递归排序右半部分
}
挖坑法:
int partSort(int *a, int begin, int end) {
int key = a[begin]; // 选择第一个元素作为基准值
int hole = begin; // 初始化坑的位置
while (begin < end) { // 右边找小,左边找大
while (begin < end && a[end] >= key) end--; // 在右边找小于基准值的元素
a[hole] = a[end]; // 将找到的元素填入坑中
hole = end; // 更新坑的位置
while (begin < end && a[begin] <= key) begin++; // 在左边找大于基准值的元素
a[hole] = a[begin]; // 将找到的元素填入坑中
hole = begin; // 更新坑的位置
}
a[hole] = key; // 将基准值填入最终的坑中
return hole; // 返回基准值的最终位置
}
void quickSort(int *a, int begin, int end) {
if (begin >= end) return; // 如果区间无效,直接返回
int key = partSort(a, begin, end); // 分区操作
quickSort(a, begin, key - 1); // 递归排序左半部分
quickSort(a, key + 1, end); // 递归排序右半部分
}
双指针法:
int partSort(int *a, int begin, int end) {
int keyi = begin; // 选择第一个元素作为基准值
int pre = begin; // 初始化小于基准值的区域的末尾
int cur = begin + 1; // 初始化当前遍历的位置
while (cur <= end) { // 遍历数组
if (a[cur] < a[keyi] && ++pre != cur) // 如果当前元素小于基准值
swap(&a[pre], &a[cur]); // 交换到小于基准值的区域
++cur; // 移动到下一个元素
}
swap(&a[keyi], &a[pre]); // 将基准值放到中间位置
keyi = pre; // 更新基准值的最终位置
return keyi; // 返回基准值的最终位置
}
void quickSort(int *a, int begin, int end) {
if (begin >= end) return; // 如果区间无效,直接返回
int key = partSort(a, begin, end); // 分区操作
quickSort(a, begin, key - 1); // 递归排序左半部分
quickSort(a, key + 1, end); // 递归排序右半部分
}
非递归版本:
void quickSortNonR(int *a, int begin, int end) {
stack<int> st; // 使用栈来模拟递归
st.push(end); // 将结束位置压入栈
st.push(begin); // 将开始位置压入栈
while (!st.empty()) { // 当栈不为空时
int left = st.top(); // 获取左边界
st.pop();
int right = st.top(); // 获取右边界
st.pop();
int keyi = partSort(a, left, right); // 分区操作
if (keyi + 1 < right) { // 如果右半部分有效
st.push(right); // 将右半部分的结束位置压入栈
st.push(keyi + 1); // 将右半部分的开始位置压入栈
}
if (left < keyi - 1) { // 如果左半部分有效
st.push(keyi - 1); // 将左半部分的结束位置压入栈
st.push(left); // 将左半部分的开始位置压入栈
}
}
}
(四)堆排序

原理思路:
堆排序是一种基于堆数据结构的排序算法。它通过构建一个最大堆(父亲大于孩子)或最小堆(父亲小于孩子),然后反复从堆中取出最大(或最小)元素并将其放置到数组的末尾(或开头),从而实现整个数组的排序。
堆排序的关键在于“堆调整”操作,通过调整堆的结构,确保堆的性质(最大堆或最小堆)始终满足。
该算法的核心在于利用堆的性质,高效地找到数组中的最大(或最小)元素,并逐步构建有序序列。
代码实现:
void adjustDown(int *a, int n, int parent) {
int child = parent * 2 + 1; // 子节点的索引
while (child < n) { // 当子节点存在时
if (child + 1 < n && a[child + 1] > a[child]) ++child; // 如果右子节点更大,选择右子节点
if (a[child] > a[parent]) { // 如果子节点大于父节点
swap(&a[child], &a[parent]); // 交换父子节点
parent = child; // 更新父节点的位置
child = parent * 2 + 1; // 更新子节点的索引
} else break; // 如果父节点大于子节点,结束调整
}
}
void heapSort(int *a, int n) {
for (int i = (n - 1 - 1) / 2; i >= 0; i--) adjustDown(a, n, i); // 构建最大堆
int end = n - 1; // 设置堆的末尾位置
while (end > 0) { // 当堆中还有元素时
swap(&a[0], &a[end]); // 将堆顶元素(最大值)放到数组末尾
adjustDown(a, end, 0); // 调整堆
--end; // 缩小堆的范围
}
}
四、非比较排序
(一)计数排序

原理思路:
计数排序是一种非比较排序算法,适用于整数排序。它通过统计每个整数出现的次数,然后根据统计结果重新构建有序数组。
算法的核心在于找到数组中的最大值和最小值,根据它们的差值申请一个辅助数组(计数数组),用于统计每个整数出现的次数。
最后,根据计数数组中的统计结果,从小到大依次将元素写回原数组,从而实现排序。
代码实现:
void countingSort(int arr[], int n) {
int min = arr[0], max = arr[0]; // 初始化最小值和最大值
for (int i = 1; i < n; i++) { // 遍历数组,找到最小值和最大值
if (arr[i] < min) min = arr[i];
if (arr[i] > max) max = arr[i];
}
int range = max - min + 1; // 计算范围
int *count = (int *)malloc(sizeof(int) * range); // 分配计数数组
memset(count, 0, sizeof(int) * range); // 初始化计数数组为 0
for (int i = 0; i < n; i++) { // 统计每个元素出现的次数
count[arr[i] - min]++;
}
int index = 0; // 初始化原数组的索引
for (int i = 0; i < range; i++) { // 遍历计数数组
while (count[i] > 0) { // 如果当前元素出现次数大于 0
arr[index++] = i + min; // 将元素写回原数组
count[i]--; // 减少计数
}
}
free(count); // 释放计数数组
}
五、总结
本文介绍了三种稳定排序算法(冒泡、插入、归并)和四种非稳定排序算法(选择、希尔、快速、堆排序)的原理及代码实现。稳定排序通过相邻元素比较和交换(冒泡)、构建有序序列(插入)或分治合并(归并)实现排序;非稳定排序则通过选择最值(选择)、分组插入(希尔)、基准值分区(快速)或堆结构调整(堆排序)完成排序。文章还分析了各算法的核心思路和优化方法,如冒泡排序的提前终止、快速排序的三数取中等。这些经典排序算法为不同场景下的数据处理提供了多样化的解决方案。

被折叠的 条评论
为什么被折叠?



