在排序算法中,稳定性是一个重要的概念,指的是在排序过程中,如果两个元素的值相等,它们在排序后的相对位置与排序前的相对位置保持不变的特性。
稳定排序:在排序时,相等的元素的相对顺序不会改变。例如,在对一组学生按成绩排序时,如果两个学生的成绩相同,它们在排序后的顺序与原来顺序相同(例如,原来是学生 A 和 B,排序后仍然是 A 和 B)。
不稳定排序:在排序时,相等的元素的相对顺序可能会改变。例如,如果两个成绩相同的学生在排序后顺序发生变化,则该排序算法是不稳定的。
稳定性在某些情况下非常重要,特别是当需要多次排序时。例如,如果你首先按姓氏排序,然后按名字排序,稳定排序可以确保在按名字排序时,同一姓氏的人的顺序不会被打乱。
名词解释:
冒泡排序(Bubble Sort)是一种简单的排序算法,它通过重复比较相邻的元素并交换它们的顺序,直到整个数组有序。
冒泡排序需要进行 n-1 轮比较,其中 n 是待排序数组的元素个数。
在最坏情况下(如逆序排列),总比较次数为:
(n−1)+(n−2)+(n−3)+…+1 =
这个公式是等差数列求和公式的结果。
冒泡排序是原地排序算法,空间复杂度为 O(1)。
冒泡排序是一种稳定的排序算法。在冒泡排序中,当相邻的两个元素相等时,算法不会改变它们的相对顺序,因为只有在第一个元素大于第二个元素时才会进行交换。因此,相等元素的相对位置在排序后保持不变。
#include
#include
#include
// 随机生成数组函数
int* Generate_Random_Array(int n, int min, int max)
{
int f = -1;
if (min < 0)
f = 1;
int* array = (int*)malloc(n * sizeof(int)); // 动态分配内存
if (array == NULL)
exit(-1);
for (int i = 0; i < n; i++)
{
array[i] = min + rand() % (max + f * min + 1);// 生成 min 到 max 之间的随机数
}
return array;
}
// 复制数组函数
int* Copy_Array(int* source, int n)
{
int* copy = (int*)malloc(n * sizeof(int));
if (copy == NULL)
exit(-1);
for (int i = 0; i < n; i++)
{
copy[i] = source[i];
}
return copy;
}
// 排序计时函数
void SortTime(void(*p)(int*, int, int), int* arr, int n, int isAscending)
{
// 记录开始时间
clock_t start = clock();
p(arr, n, isAscending);//调用排序函数
// 记录结束时间
clock_t end = clock();
// 计算并打印排序时间
double time_taken = (double)(end - start) / CLOCKS_PER_SEC;
printf("排序所需时间: %f 秒\n", time_taken);
}
// 打印数组的函数
void print(int* p, int n)
{
for (int i = 0; i < n; ++i) // 遍历数组
{
printf("%d ", p[i]);
}
printf("\n");
}
// 冒泡排序函数最初版
void Bubble_Sort1(int* p, int n, int isAscending)
{
// 外层循环,控制总轮数
for (int i = n - 1; i > 0; --i)
{
// 内层循环,进行相邻元素比较和交换
for (int j = 0; j < i; ++j)
{
// 根据升序或降序规则进行比较
if ((p[j] > p[j + 1] && isAscending) || (p[j] < p[j + 1] && !isAscending))
{
// 交换元素
int tmp = p[j];
p[j] = p[j + 1];
p[j + 1] = tmp;
}
}
}
}
// 冒泡排序函数优化版(增加isSorted标志位,用于判断每轮循环后是否有序,避免已经有序还在排序的情况,减少了比较次数)
void Bubble_Sort2(int* p, int n, int isAscending)
{
// 外层循环,控制总轮数
for (int i = n - 1; i > 0; --i)
{
int isSorted = 1;//重置有序标志
// 内层循环,进行相邻元素比较和交换
for (int j = 0; j < i; ++j)
{
// 根据升序或降序规则进行比较
if ((p[j] > p[j + 1] && isAscending) || (p[j] < p[j + 1] && !isAscending))
{
// 交换元素
int tmp = p[j];
p[j] = p[j + 1];
p[j + 1] = tmp;
isSorted = 0; //无序标志
}
}
// 如果一轮中没有发生交换,说明数组已经有序,提前退出
if (isSorted)
break;
}
}
// 冒泡排序函数最优版(增加了SortInidex用于记录序列尾部局部有序时最后一次交换的位置,通过赋值给i改变循环的轮数,减少了比较次数)
void Bubble_Sort3(int* p, int n, int isAscending)
{
// 外层循环,控制总轮数
for (int i = n - 1; i > 0; --i)
{
int SortInidex = 0;//该初始值是为数组完全有序时循环提前退出准备的(该值小于等于0时在外层循环判断为false)
// 内层循环,进行相邻元素比较和交换
for (int j = 0; j < i; ++j)
{
// 根据升序或降序规则进行比较
if ((p[j] > p[j + 1] && isAscending) || (p[j] < p[j + 1] && !isAscending))
{
// 交换元素
int tmp = p[j];
p[j] = p[j + 1];
p[j + 1] = tmp;
SortInidex = j + 1;// 记录最后一次交换的位置
}
}
// 更新 i 为最后一次交换的位置,以缩小未排序的范围
// 这意味着在此位置之后的元素已排序
i = SortInidex;
}
}
// 冒泡排序函数改良版鸡尾酒排序
void Cocktail_Sort(int* p, int n, int isAscending)
{
// 交换临时变量
int tmp = 0;
// 无序数列的左边界
int leftBorder = 0;
// 无序数列的右边界
int rightBorder = n - 1;
// 外层循环,控制排序的轮数
for (int i = 0; i < n / 2; i++)
{
// 记录右侧最后一次交换的位置
int lastRightExchange = leftBorder; // 初始值设为左边界
// 奇数轮,从左向右比较和交换
for (int j = leftBorder; j < rightBorder; j++)
{
// 根据升序或降序规则进行比较
if ((p[j] > p[j + 1] && isAscending) || (p[j] < p[j + 1] && !isAscending))
{
// 交换元素
tmp = p[j];
p[j] = p[j + 1];
p[j + 1] = tmp;
// 记录最后一次交换的位置
lastRightExchange = j;
}
}
// 更新右边界为最后一次交换的位置
rightBorder = lastRightExchange;
// 记录左侧最后一次交换的位置
int lastLeftExchange = n; // 初始值设为 n,表示未发生交换
// 偶数轮,从右向左比较和交换
for (int j = rightBorder; j > leftBorder; j--)
{
// 根据升序或降序规则进行比较
if ((p[j] < p[j - 1] && isAscending) || (p[j] > p[j - 1] && !isAscending))
{
// 交换元素
tmp = p[j];
p[j] = p[j - 1];
p[j - 1] = tmp;
// 记录最后一次交换的位置
lastLeftExchange = j;
}
}
// 更新左边界为最后一次交换的位置
leftBorder = lastLeftExchange;
}
}
// 主函数
int main()
{
//排序函数测试
int num = 20;
int* p = Generate_Random_Array(num, -9, 100);
printf("原数组:");
print(p, num);
Bubble_Sort1(p, num, 1);
printf("Bubble_Sort1升序:");
print(p, num);
Bubble_Sort1(p, num, 0);
printf("Bubble_Sort1降序:");
print(p, num);
Bubble_Sort2(p, num, 1);
printf("Bubble_Sort2升序:");
print(p, num);
Bubble_Sort2(p, num, 0);
printf("Bubble_Sort2降序:");
print(p, num);
Bubble_Sort3(p, num, 1);
printf("Bubble_Sort3升序:");
print(p, num);
Bubble_Sort3(p, num, 0);
printf("Bubble_Sort3降序:");
print(p, num);
Cocktail_Sort(p, num, 1);
printf("Cocktail_Sort升序:");
print(p, num);
Cocktail_Sort(p, num, 0);
printf("Cocktail_Sort降序:");
print(p, num);
//排序时间测试
int n = 20000;
printf("\n\n排序时间测试,排序个数:%d\n", n);
int* arr = Generate_Random_Array(n, -100, 100000);
int* arr1 = Copy_Array(arr, n);
int* arr2 = Copy_Array(arr, n);
int* arr3 = Copy_Array(arr, n);
int* arr4 = Copy_Array(arr, n);
printf("Bubble_Sort1");
SortTime(Bubble_Sort1, arr1, n, 1);
printf("Bubble_Sort2");
SortTime(Bubble_Sort2, arr2, n, 1);
printf("Bubble_Sort3");
SortTime(Bubble_Sort3, arr3, n, 1);
printf("Cocktail_Sort");
SortTime(Cocktail_Sort, arr4, n, 1);
// 释放动态分配的内存
free(arr);
free(arr1);
free(arr2);
free(arr3);
free(arr4);
return 0;
}
基本原理:
遍历方向:
效率:
选择排序是一种简单的排序算法,它通过不断选择未排序部分中的最小(或最大)元素,并将其放到已排序部分的末尾,最终实现整个数组的排序。
选择排序需要进行 n-1 轮比较,其中 n 是待排序数组的元素个数。
选择排序的总比较次数为:
(n−1)+(n−2)+(n−3)+…+1 =
这个公式是等差数列求和公式的结果。
选择排序是原地排序算法,空间复杂度为 O(1)。
选择排序是一种不稳定的排序算法。在选择最小元素的过程中,相等元素的相对顺序可能会改变,因为最小元素的交换可能会导致相等元素的顺序发生变化。
在一般情况下,选择排序的效率通常会优于冒泡排序,尤其是在需要减少交换次数时。然而,在大规模数据排序时,这两种算法都不是最佳选择,更高效的排序算法(如快速排序、归并排序或堆排序)更为适合。
双向选择排序是一种改进的选择排序,它在每一轮中同时选择未排序部分的最小和最大元素。这种方法可以减少排序所需的总轮数。
双向选择排序其基本思想是同时在未排序部分找到最小和最大元素,并将它们放置到已排序部分的两端。
分区:
同时选择:
假设我们有一个数组 arr = {5, 3, 8, 6, 2}
,我们想要进行升序排序:
第一次选择:
{5, 3, 8, 6, 2}
中找到最小值 2
和最大值 8
。2
放到数组的开头,将 8
放到数组的末尾。{2, 3, 6, 5, 8}
(已排序部分为 {2}
,未排序部分为 {3, 6, 5}
)。第二次选择:
{3, 6, 5}
中找到最小值 3
和最大值 6
。3
放到已排序部分的末尾(即当前最小位置),将 6
放到数组的末尾。{2, 3, 5, 6, 8}
(已排序部分为 {2, 3}
,未排序部分为空)。#include "All_Sort.h"
// 选择排序函数
void Selection_Sort(int* arr, int n, int isAscending)
{
for (int i = 0; i < n - 1; i++)
{
// 假设当前元素是最小/大值
int Index = i;
for (int j = i + 1; j < n; j++)
{
if (arr[j] < arr[Index] && isAscending || arr[j] > arr[Index] && !isAscending)
Index = j;// 更新最小/大值的索引
}
// 交换当前元素与最小/大值
if (Index != i)
{
int temp = arr[i];
arr[i] = arr[Index];
arr[Index] = temp;
}
}
}
// 双向选择排序函数
void Bidirectional_Selection_Sort(int* arr, int n, int isAscending)
{
for (int i = 0; i < n / 2; i++)
{
int minIndex = i;
int maxIndex = i;
for (int j = i + 1; j < n - i; j++)
{
if (arr[j] < arr[minIndex] && !isAscending || arr[j] > arr[minIndex] && isAscending)
minIndex = j;
if (arr[j] > arr[maxIndex] && !isAscending || arr[j] < arr[maxIndex] && isAscending)
maxIndex = j;
}
// 交换最小/大元素
if (minIndex != i)
{
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
// 交换最大/小元素(注意 maxIndex 的位置可能已变)
if (maxIndex == i)
maxIndex = minIndex; // 如果最小/大元素在 maxIndex 位置,要更新 maxIndex
if (maxIndex != n - 1 - i)
{
int temp = arr[n - 1 - i];
arr[n - 1 - i] = arr[maxIndex];
arr[maxIndex] = temp;
}
}
}
以下是关键的代码段:
// 交换最大/小元素(注意 maxIndex 的位置可能已变)
if (maxIndex == i)
maxIndex = minIndex; // 如果最小/大元素在 maxIndex 位置,要更新 maxIndex
初始假设:
i
是未排序部分的最小值。maxIndex
也是当前未排序部分的最大值。找到最小和最大元素:
minIndex
和 maxIndex
的值,以找到未排序部分的最小值和最大值。交换:
minIndex
)到当前的 i
位置。i
是最大值的位置(即 maxIndex == i
),那么在交换最小值后,最大值的位置可能会发生变化。maxIndex
maxIndex
:
i
位置时,原本在 i
位置的元素(即最大值)现在位于未排序部分的其他位置。这时,maxIndex
指向的可能是一个不再是最大值的位置。maxIndex
更新为 minIndex
的位置,以确保在后续的交换中,我们能正确交换最大值。假设我们有一个数组 arr = {5, 3, 8, 6, 2}
,我们想要进行升序排序。
初始状态:
minIndex = 0
(指向 5
)maxIndex = 0
(指向 5
)内部循环(查找最小值和最大值):
arr[1] = 3
→ minIndex
更新为 1
(3 是当前最小值)arr[2] = 8
→ maxIndex
更新为 2
(8 是当前最大值)arr[3] = 6
→ maxIndex
仍为 2
(8 仍然是最大值)arr[4] = 2
→ minIndex
更新为 4
(2 是当前最小值)找到的最小和最大:
2
(索引 4
),最大值 8
(索引 2
)。交换最小值:
2
交换到位置 0
:{2, 3, 8, 6, 5}
。更新 maxIndex
:
8
的原始位置是 2
,但现在数组的结构已经改变。0
被交换,原本在 0
位置的 5
现在在未排序部分的其他位置。maxIndex
仍然指向 2
,我们需要检查这个索引是否仍然是最大值。maxIndex
没有指向 i
(即 maxIndex
仍然是 2
),我们继续。交换最大值:
8
(最大值)交换到未排序部分的末尾(即位置 4
):{2, 3, 5, 6, 8}
。maxIndex
在某些情况下,如果最小值在 maxIndex
位置(例如如果初始数组为 {8, 3, 5, 6, 2}
),而我们在交换最小值后,最大值的位置可能会被改变:
假设数组为 {8, 3, 5, 6, 2}
:
minIndex = 4
(指向 2
),maxIndex = 0
(指向 8
)。{2, 3, 5, 6, 8}
。maxIndex
仍然指向 0
(原始最大值的位置),而 8
被交换到了末尾。更新 maxIndex
:
minIndex
是 4
(当前最小值的索引)与 maxIndex
是 0(
当前最大值的索引) 交换了
,所有需要更新 maxIndex
为 minIndex
的位置(因为 8
已经不在原来的位置)。 更新 maxIndex
是为了确保在后续的交换中,我们能正确交换最大值。通过这个例子,可以看到在交换最小值后,原本的最大值位置可能会发生变化,因此需要根据新的数组状态进行更新,以确保最大值的正确处理。
堆排序(Heap Sort)是一种基于堆数据结构的排序算法,通过构建最大堆(或最小堆)来实现排序。其主要思想是利用堆的特性来进行排序。堆是一种特殊的完全二叉树,其中每个节点的值都大于或等于(最大堆)或小于或等于(最小堆)其子节点的值。
对于一个索引为 i
的节点,其左右子节点的索引可以通过以下规则计算:
左子节点:在数组中,左子节点总是位于当前节点的下方,并且在同一层的节点之前。因此,左子节点的索引为:
left = 2 *
i
+ 1
右子节点:右子节点位于当前节点的下方,并且在左子节点之后。因此,右子节点的索引为:
right = 2 *
i
+ 2
这种关系的原因如下:
考虑以下完全二叉树及其对应的数组表示:
0
/ \
1 2
/ \ / \
3 4 5 6
0
的索引是 0
。1
的索引是 2 * 0 + 1 = 1
。2
的索引是 2 * 0 + 2 = 2
。1
的左子节点 3
的索引是 2 * 1 + 1 = 3
,右子节点 4
的索引是 2 * 1 + 2 = 4
。2
的左子节点 5
的索引是 2 * 2 + 1 = 5
,右子节点 6
的索引是 2 * 2 + 2 = 6
。叶子节点的起始索引:
n
个节点的完全二叉树中,叶子节点的起始索引为 n/2
(向下取整)。n = 7
,叶子节点的索引为 3
, 4
, 5
, 6
(即 n/2 = 3.5
,取整为 3
)。最后一个非叶子节点的索引:
n/2 - 1
。n = 7
,最后一个非叶子节点的索引为 3 - 1 = 2
,对应的节点值为 2
。假设有一个包含 8 个节点的完全二叉树(n = 8
):
0
/ \
1 2
/ \ / \
3 4 5 6
/
7
8/2 = 4
,对应的叶子节点为 4
, 5
, 6
, 7
(索引 4
, 5
, 6
, 7
)。8/2 - 1 = 3
,对应的节点是 3
。n/2 - 1
是基于完全二叉树的结构特性。定义:在最大堆中,每个节点的值都大于或等于其子节点的值,根节点是最大值。
定义:在最小堆中,每个节点的值都小于或等于其子节点的值,根节点是最小值。
n/2 - 1
。heapify
函数,确保以该节点为根的子树满足最大/小堆的性质。void buildHeap(int* arr, int n, int IsMax)
{
for (int i = n / 2 - 1; i >= 0; i--)
{
heapify(arr, n, i, IsMax);
}
}
n / 2 - 1
:从最后一个非叶子节点开始。for
循环:从这个索引向下到 0
,确保整个树的每个非叶子节点都被堆化。i
:
i
是 0(根节点),则没有父节点。(i - 1) / 2
。// 构建堆函数递归法
void heapify_recursion(int* arr, int n, int i, int IsMax)
{
int m = i; // 假设当前节点为最大/小值
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 如果左子节点比当前最大值大/小,更新最大/小
if (left < n && ((IsMax && arr[left] > arr[m]) || (!IsMax && arr[left] < arr[m])))
{
m = left;
}
// 如果右子节点比当前最大值大/小,更新最大/小值
if (right < n && ((IsMax && arr[right] > arr[m]) || (!IsMax && arr[right] < arr[m])))
{
m = right;
}
// 如果最大值不是当前节点,进行交换并继续堆化
if (m != i)
{
// 交换当前节点与找到的最大/小值节点
int temp = arr[i];
arr[i] = arr[m];
arr[m] = temp;
// 递归堆化受影响的子树
heapify_recursion(arr, n, m, IsMax);
}
}
// 构建堆函数递迭代法
void heapify_iterate(int* arr, int n, int i, int IsMax)
{
while (1)
{
int m = i; // 假设当前节点为最大/小值
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 如果左子节点比当前最大值大/小,更新最大/小
if (left < n && ((IsMax && arr[left] > arr[m]) || (!IsMax && arr[left] < arr[m])))
{
m = left;
}
// 如果右子节点比当前最大值大/小,更新最大/小值
if (right < n && ((IsMax && arr[right] > arr[m]) || (!IsMax && arr[right] < arr[m])))
{
m = right;
}
// 如果最大值不是当前节点,进行交换并继续堆化
if (m != i)
{
// 交换当前节点与找到的最大/小值节点
int temp = arr[i];
arr[i] = arr[m];
arr[m] = temp;
// 更新当前节点为被交换的子节点
i = m; // 更新索引为最大(或最小)子节点
}
else
{
break;// 如果没有交换,结束循环
}
}
}
// 排序最大/小堆的函数
void heap_sort_recursion(int* arr, int n, int IsMax)
{
// 第一步:建立最大堆
for (int i = n / 2 - 1; i >= 0; i--)
{
heapify_recursion(arr, n, i, IsMax);
}
// 第二步:一个一个从堆中取出元素
for (int i = n - 1; i > 0; i--)
{
// 将当前最大/小值移动到数组末尾
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
heapify_recursion(arr, i, 0, IsMax); // 重新堆化根节点
}
}
void heap_sort_iterate(int* arr, int n, int IsMax)
{
// 第一步:建立最大/小堆
for (int i = n / 2 - 1; i >= 0; i--)
{
heapify_iterate(arr, n, i, IsMax);
}
// 第二步:一个一个从堆中取出元素
for (int i = n - 1; i > 0; i--)
{
// 将当前最大/小值移动到数组末尾
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
heapify_iterate(arr, i, 0, IsMax); // 重新堆化根节点
}
}
堆排序的基本步骤如下:
构建最大/小堆:
排序过程:
堆排序是一种不稳定的排序算法。在排序过程中,可能会改变相等元素的相对位置,因为堆的调整过程依赖于树的结构。
作用:将一个节点向下移动,以维护堆的性质。通常在删除堆顶元素(最大值或最小值)时使用。
作用:将一个节点向上移动,以维护堆的性质。通常在插入新元素时使用。
shiftUp
:
shiftUp
在最坏情况下需要沿着树的高度向上移动,树的高度为 log(n),因此时间复杂度为 O(log n)。shiftDown
:
shiftDown
也在最坏情况下需要沿着树的高度向下移动,时间复杂度同样为 O(log n)。shiftDown
可能在某些情况下更加频繁地进行交换,因为它需要检查两个子节点,而 shiftUp
只需检查一个父节点。插入排序(Insertion Sort)是一种简单的排序算法,它通过构建一个有序的序列,将待排序的元素逐个插入到已排序的序列中,直到整个数组有序。
插入排序是原地排序算法,空间复杂度为 O(1)。
插入排序是一种稳定的排序算法。在插入过程中,相邻的两个相等元素的相对顺序不会改变,因为只有在第一个元素大于第二个元素时才会进行移动。
#include "All_Sort.h"
//插入排序
void Insert_Sort0(int* p, int n, int isAscending)
{
for (int i = 1; i < n; ++i)
{
int cur = i;
while ((cur > 0) && (p[cur] < p[cur - 1] && isAscending || p[cur] > p[cur - 1] && !isAscending))
{
int temp = p[cur];
p[cur] = p[cur - 1];
p[cur - 1] = temp;
--cur;
}
}
}
//插入排序优化交换为移动
void Insert_Sort1(int* p, int n, int isAscending)
{
for (int i = 1; i < n; ++i)
{
int key = p[i]; // 当前要插入的元素
int j = i - 1; // 已排序部分的最后一个索引
// 根据排序方式调整插入逻辑
while (j >= 0 && ((isAscending && p[j] > key) || (!isAscending && p[j] < key)))
{
p[j + 1] = p[j]; // 移动元素
j--; // 向前移动
}
p[j + 1] = key; // 插入元素
}
}
// 二叉搜索返回要插入的元素的下标
int Binary_Search_Index(int* p, int n, int v, int isAscending)
{
int begin = 0;
int end = n; // 注意这里的 end 应为 n
while (begin < end)
{
int mid = (begin + end) >> 1; // 相当于 (begin + end) / 2
if ((v < p[mid] && isAscending) || (v > p[mid] && !isAscending))
{
end = mid; // 找到左侧
}
else
{
begin = mid + 1; // 找到右侧
}
}
return begin; // 返回插入的位置
}
// 插入排序优化为二分查找
void Insert_Sort2(int* p, int n, int isAscending)
{
for (int i = 1; i < n; ++i)
{
int index = Binary_Search_Index(p, i, p[i], isAscending);
int key = p[i]; // 备份当前要插入的元素
// 根据排序方式调整插入逻辑
for (int j = i - 1; j >= index; --j)
{
p[j + 1] = p[j]; // 移动元素
}
p[index] = key; // 插入元素
}
}
归并排序是一种基于分治法的排序算法,具有较好的时间复杂度和稳定性。它的基本思想是将数组分成两个子数组,分别进行排序,然后将已排序的子数组合并成一个有序的数组。
归并排序是一种稳定的排序算法,因为相同元素的相对顺序在排序后保持不变。
#include "All_Sort.h"
void Merge(int* p, int* arr, int begin, int mid, int end, int isAscending)
{
int li = 0; // 左半部分数组开始下标
int le = mid - begin + 1; // 左半部分数组结束下标
int ri = mid + 1; // 右半部分数组开始下标
int ai = begin; // 将来要覆盖的位置下标
// 备份数组左半部分
for (int i = 0; i < le; i++)
{
arr[i] = p[begin + i];
}
// 合并两个已排序的部分
while (li < le && ri <= end)
{
if (arr[li] <= p[ri] && isAscending || arr[li] >= p[ri] && !isAscending)
{
p[ai++] = arr[li++];
}
else
{
p[ai++] = p[ri++];
}
}
// 复制剩余左半部分
while (li < le)
{
p[ai++] = arr[li++];
}
// 右半部分不需要复制,因为它已经在原数组中
}
//归并排序递归法
void Merge_Sort_recursion(int* p, int* arr, int begin, int end, int isAscending)
{
if (begin < end)
{
int mid = begin + (end - begin) / 2; // 防止整数溢出
// 递归排序左半部分
Merge_Sort_recursion(p, arr, begin, mid, isAscending);
// 递归排序右半部分
Merge_Sort_recursion(p, arr, mid + 1, end, isAscending);
// 合并已排序的部分
Merge(p, arr, begin, mid, end, isAscending);
}
}
void Merge_Sort0(int* p, int n, int isAscending)
{
// 创建临时数组
int* arr = (int*)malloc(sizeof(int) * (n / 2 + 1)); // 只分配一次
if (arr == NULL) {
perror("Failed to allocate memory");
exit(EXIT_FAILURE);
}
int begin = 0;
Merge_Sort_recursion(p, arr, begin, n - 1, isAscending);
// 释放临时数组
free(arr);
}
void Merge1(int* p, int* arr, int begin, int mid, int end, int isAscending)
{
int li = begin; // 左半部分的开始索引
int ri = mid + 1; // 右半部分的开始索引
int ai = begin; // 最终合并数组的索引
// 合并两个已排序的部分
while (li <= mid && ri <= end)
{
if ((p[li] <= p[ri] && isAscending) || (p[li] >= p[ri] && !isAscending))
{
arr[ai++] = p[li++];
}
else
{
arr[ai++] = p[ri++];
}
}
// 复制剩余左半部分
while (li <= mid)
{
arr[ai++] = p[li++];
}
// 复制剩余右半部分
while (ri <= end)
{
arr[ai++] = p[ri++];
}
}
// 非递归归并排序
void Merge_Sort1(int* p, int n, int isAscending)
{
// 为归并过程分配一个临时数组
int* arr = (int*)malloc(sizeof(int) * n); // 全部分配一次
if (arr == NULL) {
perror("Failed to allocate memory"); // 检查内存分配是否成功
exit(EXIT_FAILURE);
}
// 从大小为1的子数组开始,逐步增大子数组的大小
for (int size = 1; size < n; size *= 2)
{
// 遍历数组,合并相邻的子数组
for (int begin = 0; begin < n; begin += 2 * size)
{
// 计算子数组的中间位置和结束位置
int mid = (begin + size - 1 < n - 1) ? (begin + size - 1) : (n - 1);
int end = (begin + 2 * size - 1 < n - 1) ? (begin + 2 * size - 1) : (n - 1);
// 合并子数组 [begin, mid] 和 [mid + 1, end]
Merge1(p, arr, begin, mid, end, isAscending);
}
// 将合并后的结果复制回原数组
for (int i = 0; i < n; i++)
{
p[i] = arr[i]; // 更新原数组为合并后的结果
}
}
free(arr); // 释放临时数组的内存
}
归并排序是一种高效的排序算法,具有以下优缺点:
稳定性:
时间复杂度:
适用于大规模数据:
分治思想:
可并行化:
空间复杂度:
不适合小规模数据:
实现复杂性:
递归调用开销:
归并排序是一种高效且稳定的排序算法,适合大规模数据的排序,但其空间复杂度和实现复杂性可能在某些情况下限制其使用。
快速排序(Quick Sort)是一种常用的排序算法,采用分治法策略对数据进行排序。
快速排序的基本步骤如下:
选择基准:
从数组中选择一个元素作为“基准”(pivot)。常见的选择方法包括选择第一个元素、最后一个元素、中间元素或随机选择。分区操作:
将数组重新排列,使得所有小于基准的元素放在基准的左侧,所有大于基准的元素放在基准的右侧。此时基准元素的位置就是它在最终排序数组中的位置。递归/迭代排序:
对基准左侧和右侧的子数组分别递归/迭代应用快速排序。终止条件:
当子数组的大小为1或0时,递归终止。时间效率高:
在大多数情况下,快速排序比其他 O(nlogn) 的排序算法(如归并排序和堆排序)更快。原地排序:
快速排序只需要少量的额外空间,通常是 O(logn),因此适合大规模数据。最坏情况性能差:
在某些情况下(如已排序数组),快速排序的性能会降到 O(n^2)。递归深度:
递归调用的深度可能导致栈溢出,尤其是在处理大数据集时。不稳定排序:
快速排序不保证相等元素的相对顺序不变。#include "All_Sort.h"
// 分区函数
int Partition(int* arr, int low, int high, int isAscending)
{
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = low - 1; // 小于基准的元素索引
for (int j = low; j < high; j++)
{
if (arr[j] < pivot && isAscending || arr[j] > pivot && !isAscending)
{
i++;
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 交换 arr[i + 1] 和 arr[high](基准)
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
// 快速排序递归函数
void Quick_Sort_D(int* arr, int low, int high,int isAscending)
{
if (low < high)
{
// 分区操作
int pivotIndex = Partition(arr, low, high, isAscending);
// 递归排序基准左侧和右侧的子数组
Quick_Sort_D(arr, low, pivotIndex - 1, isAscending);
Quick_Sort_D(arr, pivotIndex + 1, high, isAscending);
}
}
void Quick_Sort0(int* p, int n, int isAscending)
{
int low = 0;
Quick_Sort_D(p, low, n - 1, isAscending);
}
// 快速排序非递归函数
void Iterative_Quick_Sort(int* arr, int n, int isAscending)
{
int* stack = (int*)malloc(sizeof(int) * n); // 创建栈
int top = -1; // 栈顶指针
// 将初始的整个数组范围推入栈
stack[++top] = 0;
stack[++top] = n - 1;
// 当栈不为空时
while (top >= 0)
{
// 弹出范围
int high = stack[top--];
int low = stack[top--];
// 进行分区
int pivotIndex = Partition(arr, low, high, isAscending);
// 如果基准左侧还有元素,推入栈
if (pivotIndex - 1 > low)
{
stack[++top] = low;
stack[++top] = pivotIndex - 1;
}
// 如果基准右侧还有元素,推入栈
if (pivotIndex + 1 < high)
{
stack[++top] = pivotIndex + 1;
stack[++top] = high;
}
}
free(stack); // 释放栈内存
}
希尔排序(Shell Sort)是一种基于插入排序的排序算法,通过将待排序的数组分成多个子序列来进行排序,从而提升插入排序的效率。希尔排序是非稳定的排序算法,其时间复杂度取决于所选择的间隔序列。
选择间隔:
初始时,选择一个间隔(gap),将数组分为若干个子序列。每个子序列由间隔为 gap 的元素组成。排序子序列:
对每个子序列进行插入排序。这一步骤可以看作是对多个小数组进行排序。缩小间隔:
逐步减小间隔,直到间隔为 1,此时整个数组已基本有序。最终插入排序:
对最后一个间隔为 1 的子序列进行一次插入排序,完成排序。希尔排序的平均时间复杂度取决于所使用的间隔序列(gap sequence)。以下是一些常见间隔序列对应的平均时间复杂度:
简单间隔序列(例如:每次将间隔减半):
Hibbard 间隔序列(1, 3, 7, 15, ...,即 2^k - 1):
Sedgewick 间隔序列(例如:1, 5, 19, 41, ...):
Knuth 间隔序列(1, 4, 13, 40, ...,即 (3^k - 1) / 2):
#include "All_Sort.h"
// 希尔排序函数
void Shell_Sort0(int* arr, int n, int isAscending)
{
// 初始间隔
for (int gap = n / 2; gap > 0; gap /= 2)
{
// 对每个间隔进行插入排序
for (int i = gap; i < n; i++)
{
int temp = arr[i];
int j = 0;
// 底层使用插入排序 因为插入排序的时间复杂度与逆序对个数成正比
for (j = i; j >= gap && (arr[j - gap] > temp && isAscending || arr[j - gap] < temp && !isAscending); j -= gap)
{
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
// 计算 Knuth 间隔序列
void Knuth_Gaps(int n, int* gaps, int* gap_count)
{
int k = 0;
int gap = 1;
while (gap < n)
{
gaps[k++] = gap;
gap = gap * 3 + 1; // 生成 (3^k - 1) / 2 的序列
}
*gap_count = k; // 返回间隔个数
}
// Knuth 希尔排序函数
void Shell_Sort1(int* arr, int n, int isAscending)
{
int gaps[100]; // 存储间隔序列
int gap_count = 0;
// 计算间隔序列
Knuth_Gaps(n, gaps, &gap_count);
// 从大的间隔开始排序
for (int k = gap_count - 1; k >= 0; k--)
{
int gap = gaps[k]; // 当前间隔
// 对每个间隔进行插入排序
for (int i = gap; i < n; i++)
{
int temp = arr[i];
int j = i;
// 使用插入排序进行排序
while (j >= gap && ((isAscending && arr[j - gap] > temp) || (!isAscending && arr[j - gap] < temp)))
{
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp; // 插入当前元素
}
}
}
较快的排序速度:
相比于简单的排序算法(如冒泡排序和插入排序),希尔排序在处理较大的数据集时通常更快,尤其是当数据部分有序时。原地排序:
希尔排序是一种原地排序算法,不需要额外的存储空间,除了几个辅助变量。适用范围广:
不仅适用于小型数据集,也能有效处理中等规模的数据集。不稳定排序:
希尔排序在交换元素时可能会改变相同元素的相对顺序,因此不是稳定的排序算法。时间复杂度不确定:
希尔排序的时间复杂度依赖于间隔序列的选择。最坏情况下时间复杂度为 O(n^2),而使用良好的间隔序列时可以达到 O(nlog2n)。但没有统一的标准,可能导致性能不稳定。间隔序列的选择:
不同的间隔序列对排序效率有很大影响,选择合适的间隔序列需要经验,且没有一种通用的方法。对于非常大的数据集性能有限:
尽管希尔排序比简单排序算法快,但对于非常大的数据集,仍然不如一些高级排序算法(如快速排序、归并排序)高效。计数排序是一种非比较的排序算法,适用于范围有限的整数排序。它通过计算每个元素出现的次数,进而确定每个元素在排序后位置的算法。
计数排序的时间复杂度与待排序数组的元素个数 n 和元素值的范围 k 相关,通常表示为 O(n + k)。
计数排序需要额外的空间来存储计数数组,因此空间复杂度为 O(k),其中 k 是元素值的范围。
计数排序是一种稳定的排序算法。相同元素的相对顺序在排序后保持不变,因为我们在填充输出数组时是按照计数数组的顺序进行的。
优点
缺点
#include "All_Sort.h"
void Count_Sort(int* p, int n, int isAscending)
{
// 找到最大值和最小值
int max = p[0];
int min = p[0];
for (int i = 1; i < n; ++i)
{
if (p[i] > max)
max = p[i];
if (p[i] < min)
min = p[i];
}
// 计算范围
int range = max - min + 1;
int* c = (int*)calloc(range, sizeof(int)); // 创建计数数组
if (NULL == c)
exit(-1);
// 统计原数组数值出现的次数
for (int i = 0; i < n; i++)
{
c[p[i] - min]++; // 使用偏移量处理小于0的值
}
// 根据计数数组构建输出数组
int j = 0;
if (isAscending)
{
for (int i = 0; i < range; i++)
{
while (c[i]--)
{
p[j++] = i + min; // 填充回原数组
}
}
}
else
{
// 降序排序
for (int i = range - 1; i >= 0; i--)
{
while (c[i]--)
{
p[j++] = i + min; // 填充回原数组
}
}
}
free(c); // 释放计数数组的内存
}
void Count_Sort1(int* p, int n, int isAscending)
{
// 找到最大值和最小值
int max = p[0];
int min = p[0];
for (int i = 1; i < n; ++i)
{
if (p[i] > max)
max = p[i];
if (p[i] < min)
min = p[i];
}
// 计算范围
int range = max - min + 1;
int* c = (int*)calloc(range, sizeof(int)); // 创建计数数组
if (c == NULL)
exit(-1);
// 统计原数组数值出现的次数
for (int i = 0; i < n; i++)
{
c[p[i] - min]++;
}
// 根据计数数组构建输出数组
int* output = (int*)malloc(n * sizeof(int)); // 创建输出数组
if (output == NULL)
{
free(c);
exit(-1);
}
// 计算累积计数
if (isAscending)
{
for (int i = 1; i < range; i++)
{
c[i] += c[i - 1]; // 累加计数
}
// 从后往前填充输出数组,以保持稳定性
for (int i = n - 1; i >= 0; i--)
{
output[c[p[i] - min] - 1] = p[i];
c[p[i] - min]--; // 减少计数
}
}
else
{
for (int i = range - 2; i >= 0; i--)
{
c[i] += c[i + 1]; // 反向累加计数
}
// 从后往前填充输出数组,以保持稳定性
for (int i = n - 1; i >= 0; i--)
{
output[c[p[i] - min] - 1] = p[i];
c[p[i] - min]--; // 减少计数
}
}
// 将排序后的数据拷贝回原数组
for (int i = 0; i < n; i++)
{
p[i] = output[i];
}
free(c); // 释放计数数组的内存
free(output); // 释放输出数组的内存
}
计算累积计数是计数排序中的一个重要步骤,它的目的是为了确定每个元素在最终排序结果中应该放置的位置,保证了排序的稳定性。
假设我们有以下数组需要排序:
arr = [4, 2, 2, 8, 3, 3, 1]
我们要对这个数组进行升序排序。
首先,我们需要找到数组中的最大值和最小值:
max
) = 8min
) = 1计算值的范围:
range = max - min + 1 = 8 - 1 + 1 = 8
创建一个大小为 range
的计数数组并初始化为0:
c = [0, 0, 0, 0, 0, 0, 0, 0] // 对应 1, 2, 3, 4, 5, 6, 7, 8
遍历原数组,统计每个元素出现的次数:
对于 arr[0] = 4: c[4-1]++ -> c[3]++
对于 arr[1] = 2: c[2-1]++ -> c[1]++
对于 arr[2] = 2: c[2-1]++ -> c[1]++
对于 arr[3] = 8: c[8-1]++ -> c[7]++
对于 arr[4] = 3: c[3-1]++ -> c[2]++
对于 arr[5] = 3: c[3-1]++ -> c[2]++
对于 arr[6] = 1: c[1-1]++ -> c[0]++
计数数组现在变为:
c = [1, 2, 2, 1, 0, 0, 0, 1] // 代表 1, 2, 3, 4, 5, 6, 7, 8 的出现次数
将计数数组转换为累积计数数组:
c[1] += c[0] -> c[1] = 3
c[2] += c[1] -> c[2] = 5
c[3] += c[2] -> c[3] = 6
c[4] += c[3] -> c[4] = 6
c[5] += c[4] -> c[5] = 6
c[6] += c[5] -> c[6] = 6
c[7] += c[6] -> c[7] = 7
现在累积计数数组为:
c = [1, 3, 5, 6, 6, 6, 6, 7]
创建一个输出数组,其大小与输入数组相同:
output = [0, 0, 0, 0, 0, 0, 0]
从后向前遍历原数组,根据累积计数数组填充输出数组:
对于 arr[6] = 1:
output[c[1-1]-1] = 1 -> output[0] = 1
c[1-1]--
对于 arr[5] = 3:
output[c[3-1]-1] = 3 -> output[4] = 3
c[3-1]--
对于 arr[4] = 3:
output[c[3-1]-1] = 3 -> output[3] = 3
c[3-1]--
对于 arr[3] = 8:
output[c[8-1]-1] = 8 -> output[6] = 8
c[8-1]--
对于 arr[2] = 2:
output[c[2-1]-1] = 2 -> output[2] = 2
c[2-1]--
对于 arr[1] = 2:
output[c[2-1]-1] = 2 -> output[1] = 2
c[2-1]--
对于 arr[0] = 4:
output[c[4-1]-1] = 4 -> output[5] = 4
c[4-1]--
最终,输出数组为:
output = [1, 2, 2, 3, 3, 4, 8]
将排序后的数据拷贝回原数组 arr
:
arr = [1, 2, 2, 3, 3, 4, 8]
通过上述步骤,我们成功地对数组进行了计数排序,得到了升序排列的结果。计数排序的时间复杂度为 O(n + k),其中 n 是输入数组的大小,k 是计数数组的范围。
基数排序(Radix Sort)是一种非比较排序算法,适用于整数和字符串的排序。它通过将数据分成单个数字或字符,并逐位进行排序,通常使用计数排序作为子过程。基数排序的关键在于它可以在 O(nk) 的时间复杂度内完成排序,其中 n 是待排序元素的数量,k 是数字的位数。
确定最大数的位数:
按位排序:
重复:
d
是数字的位数,n
是数组的大小,k
是基数(例如,0-9 的数字,k=10)。优点:
缺点:
基数排序适合用于处理大量的整数数据,尤其是当数据范围相对较小且位数不多时,能够显著提高排序效率。
#include "All_Sort.h"
//基数排序
void Radix_Sort(int *arr, int n, int isAscending)
{
// 找到最大值,以确定位数
int max = arr[0];
for (int i = 1; i < n; i++)
{
if (arr[i] > max) {
max = arr[i];
}
}
// 从最低位到最高位进行排序
for (int exp = 1; max / exp > 0; exp *= 10)
{
Count_Sort1(arr, n, isAscending);//计数排序
}
}
桶排序(Bucket Sort)是一种分布式排序算法,适用于将数据分布在特定范围内的情况。它通过将数据划分到多个“桶”中,然后对每个桶内的数据进行排序,最后再将所有桶中的数据合并起来。
创建桶:
根据待排序数据的范围和数量,创建若干个桶,以便将数据均匀分布到这些桶中。分配数据:
将待排序数据分配到各个桶中。每个元素根据其值被放入相应的桶中。桶内排序:
对每个非空的桶进行排序。可以使用任何排序算法(如插入排序、快速排序等),但一般选择适合桶内小数据量的排序算法。合并桶:
将所有桶中的数据按顺序合并,得到最终的排序结果。桶排序适合用于处理浮点数和整数数据,尤其是数据范围已知且分布相对均匀时,能够显著提高排序效率。
不同的排序算法适用于不同的应用场景。以下是一些常见排序算法的应用场景总结:
在选择合适的排序算法时,以下因素需要考虑:
数据规模:
数据分布:
内存限制:
稳定性:
选择合适的排序算法时,需要考虑数据规模、数据分布、内存限制和是否需要稳定排序等因素。