在计算机科学领域,排序算法是数据处理的核心技术之一。无论是小规模数据的简单整理,还是大规模数据的高效处理,选择合适的排序算法直接影响着程序的性能。本文将深入解析常见排序算法的核心思想、实现细节、特性对比及适用场景,帮助读者全面掌握这一关键技术。
一、排序的本质与应用场景
1. 排序的定义
排序是将一组记录按照某个关键字的大小,按递增或递减顺序排列的过程。例如,将数组[5,3,9,6,2]排序为[2,3,5,6,9],本质上是通过关键字(这里是数值大小)重新组织数据的顺序。
2. 实际应用场景
- 电商平台:按价格、销量、评论数对商品进行排序,提升用户体验。
- 数据库系统:对查询结果排序以满足用户需求(如 “按时间倒序”)。
- 科学计算:预处理数据以加速后续分析(如统计、机器学习模型训练)。
- 竞赛与算法题:排序是解决复杂问题的基础(如贪心、动态规划的前置步骤)。
3.常见的排序算法
二、插入排序:增量构建有序序列
1. 直接插入排序(Straight Insertion Sort)
核心思想:
将未排序元素逐个插入已排序序列的合适位置,类似整理扑克牌时逐张插入手牌。每次从未排序区间取一个元素,与已排序区间的元素从后向前比较,找到合适位置后插入,后移沿途较大的元素。
算法步骤:
- 假设前 i 个元素已排序,取第 i+1 个元素作为待插入元素。
- 从已排序序列末尾向前搜索,找到第一个比待插入元素大的位置。
- 将该位置之后的元素后移一位,腾出空间插入待插入元素。
- 重复直至所有元素插入完成。
代码实现(C 语言):
void InsertSort(int* a, int n) {
for (int i = 0; i < n - 1; i++) {
int end = i; // 已排序区间末尾
int tmp = a[end + 1]; // 待插入元素
while (end >= 0 && a[end] > tmp) { // 向前搜索插入位置
a[end + 1] = a[end]; // 元素后移
end--;
}
a[end + 1] = tmp; // 插入到正确位置
}
}
特性分析:
- 时间复杂度:最好
O(n)
(已有序),最坏 / 平均O(n^2)
。 - 空间复杂度:
O(1)
(原地排序)。 - 稳定性:稳定(相同元素保持原顺序)。
- 适用场景:小规模数据或近乎有序的数组(如有序度 > 80%)。
2. 希尔排序(Shell Sort)
核心思想:
通过 “增量序列” 将数组分组,对每组进行插入排序,逐步缩小增量至 1(退化为直接插入排序)。增量序列(如 gap = n/3 + 1
)的设计是关键,通过分组预排序使数组更接近有序,提升最终插入效率。
算法步骤:
- 初始化增量
gap = n
,通常取gap = n/2
或gap = n/3 + 1
。 - 按
gap
分组(下标相差gap
的元素为一组),对每组进行插入排序。 - 更新
gap = gap/3 + 1
,重复直至gap = 1
。
代码实现(C 语言):
void ShellSort(int* a, int n) {
int gap = n; // 初始增量
while (gap > 1) {
gap = gap / 3 + 1; // 推荐增量序列
for (int i = 0; i < n - gap; i++) { // 分组插入排序
int end = i;
int tmp = a[end + gap]; // 同组待插入元素
while (end >= 0 && a[end] > tmp) {
a[end + gap] = a[end]; // 组内元素后移
end -= gap; // 组内指针前移
}
a[end + gap] = tmp; // 插入到组内正确位置
}
}
}
特性分析:
- 时间复杂度:平均约
O(n^1.3)
,优于直接插入排序。 - 空间复杂度:
O(1)
。 - 稳定性:不稳定(分组可能打乱相同元素顺序)。
- 适用场景:中等规模数组(
n=100~1000
)。
三、选择排序:极值定位的直接策略
1. 直接选择排序(Straight Selection Sort)
核心思想:
每次从未排序区间选择最小(或最大)元素,与区间起点交换,逐步缩小未排序区间。
算法步骤:
- 遍历未排序区间
[i, n-1]
,记录最小元素下标min_idx
。 - 若
min_idx
不等于i
,交换a[i]
与a[min_idx]
。 - 重复直至所有元素排序。
代码实现(C 语言):
void SelectSort(int* a, int n) {
for (int i = 0; i < n - 1; i++) {
int min = i;
for (int j = i + 1; j < n; j++) { // 寻找最小值
if (a[j] < a[min]) {
min = j;
}
}
if (min != i) { // 交换最小值到当前起点
swap(&a[i], &a[min]);
}
}
}
这样写效率不是很高,我们可以同时寻找最小值和最大值,寻找到最小值和最大值过后,将其交换到首尾节点,以此类推,这样每次搜索的范围就减少了一半,代码如下:
void SelectSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1; i < end; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
if (maxi == begin)
{
maxi = mini;
}
Swap(&arr[mini], &arr[begin]);
Swap(&arr[maxi], &arr[end]);
begin++;
end--;
}
}
特性分析:
- 时间复杂度:固定
O(n^2)
(无论数据是否有序)。 - 空间复杂度:
O(1)
。 - 稳定性:不稳定(相同元素相对顺序可能改变)。
- 适用场景:仅用于教学演示,实际中极少使用。
2. 堆排序(Heap Sort)
核心思想:
利用堆(大顶堆 / 小顶堆)的性质,每次取出堆顶元素(最大值 / 最小值),调整堆结构直至排序完成。升序排序建大顶堆,降序建小顶堆。
算法步骤:
- 建堆:从最后一个非叶子节点开始,自底向上调整为大顶堆。
- 排序:交换堆顶与堆尾元素,堆大小减 1,向下调整堆顶,重复直至堆空。
关键代码(向下调整函数):
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/2 - 1; i >= 0; i--) {
AdjustDown(a, n, i);
}
// 排序过程
for (int i = n - 1; i > 0; i--) {
swap(&a[0], &a[i]); // 交换堆顶与堆尾
AdjustDown(a, i, 0); // 调整剩余堆
}
}
除了向下建堆外还可以采用向上建堆,代码如下:
void HPjustUp(HPDataType* arr,int child)
{
int parent = (child - 1) / 2;
while (child>0)
{
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
特性分析:
- 时间复杂度:始终
O(n log n)
(建堆O(n)
,每次调整O(log n)
)。 - 空间复杂度:
O(1)
(原地排序)。 - 稳定性:不稳定(堆调整可能交换相同元素)。
- 适用场景:大规模数据且内存受限(如嵌入式设备)。
四、交换排序:通过元素交换实现全局有序
1. 冒泡排序(Bubble Sort)
核心思想:
相邻元素比较,逆序则交换,使较大元素逐步 “冒泡” 到数组末尾。通过标志位优化可提前终止(若某轮无交换,说明已有序)。
算法步骤:
- 遍历数组,比较相邻元素,前大于后则交换。
- 每轮最大元素到达末尾,未排序区间缩小 1。
- 若某轮无交换,提前终止。
优化代码实现:
void BubbleSort(int* a, int n) {
for (int i = 0; i < n; i++) {
int exchange = 0; // 标志位判断是否有序
for (int j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) {
swap(&a[j], &a[j + 1]);
exchange = 1;
}
}
if (!exchange)
break; // 已有序,提前结束
}
}
特性分析:
- 时间复杂度:最好
O(n)
,最坏 / 平均O(n²)
。 - 空间复杂度:
O(1)
。 - 稳定性:稳定(相同元素保持原顺序)。
- 适用场景:小规模数据或教学演示。
2. 快速排序(Quick Sort)
2.1递归法实现快速排序
核心思想:
分治策略,选择基准值(Pivot),将数组划分为左(<基准)、中(= 基准)、右(> 基准)三部分,递归排序左右子数组。
三种分区实现:
- Hoare 版本(左右指针法):右指针先找小,左指针找大,交换直至相遇,基准值归位。
- 选左端点为基准值,左右指针
left
、right
分别指向基准值下一个位置和右端点。 right
先向左找小于基准的元素,left
向右找大于基准的元素,交换两者,直至left >= right
。- 交换基准值与
right
位置元素,返回基准最终下标。
代码实现
int QuickSort_Find(int* arr, int left, int right)
{
int keyi = left; // 基准值
left++;
while (left <= right)
{
while (left <= right && arr[keyi] < arr[right])// 右指针先找小
{
right--;
}
while (left <= right && arr[keyi] > arr[left]) // 左指针找大
{
left++;
}
if (left <= right)
{
Swap(&arr[left++], &arr[right--]);// 交换左右找到的逆序对
}
}
Swap(&arr[keyi], &arr[right]);// 基准值归位
return right;// 返回基准下标
}
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = QuickSort_Find1(arr, left, right);
QuickSort(arr, left, keyi-1);
QuickSort(arr, keyi+1, right);
}
- 挖坑法:基准值形成 “坑”,左右指针交替填充,最终填入基准值。
创建左右指针。首先从右向左找出比基准小的数据,找到后立即放入左边坑中,当前位置变为新 的"坑",然后从左向右找出比基准大的数据,找到后立即放入右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放入当前的"坑"中,返回当前"坑"下标(即分界值下标)
- 选左端点为基准值,记录为
key
,形成 “坑” 位置left
。 right
向左找小于key
的元素,填入坑位,新坑为right
。left
向右找大于key
的元素,填入新坑,新坑为left
。- 重复直至
left == right
,填入key
,返回坑位下标。
- 前后指针法(Lomuto):前指针标记小于基准的末尾,后指针遍历交换,基准值与前指针交换。
创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。
代码实现:
int QuickSort_Find(int* arr, int left, int right)
{
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
return prev;
}
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int keyi = QuickSort_Find(arr, left, right);
QuickSort(arr, left, keyi-1);
QuickSort(arr, keyi+1, right);
}
2.2迭代法实现快速排序
快速排序的递归实现简洁高效,但在处理大规模数据时可能因递归深度过大导致栈溢出(如最坏情况下递归深度达 O(n)
)。迭代实现通过显式使用栈(Stack)模拟递归过程,避免了这一问题,更适合工程环境中的稳健性要求。以下是基于 Lomuto 分区方案(前后指针法)的迭代实现详解。
1. 核心思想:栈模拟递归分区
- 递归转迭代:
递归版本中,每次分区后自动压栈(系统栈)处理左右子区间;迭代版本需手动维护一个栈,显式存储待排序的区间[left, right]
。 - 栈操作逻辑:
- 初始时将整个数组区间
[begin, end]
压栈(注意栈的后进先出特性,先压右端点,再压左端点)。 - 循环取栈顶区间,进行分区操作,得到基准位置
keyi
。 - 将左右子区间(若有效)压栈,继续处理直至栈空。
- 初始时将整个数组区间
2. 算法步骤(基于 Lomuto 分区)
- 初始化栈:压入初始区间
[left, right]
(注意顺序:先压右端点,再压左端点)。 - 循环处理栈内区间:
- 弹出栈顶元素,得到当前处理区间
[begin, end]
。 - 执行分区操作(选择
begin
为基准值,通过前后指针法划分区间)。 - 记录基准位置
keyi
,将左右子区间(若长度大于 1)压栈(先压右子区间,再压左子区间,确保左子区间先处理)。
- 弹出栈顶元素,得到当前处理区间
- 分区逻辑:
prev
指针标记小于基准值的末尾,初始为begin
。cur
指针从begin+1
开始遍历,若arr[cur] < arr[begin]
,则prev++
并交换arr[prev]
与arr[cur]
。- 遍历结束后,交换基准值
arr[begin]
与arr[prev]
,使基准值归位。
3. 代码实现(C 语言,含栈结构体定义,完整代码见附录)
// 栈结构体定义(假设已实现基础栈操作)
typedef struct Stack {
int* data;
int top;
int capacity;
} Stack;
// 栈初始化、压栈、弹栈等函数(此处省略具体实现,需确保栈操作正确)
void StackInit(Stack* st);
void StackPush(Stack* st, int value);
int StackPop(Stack* st);
int StackTop(Stack* st);
bool StackEmpty(Stack* st);
void StackDestroy(Stack* st);
void QuickSort(int* arr, int left, int right)
{
Stack ST;
StackInit(&ST); // 初始化
StackPush(&ST, right); // 先压右端点,再压左端点
StackPush(&st, left);
StackPush(&ST, left);
while (!StackEmpty(&ST))
{
int begin = StackTop(&ST);// 取出左端点
StackPop(&ST);
int end = StackTop(&ST); // 取出右端点
StackPop(&ST);
int keyi = begin; // 基准值下标(左端点)
int prev = begin;// 前指针:标记小于基准的末尾
int cur = prev + 1; // 后指针:遍历未排序区间
while (cur <= end)
{
if (arr[cur] < arr[keyi] && ++prev != cur)// 发现更小元素,前指针右移,非自身时交换(避免无意义交换)
{
Swap(&arr[prev], &arr[cur]);// 交换到prev位置
}
cur++;// 后指针继续右移
}
Swap(&arr[keyi], &arr[prev]); // 基准值归位到prev位置,此时arr[prev]是基准值
// 压栈顺序:先右子区间,再左子区间(保证左子区间先被处理)
// 右子区间:[prev+1, end]
keyi = prev;
if (keyi + 1 < end)
{
StackPush(&ST, end);
StackPush(&ST, keyi+1);
}
// 左子区间:[begin, prev-1]
if (begin < keyi - 1)
{
StackPush(&ST, keyi - 1);
StackPush(&ST, begin);
}
}
StackDestroy(&ST);// 释放栈内存
}
4. 关键细节解析
- 栈容量初始化:
栈的初始容量可以根据数据规模设置(如right - left + 1
),避免动态扩容带来的性能损耗。 - 分区优化:
- 代码中
if (prev != cur)
避免了元素与自身交换(当cur
追上prev
时,交换无意义)。 - 基准值固定为左端点,实际工程中应添加 随机化基准值 逻辑(如随机选择基准并与左端点交换),避免最坏情况
O(n²)
。
- 代码中
- 压栈顺序:
由于栈是后进先出,先压右子区间[prev+1, end]
,再压左子区间[begin, prev-1]
,确保左子区间先被处理(与递归的深度优先顺序一致)。
5. 迭代 vs 递归:优缺点对比
特性 | 递归实现 | 迭代实现 |
---|---|---|
实现复杂度 | 简洁(系统栈自动管理区间) | 较复杂(需手动维护栈结构) |
栈溢出风险 | 高(最坏递归深度 O(n) ) | 低(栈容量可控,或使用循环队列优化) |
空间效率 | 平均 O(log n) (系统栈) | 平均 O(log n) (用户栈,常数因子略大) |
调试难度 | 易(递归调用栈清晰) | 较难(需跟踪栈内区间状态) |
适用场景 | 小规模数据或教学演示 | 大规模数据或生产环境(稳健性优先) |
6. 工程优化建议
- 随机化基准值:
在分区前随机选择基准值,避免有序数组导致的最坏情况:// 随机化基准值(在分区前调用) int RandomPivot(int* arr, int begin, int end) { srand(time(NULL)); // 初始化随机种子(仅需调用一次) int pivot = begin + rand() % (end - begin + 1); // 生成[begin, end]随机下标 Swap(&arr[begin], &arr[pivot]); // 交换到左端点,作为基准值 return begin; }
- 小规模区间切换插入排序:
当区间长度小于阈值(如 16)时,切换为插入排序,减少递归 / 迭代开销:#define INSERTION_SORT_THRESHOLD 16 if (end - begin + 1 <= INSERTION_SORT_THRESHOLD) { InsertSort(arr + begin, end - begin + 1); // 插入排序子数组 continue; }
- 尾递归优化:
迭代实现本质是尾递归优化的结果,确保每次仅处理一个子区间,另一个子区间通过栈延迟处理,减少栈空间占用。
迭代实现的快速排序通过显式栈管理,在保持平均 O(n log n)
效率的同时,避免了递归栈溢出的风险,是生产环境中的可靠选择。理解其核心逻辑(栈模拟递归、分区策略、基准值优化)能帮助我们更好地应对大规模数据排序需求。结合随机化基准值和插入排序切换等优化,迭代快排能在工程中发挥出卓越的性能。
特性分析
- 时间复杂度:平均
O(n log n)
,最坏O(n²)
(可通过随机化基准值优化)。 - 空间复杂度:递归栈平均
O(log n)
,最坏O(n)
(非递归实现可避免栈溢出)。 - 稳定性:不稳定(相同元素可能跨区交换)。
- 适用场景:大规模数据的通用排序(工程首选,平均效率最高)。
五、归并排序:稳定排序的分治方案
核心思想:
分治策略,递归分解数组为子数组,排序后合并(二路归并),确保合并后的数组有序。合并时借助临时数组存储中间结果。
算法步骤:
- 分解:递归将数组分成左右两半,直至子数组长度为 1(已有序)。
- 合并:双指针遍历左右子数组,按顺序合并到临时数组,再拷贝回原数组。
合并逻辑(关键函数):
void _MergeSort(int* arr, int left, int right, int* tmp)
{
if (left >= right) //分解数组
{
return;
}
int mid = (left + right) / 2;
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid+1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = begin1;
while (begin1 <= end1 && begin2 <= end2) // 合并左右子数组
{
if (arr[begin1] > arr[begin2])
{
tmp[index++] = arr[begin2++];
}
else
{
tmp[index++] = arr[begin1++];
}
}
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
memcpy(&arr[left], &tmp[left], sizeof(int) * (right - left + 1)); // 拷贝回原数组
}
void MergeSort(int* a, int n) {
int* tmp = (int*)malloc(n * sizeof(int));
_MergeSort(a, 0, n - 1, tmp); // 递归分解
free(tmp);
}
特性分析:
- 时间复杂度:始终
O(n log n)
(不依赖数据初始状态)。 - 空间复杂度:
O(n)
(需要临时数组)。 - 稳定性:稳定(合并时保留相同元素原顺序)。
- 适用场景:稳定性要求高的场景(如多关键字排序)或链表排序。
六、非比较排序:计数排序(Counting Sort)
核心思想:
利用统计频率的方式,将元素按值出现的次数直接映射到目标位置,适用于值域范围较小的整数排序。
算法步骤:
- 确定值域:找到最小值
min
和最大值max
,计算范围range = max - min
。 - 统计频率:创建计数数组
count
,记录每个值的出现次数。 - 回填数组:按值从小到大,根据计数次数将元素填回原数组,保证稳定性。
代码实现:
void CountSort(int* arr, int n)
{
int max = arr[0];
int min = arr[0];
int i = 1;
while (i < n) // 找值域范围
{
if (arr[i] > max)
{
max = arr[i];
}
if (arr[i] < min)
{
min = arr[i];
}
i++;
}
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int)); // 初始化计数数组
if (count == NULL)
{
perror("calloc fail");
exit(1);
}
for (i = 0; i < n; i++) // 统计频率
{
count[arr[i] - min]++;
}
int index = 0;
for (i = 0; i < range; i++) // 按值顺序回填
{
while (count[i]--)
{
arr[index++] = i + min;
}
}
}
特性分析:
- 时间复杂度:o(n+range)(
k
为值域范围,线性时间)。 - 空间复杂度:o(range)(依赖值域,稀疏数据可能浪费空间)。
- 稳定性:稳定(按值顺序填充,保留原顺序)。
- 适用场景:值域小的整数排序(如 0-100 分成绩排序)。
七、算法对比与选型指南
算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 典型场景 |
---|---|---|---|---|
直接插入排序 | O (n²)(平均) | O(1) | 稳定 | 小规模 / 近乎有序数组 |
希尔排序 | O (n^1.3)(平均) | O(1) | 不稳定 | 中等规模数组 |
堆排序 | O (n log n)(固定) | O(1) | 不稳定 | 大规模数据 / 内存受限场景 |
快速排序 | O (n log n)(平均) | O(log n) | 不稳定 | 通用大规模排序(首选) |
归并排序 | O (n log n)(固定) | O(n) | 稳定 | 稳定性要求高 / 链表排序 |
计数排序 | O (n + k)(线性) | O(k) | 稳定 | 值域小的整数排序(k 较小) |
注:稳定性是假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
选型建议
- 小规模数据:直接插入排序(简单高效,近乎有序时最优)。
- 大规模数据:
- 优先快速排序(平均效率最高,需随机化基准值防最坏情况)。
- 稳定性优先选归并排序,内存紧张选堆排序。
- 特殊场景:
- 整数且值域小:计数排序(线性时间,如成绩、年龄排序)。
- 链表排序:归并排序(无需随机访问,递归更自然)。
结语
排序算法是数据结构的核心基础,每种算法都体现了不同的设计思想:插入排序的增量构建、快速排序的分治分区、计数排序的统计映射等。理解它们的核心逻辑、复杂度特性及适用场景,是在实际开发中做出正确选择的关键。从基础的 O(n²)
算法到高效的 O(n log n)
算法,再到突破比较限制的线性排序,排序算法的演进史也是计算机科学优化思维的缩影。掌握这些知识,不仅能写出更高效的代码,还能培养 “根据问题特性选择合适工具” 的工程思维。
附录
1.比较相同数据各排序算法运行时间的代码
//test.c
void TestOP()
{
srand(time(0));
const int N = 200000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
int* a7 = (int*)malloc(sizeof(int) * N);
int* a8 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
a7[i] = a1[i];
a8[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
QuickSort(a5, 0, N - 1);
int end5 = clock();
int begin6 = clock();
MergeSort(a6, N);
int end6 = clock();
int begin7 = clock();
BubbleSort(a7, N);
int end7 = clock();
int begin8 = clock();
CountSort(a8, N);
int end8 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
printf("BubbleSort:%d\n", end7 - begin7);
printf("CountSort:%d\n", end8 - begin8);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
free(a8);
}
2.栈的代码
//Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
STDataType* arr;
int top; // 栈顶
int capacity; // 容量
}Stack;
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
//Stack.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
// 初始化栈
void StackInit(Stack* ps)
{
ps->arr = NULL;
ps->capacity = ps->top = 0;
}
// 入栈
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
if (ps->capacity == ps->top)
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* tmp = (STDataType*)realloc(ps->arr, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(1);
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
ps->arr[ps->top++] = data;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
// 出栈
void StackPop(Stack* ps)
{
assert(!StackEmpty(ps));
--ps->top;
}
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->arr[ps->top - 1];
}
// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->top;
}
// 销毁栈
void StackDestroy(Stack* ps)
{
if (ps->arr != NULL)
free(ps->arr);
ps->arr = NULL;
ps->capacity = ps->top = 0;
}
3.所有排序算法的代码
//sort.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
//直接选择排序
void SelectSort(int*arr, int n);
//堆排序
void HeapSort(int* arr, int n);
//快速排序
void QuickSort(int* arr, int left, int right);
//归并排序
void MergeSort(int* arr, int n);
//冒泡排序
void BubbleSort(int* arr, int n);
//计数排序
void CountSort(int* arr, int n);
//Sort.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Sort.h"
#include"Stack.h"
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = tmp;
}
}
void ShellSort(int* arr, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end+gap] = tmp;
}
}
}
void SelectSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1; i < end; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
if (maxi == begin)
{
maxi = mini;
}
Swap(&arr[mini], &arr[begin]);
Swap(&arr[maxi], &arr[end]);
begin++;
end--;
}
}
void HPjustDown(int* arr, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && arr[child] > arr[child + 1])
{
child++;
}
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* arr, int n)
{
for (int i = (n - 2) / 2; i >= 0; i--)
{
HPjustDown(arr, i, n);
}
int tmp = n - 1;
while (tmp)
{
Swap(&arr[0], &arr[tmp]);
HPjustDown(arr, 0, tmp);
tmp--;
}
}
//int QuickSort_Find1(int* arr, int left, int right)
//{
// int keyi = left;
// left++;
// while (left <= right)
// {
// while (left <= right && arr[keyi] < arr[right])
// {
// right--;
// }
// while (left <= right && arr[keyi] > arr[left])
// {
// left++;
// }
// if (left <= right)
// {
// Swap(&arr[left++], &arr[right--]);
// }
// }
// Swap(&arr[keyi], &arr[right]);
// return right;
//}
//int QuickSort_Find2(int* arr, int left, int right)
//{
// int keyi = left;
// int prev = left;
// int cur = prev + 1;
// while (cur <= right)
// {
// if (arr[cur] < arr[keyi] && ++prev != cur)
// {
// Swap(&arr[prev], &arr[cur]);
// }
// cur++;
// }
// Swap(&arr[keyi], &arr[prev]);
// return prev;
//}
//void QuickSort(int* arr, int left, int right)
//{
// if (left >= right)
// {
// return;
// }
// int keyi = QuickSort_Find1(arr, left, right);
// QuickSort(arr, left, keyi-1);
// QuickSort(arr, keyi+1, right);
//}
void QuickSort(int* arr, int left, int right)
{
Stack ST;
StackInit(&ST);
StackPush(&ST, right);
StackPush(&ST, left);
while (!StackEmpty(&ST))
{
int begin = StackTop(&ST);
StackPop(&ST);
int end = StackTop(&ST);
StackPop(&ST);
int keyi = begin;
int prev = begin;
int cur = prev + 1;
while (cur <= end)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
cur++;
}
Swap(&arr[keyi], &arr[prev]);
keyi = prev;
if (keyi + 1 < end)
{
StackPush(&ST, end);
StackPush(&ST, keyi+1);
}
if (begin < keyi - 1)
{
StackPush(&ST, keyi - 1);
StackPush(&ST, begin);
}
}
StackDestroy(&ST);
}
void _MergeSort(int* arr, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid+1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] > arr[begin2])
{
tmp[index++] = arr[begin2++];
}
else
{
tmp[index++] = arr[begin1++];
}
}
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
//for (int i = left; i <= right; i++)
//{
// arr[i] = tmp[i];
//}
memcpy(&arr[left], &tmp[left], sizeof(int) * (right - left + 1));
}
void MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(arr, 0, n - 1, tmp);
free(tmp);
}
void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j + 1], &arr[j]);
}
}
}
}
void CountSort(int* arr, int n)
{
int max = arr[0];
int min = arr[0];
int i = 1;
while (i < n)
{
if (arr[i] > max)
{
max = arr[i];
}
if (arr[i] < min)
{
min = arr[i];
}
i++;
}
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("calloc fail");
exit(1);
}
for (i = 0; i < n; i++)
{
count[arr[i] - min]++;
}
int index = 0;
for (i = 0; i < range; i++)
{
while (count[i]--)
{
arr[index++] = i + min;
}
}
}