28. 排序算法
一、冒泡排序
冒泡排序(Bubble Sort)重复地遍历待排序的数列,依次比较相邻元素的值,如果它们的顺序错误(比如在升序排序中,前一个元素大于后一个元素),就交换它们的位置。遍历的过程会重复进行多次,直到整个数列变成有序状态。冒泡排序的名字来源于较小的元素会像水底的气泡一样逐渐 “浮” 到数列的顶端。
/**
* @brief 冒泡排序
*
* @param A 排序数组
* @param N 数组长度
*/
void BubbleSort(ElementType A[], int N)
{
int i = 0, j = 0;
int flag = 0;
for (i = 0; i < N - 1; i++) // 循环次数为N-1
{
flag = 0;
for (j = 0; j < N - i - 1; j++) // 每次循环,最后一个元素已经有序,因此循环次数为n-i-1
{
if (A[j] > A[j + 1])
{
Swap(&A[j], &A[j + 1]);
flag = 1;
}
}
if (flag == 0) // 全程无交换,序列已经有序
{
break;
}
}
}
/**
* @brief 交换两个数
*
* @param a 数1
* @param b 数2
*/
void Swap(ElementType * a, ElementType * b)
{
ElementType temp = * a;
* a = * b;
* b = temp;
}
最好情况下,时间复杂度为 ,最好情况下,时间复杂度为 。
二、插入排序
插入排序(Insertion Sort)的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
/**
* @brief 插入排序
*
* @param A 排序数组
* @param N 数组长度
*/
void InsertionSort(ElementType A[], int N)
{
int i = 0, j = 0;
ElementType temp = 0;
for (i = 1; i < N; i++) // 循环次数为N-1
{
temp = A[i]; // 待插入元素
for (j = i; j > 0 && A[j - 1] > temp; j--) // 寻找插入位置
{
A[j] = A[j - 1]; // 元素后移
}
A[j] = temp; // 插入元素
}
}
最好情况下,时间复杂度为 ,最好情况下,时间复杂度为 。
三、希尔排序
希尔排序(Shell Sort)是基于插入排序的一种更高效的改进版本,它通过将待排序的元素按照一定的间隔分组,对每组使用直接插入排序,随着间隔逐渐缩小,直至间隔为 1,整个序列就是一个组,此时再进行一次插入排序,整个序列就完成了排序。
/**
* @brief 希尔排序
*
* @param A 排序数组
* @param N 数组长度
*/
void ShellSort(ElementType A[], int N)
{
int i = 0, j = 0;
ElementType temp = 0;
for (int D = N / 2; D >= 1; D /= 2) // 希尔增量序列
{
// 插入排序
for (i = D; i < N; i++) // 循环次数为N-1
{
temp = A[i]; // 待插入元素
for (j = i; j >= D && A[j - D] > temp; j -= D) // 寻找插入位置
{
A[j] = A[j - D]; // 元素后移
}
A[j] = temp; // 插入元素
}
}
}
四、快速排序
快速排序(Quick Sort)的基本思想是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
/**
* @brief 快速排序
*
* @param A 排序数组
* @param N 数组长度
*/
void QuickSort(ElementType A[], int N)
{
Quick(A, 0, N - 1);
}
/**
* @brief 获取主元
*
* @param A 数组
* @param left 排序区间的左边界
* @param right 排序区间的右边界
*/
ElementType Median(ElementType A[], int left, int right)
{
int center = left + (right - left) >> 1; // 中间元素的索引
if (A[left] > A[center]) // 使中间元素比左边的元素大
{
Swap(&A[left], &A[center]);
}
if (A[left] > A[right]) // 使右边的元素比左边的元素大
{
Swap(&A[left], &A[right]);
}
if (A[center] > A[right]) // 使右边的元素比中间元素大
{
Swap(&A[center], &A[right]);
}
// 程序执行到这,A[left]<=A[center], A[center]<=A[right]
return A[right - 1]; // 返回枢轴元素
}
/**
* @brief 快速排序递归函数
*
* @param A 排序数组
* @param left 排序区间的左边界
* @param right 排序区间的右边界
*/
void Quick(ElementType A[], int left, int right)
{
int l = left, r = right;
ElementType pivot = Median(A, left, right); // 枢轴元素
ElementType temp = 0;
// 循环将比枢轴元素小的元素交换到左边,比枢轴元素大的元素交换到右边
while (l < r)
{
// 循环左指针向右移动,直到找到比枢轴元素大或相等的元素
while (A[l] < pivot)
{
l++;
}
// 循环右指针向左移动,直到找到比枢轴元素小或相等的元素
while (A[r] > pivot)
{
r--;
}
// 如果l>=r成立,说明privoot的左边的值全部小于等于pivot,右边的值全部大于等于pivot
if (l >= r)
{
break;
}
// 程序走到这里,说明l所指的元素大于pivot,r所指的元素小于pivot
Swap(&A[l], &A[r]);
// 如果交换完后,发现A[l]==pivot,则l++,继续向右移动,否则下次进入循环时,会一直交换元素,卡死
if (A[l] == pivot)
{
l++;
}
// 如果交换完后,发现A[r]==pivot,则r--,继续向左移动,否则下次进入循环时,会一直交换元素,卡死
if (A[r] == pivot)
{
r--;
}
}
// 程序执行到这,l>=r
// 如果l==r成立,必须l++,r--,否则会出现栈溢出
// 在l==r的情况下不进行任何操作,而是直接跳出循环,
// 那么下一次递归调用快速排序函数时,相同的left和right参数会再次传入,导致无限递归,最终引发栈溢出
if (l == r)
{
l++;
r--;
}
// 向左递归
if (left < r)
{
Quick(A, left, r);
}
// 向右递归
if (right > l)
{
Quick(A, l, right);
}
}
五、选择排序
选择排序(Selection Sort)的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
/**
* @brief 选择排序
*
* @param A 排序数组
* @param N 数组长度
*/
void SelectionSort(ElementType A[], int N)
{
int i = 0, j = 0;
ElementType min = 0;
for (i = 0; i < N - 1; i++) // 循环次数为N-1
{
min = i; // 记录最小元素下标
for (j = i + 1; j < N; j++)
{
if (A[j] < A[min]) // 找到最小元素
{
min = j;
}
}
Swap(&A[i], &A[min]);
}
}
六、堆排序
堆排序(Heap Sort)是一种基于二叉堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组来模拟堆的结构,堆排序可以分为两个主要阶段:建堆和调整堆。这里,我们使用最大堆来实现,逐步将最大堆的第一个元素(最大的元素)与最后一个元素交换,然后数组的长度减 1。
/**
* @brief 堆排序
*
* @param A 排序数组
* @param N 数组长度
*/
void HeapSort(ElementType A[], int N)
{
for (int i = N / 2 - 1; i >= 0; i--) // 构建最大堆
{
Heapify(A, N, i);
}
for (int i = N - 1; i >= 0; i--) // 一个个从堆顶取出元素
{
// 将当前最大的元素arr[0]与arr[i]交换
Swap(&A[0], &A[i]);
Heapify(A, i, 0); // 重新调整堆结构
}
}
/**
* @brief 将数组构建称最大堆
*
* @param A 数组
* @param N 数组的长度
* @param i 根结点的索引
*/
void Heapify(int A[], int N, int i)
{
int largest = i; // 初始化largest为根
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
if (left < N && A[left] > A[largest]) // 如果左子节点大于根
{
largest = left;
}
if (right < N && A[right] > A[largest]) // 如果右子节点大于目前已知的最大值
{
largest = right;
}
if (largest != i) // 如果最大值不是根,则交换
{
Swap(&A[i], &A[largest]);
Heapify(A, N, largest); // 递归地调整受影响的子树
}
}
七、归并排序
归并排序(Merge Sort)是一种分而治之的算法,它将原始数据分成越来越小的部分,直到每个部分只有一个元素,然后将这些部分两两合并成有序的序列,最终合并成完全有序的序列。归并排序是稳定的排序算法,且无论输入数据如何,都能保证 的时间复杂度。
/**
* @brief 归并排序
*
* @param A 排序数组
* @param N 数组长度
*/
void MergeSort(ElementType A[], int N)
{
ElementType * temp = (ElementType *)malloc(N * sizeof(ElementType));
int width = 0, left = 0, right = 0, mid = 0;
for (width = 1; width < N; width *= 2) // 计算每一趟归并的宽度
{
for (left = 0; left < N - width; left += width * 2) // 每次循环完后计算每一趟归并的左边界
{
mid = left + width - 1; // 计算中间索引
right = ((left + 2 * width - 1) >= N) ? (N - 1) : (left + 2 * width - 1); // 计算每一趟归并的右边界
Merge(A, temp, left, mid, right);
}
}
free(temp);
}
/**
* @brief 合并
*
* @param A 排序数组
* @param temp 做中转的数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
*/
void Merge(ElementType A[], ElementType temp[], int left, int mid, int right)
{
int i = left; // 左边有序序列的初始索引
int j = mid + 1; // 右边有序序列的初始索引
int k = left; // temp数组的初始索引
// 先把左右两边有序的数据按规则填充到temp数组中,直到有一边处理完毕
while (i <= mid && j <= right)
{
// 左边有序序列的元素小于等于右边有序序列的元素,即将左边的元素填充到temp中
// 右边有序序列的元素小于左边有序序列的元素,即将右边的元素填充到temp中
temp[k++] = (A[i] <= A[j]) ? A[i++] : A[j++];
}
// 将有剩余数据的一边一次填充到temp中
// 如果左边有序序列还有剩余的元素,直接填充到temp中
while (i <= mid)
{
temp[k++] = A[i++];
}
// 如果右边有序序列还有剩余的元素,直接填充到temp中
while (j <= right)
{
temp[k++] = A[j++];
}
// 将temp数组中的元素拷贝到A
// 并不是每次都拷贝所有的元素
for (i = left; i <= right; i++) {
A[i] = temp[i];
}
}
八、基数排序
基数排序(Radix Sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的基本思想是将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行排序。这样从最低位排序一直到最高位排序完成后,数列就变成一个有序序列。
#include <math.h>
/**
* @brief 基数排序
*
* @param A 排序数组
* @param N 数组长度
*/
void RadixSort(int A[], int N)
{
int maxValue = FindMax(A, N);
int n = DigitCount(maxValue);
int index = 0;
int temp = 0;
PQueue buckets[10];
for (int i = 0; i < 10; i++)
{
buckets[i] = CreateQueue();
}
for (int i = 0; i < n; i++) // 遍历每一位数
{
for (int j = 0; j < N; j++) // 遍历数组元素
{
temp = A[j] / (int)pow(10, i) % 10; // 获取对应的位
Enqueue(buckets[temp], A[j]); // 放入到对应的桶中
}
index = 0;
for (int j = 0; j < 10; j++) // 遍历桶
{
while (buckets[j]->Front != NULL) // 桶不为空
{
A[index++] = Dequeue(buckets[j]); // 取出元素放到A数组中
}
}
}
}
/**
* @brief 获取最大值
*
* @param A 数组
* @param N 数组长度
* @return int 最大值
*/
int FindMax(int A[], int N)
{
int max = A[0];
for (int i = 1; i < N; i++)
{
if (A[i] > max)
{
max = A[i];
}
}
return max;
}
/**
* @brief 获取整数的位数
*
* @param n 整数
* @return int 返回位数
*/
int DigitCount(int n)
{
int count = 0;
if (n == 0) // 0被认为有1位
{
return 1;
}
n = (n < 0) ? -n : n; // 如果n是负数,则转为正数
do
{
n /= 10; // 除以10
count++; // 位数增加
} while (n != 0);
return count;
}
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
· 全程使用 AI 从 0 到 1 写了个小工具