当你清楚的知道自己想要什么,并且意愿非常强烈的时候,你总会有办法得到的。💓💓💓
目录
✨说在前面
亲爱的读者们大家好!💖💖💖,我们又见面了,上一篇文章中我带大家学习了冒泡排序、插入排序、希尔排序、选择排序、堆排序和计数排序,如果大家没有掌握好,可以再回去看看,复习一下,再进入今天的内容。
今天我们将要学习的排序有——快速排序(霍尔法、左右指针法、非递归)、归并排序(递归、非递归)。如果大家准备好了,那就接着往下看吧~
👇👇👇
💘💘💘知识连线时刻(直接点击即可)🎉🎉🎉复习回顾🎉🎉🎉
【数据结构】排序算法(冒泡排序、插入排序、希尔排序、选择排序、堆排序、计数排序)
博主主页传送门:愿天垂怜的博客
🍋知识点一:快速排序
• 🌰1.快速排序介绍
快速排序(Quick Sort)是一种非常高效的排序算法,由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
以霍尔版本示例:
• 🌰2.霍尔排序
快速排序霍尔法基于分治法的策略来将一个大列表(或数组)分为两个较小的子列表,这两个子列表分别包含原列表中所有小于和大于某个“基准”(pivot)值的元素。然后,递归地对这两个子列表进行相同的操作,直到整个列表变得有序。
霍尔法基本思想:
-
选择基准值:从待排序的列表中选出一个元素作为基准值。选择基准值的方法有多种,如选择第一个元素、最后一个元素、中间元素,或者使用更复杂的策略如“三数取中”法来选择,以期望得到更平衡的分区。
-
分区操作:重新排列列表,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准的后面(相等的数可以到任一边,但通常放在基准的一边以保持稳定性,尽管快速排序本身不是稳定的排序算法)。这一步是快速排序的核心,也是它名称的由来——通过一次分区操作,列表就被“快速”地分成了两部分。
-
递归排序:递归地对基准值左右两边的子列表进行快速排序。由于分区操作保证了基准值左边的所有元素都不大于基准值,右边的所有元素都不小于基准值,因此可以独立地对这两个子列表进行排序。
-
终止条件:递归的终止条件是子列表的大小为0或1,即不需要再排序。
代码如下:
void QuickSort(int* arr, int left, int right)
{
assert(arr);
//保证区间存在且元素大于1
if (left >= right)
return;
//记录基准值
int keyi = left;
//记录区间端点值
int begin = left, end = right;
while (begin < end)
{
//找比基准值大的数
while (arr[end] >= arr[keyi] && begin < end)
end--;
//找比基准值小的数
while (arr[begin] >= arr[keyi] && begin < end)
begin++;
//找到后交换
Swap(arr + begin, arr + end);
}
//交换keyi与begin的值
Swap(arr + left, arr + keyi);
keyi = begin;
//递归左右区间
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
霍尔法时间复杂度分析:
-
最好情况:在最好情况下,即每次分区操作都能将数组划分为两个大小相等的部分,此时快速排序的时间复杂度为O(nlogn)。这是因为每次分区后,问题规模减半,递归深度为logn,每层递归需要遍历数组一次,所以总的时间复杂度是O(nlogn)。
-
最坏情况:在最坏情况下,例如排序序列本身为有序,此时快速排序的时间复杂度退化到O(n^2)。这是因为每次分区操作只能排除一个元素,那么总的消耗与递归深度成等差数列,总的时间复杂度是O(n^2)。
-
平均情况:平均情况下,快速排序的时间复杂度也是O(nlogn)。这是通过随机选择基准元素或采用其他策略(如三数中值分割法)来减少最坏情况发生的概率来实现的。
稳定性分析:
-
快速排序的不稳定性:
在快速排序的过程中,通过选取一个基准元素(keyi),将数组分为两部分,小于基准的元素放在基准前面,大于基准的元素放在基准后面。这个过程中,如果数组中存在两个相等的元素,并且这两个元素分布在基准的两侧,那么在交换过程中,它们的相对位置很可能会发生改变。具体来说,当基准元素与某个大于它的元素交换时,如果基准元素原本与某个等于它的元素相邻,且该等于它的元素在基准的另一侧,那么交换后,这两个相等元素的相对位置就发生了改变。 -
实例说明:
考虑一个序列 [6,6,6,5,6,6,6],如果选取第一个元素6作为keyi,进行一趟快速排序后,得到 [5,6,6,6,6,6,6] 。在这个例子中,显然6的相对位置发生了改变,因此快速排序是不稳定的。
总结:霍尔法的时间复杂度为O(nlogn),但某些情况下会退化到O(n^2),是不稳定的排序算法。
左边做key,右边先走为什么可以保证相遇的位置一定比key要小?
-
L遇R:R先走,停下来,R停下来的条件是遇到比key小的值,R停下的位置一定比key小。此时,即L没有找到大的,遇到R停下了。
-
R遇L:R先走,找小,没有找到比key小,直接和L相遇了,L停留的位置是上一轮交换的位置,上一轮交换,把比key小的值换到L的位置了。
相反:如果让右边做key,左边先走,可以保证相遇位置比key要大。
•🔥三数取中优化
霍尔版本的快速排序在效率上有些许不足,有可以优化的地方。例如,如果基准值keyi的选取不总是取第一个值,而是随机取值,那就很大程度上避免了消耗为O(n^2)的情况。对于keyi的选取,我们也可以利用三数取中的方法进行优化,即取出区间端点值和中间值中大小中间的那个数作为基准值keyi。
三数取中代码如下:
//三数取中优化
int GetMidi(int* arr, int left, int right)
{
assert(arr);
int midi = (left + right) / 2;
if (arr[left] < arr[midi])
{
if (arr[right] > arr[midi])
{
return midi;
}
else if (arr[right] < arr[left])
{
return left;
}
else return right;
}
else
{
if (arr[right] < arr[midi])
{
return midi;
}
else if (arr[left] < arr[right])
{
return right;
}
else return left;
}
}
完整代码如下:
//两数互换
Swap(int* p1, int* p2);
//三数取中优化
int GetMidi(int* arr, int left, int right);
//霍尔排序
void QuickSort(int* arr, int left, int right)
{
assert(arr);
//保证区间存在且元素大于1
if (left >= right)
return;
//找出中间值换到区间左端点位置并设置为keyi
int midi = GetMidi(arr, left, right);
Swap(arr + midi, arr + left);
int keyi = left;
int begin = left, end = right;
while (begin < end)
{
//找比基准值大的数
while (arr[end] >= arr[keyi] && begin < end)
end--;
//找比基准值小的数
while (arr[begin] >= arr[keyi] && begin < end)
begin++;
//找到后交换
Swap(arr + begin, arr + end);
}
//交换keyi与begin的值
Swap(arr + left, arr + keyi);
keyi = begin;
//递归左右区间
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
快速排序的霍尔版本在结合三数取中优化后,通过提高分区质量和减少最坏情况发生的概率,显著提升了算法的稳定性和效率。这一优化策略在处理各种输入情况时都能保持较好的性能,是快速排序算法中一种重要的优化手段。
•🔥小区间优化
快速排序的小区间优化是一种在数据量较小的情况下,采用其他更高效排序算法(如插入排序)来替代快速排序的优化策略。这种优化可以显著减少递归深度,提高排序效率,并降低栈溢出的风险。
在快速排序过程中,随着递归的深入,子数组的长度会逐渐减小。当子数组的长度减小到一定程度(通常是一个较小的阈值,我们可以选取10)时,继续使用快速排序并不划算,因为此时快速排序的递归调用和分区操作所带来的开销可能会超过其排序效率的提升。因此,我们可以用插入排序来对这些小区间进行排序,这就是小区间优化的基本思想。
小区间优化代码如下:
//插入排序
void InsertSort(int* arr, int length);
//小区间优化
if (right - left + 1 < 10)
{
InsertSort(arr, right - left + 1);
}
代码如下:
//两数互换
Swap(int* p1, int* p2);
//三数取中优化
int GetMidi(int* arr, int left, int right);
//插入排序
void InsertSort(int* arr, int length);
//霍尔排序
void QuickSort(int* arr, int left, int right)
{
assert(arr);
//保证区间存在且元素大于1
if (left >= right)
return;
//找出中间值换到区间左端点位置并设置为keyi
int midi = GetMidi(arr, left, right);
Swap(arr + midi, arr + left);
int keyi = left;
int begin = left, end = right;
//小区间优化
if (right - left + 1 < 10)
{
InsertSort(arr, right - left + 1);
}
while (begin < end)
{
//找比基准值大的数
while (arr[end] >= arr[keyi] && begin < end)
end--;
//找比基准值小的数
while (arr[begin] >= arr[keyi] && begin < end)
begin++;
//找到后交换
Swap(arr + begin, arr + end);
}
//交换keyi与begin的值
Swap(arr + left, arr + keyi);
keyi = begin;
//递归左右区间
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
小区间优化是快速排序算法中一种有效的优化策略,它可以在数据量较小的情况下提高排序效率并降低递归深度。在实际应用中,可以根据数据的特性和需求选择合适的阈值,并结合其他优化策略来实现更好的排序效果。
在快速排序中,当子数组的长度减小到一定程度时,选择插入排序而不是其他排序算法(如归并排序、堆排序等)的原因主要有以下两点:
-
局部性原理:
插入排序在处理小数据集时非常高效,特别是当数组已经部分有序时。这是因为插入排序在将元素插入到已排序序列中时,会利用到数据的局部性(locality of reference),即连续访问的数据项在物理位置上也比较接近。这种特性使得插入排序在缓存(cache)中的命中率较高,从而提高了排序速度。 -
低开销:
插入排序的实现相对简单,不需要额外的存储空间(如归并排序所需的额外数组),也不需要复杂的数据结构(如堆排序中的堆)。因此,在处理小数据集时,插入排序的开销相对较低。
• 🌰3.前后指针法
快速排序的前后指针法是一种高效且易于实现的排序算法分区策略,它通过前后指针的来回移动和元素的交换来实现数组的分区和排序。
前后指针法的基本思想:
- 初始化:设置prev和cur指针,并选定一个基准值(通常选择prev指针所指的元素作为基准值)。
- 移动cur指针:cur指针从prev的下一个位置开始,向后遍历数组。
- 比较与交换:
- 当cur指针指向的元素小于基准值时,如果prev的下一个位置不是cur(即++prev != cur),则将prev向后移动一位,并交换prev和cur所指的元素。
- 如果cur指针指向的元素大于或等于基准值,则只移动cur指针。
- 重复上述步骤:直到cur指针越界(即cur > right),此时prev指针的位置就是基准值在排序后数组中的正确位置。
- 放置枢纽值:将prev指针所指的元素(即原来的枢纽值)与最终prev的位置所指的元素进行交换,完成分区。
代码如下:
//两数互换
Swap(int* p1, int* p2);
//三数取中优化
int GetMidi(int* arr, int left, int right);
//前后指针法
void QuickSort(int* arr, int left, int right)
{
if (left >= right)
return;
//三数取中
int midi = GetMidi(arr, left, right);
Swap(arr + left, arr + midi);
int key = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[key] && (++prev) != cur)
Swap(arr + prev, arr + cur);
cur++;
}
Swap(arr + prev, arr + key);
key = prev;
//左右递归
QuickSort(arr, left, key - 1);
QuickSort(arr, key + 1, right);
}
我们可以将霍尔法和前后指针法作为两种方法可供选用,这样我们需要将函数改为int类型,函数返回的是基准值key值的下标:
//霍尔版本
int PartSort1(int* arr, int left, int right)
{
assert(arr);
if (left >= right)
return;
//三数取中优化
int midi = GetMidi(arr, left, right);
Swap(arr + left, arr + midi);
int key = left;
int begin = left, end = right;
//小区间优化
if (right - left + 1 < 10)
{
InsertSort(arr, right - left + 1);
}
while (begin < end)
{
while (begin < end && arr[end] >= arr[key])
end--;
while (begin < end && arr[begin] <= arr[key])
begin++;
Swap(arr + begin, arr + end);
}
Swap(arr + key, arr + begin);
return begin;
}
//前后指针法
int PartSort2(int* arr, int left, int right)
{
//三数取中
int midi = GetMidi(arr, left, right);
Swap(arr + left, arr + midi);
int key = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[key] && (++prev) != cur)
Swap(arr + prev, arr + cur);
cur++;
}
Swap(arr + prev, arr + key);
return prev;
}
现在我们可以比较完整地写出快速排序递归方法的代码:
//两数互换
Swap(int* p1, int* p2);
//三数取中优化
int GetMidi(int* arr, int left, int right);
//霍尔版本
int PartSort1(int* arr, int left, int right);
//前后指针法
int PartSort2(int* arr, int left, int right);
//快速排序
void QuickSort(int* arr, int left, int right)
{
assert(arr);
if (left >= right)
return;
//PartSort1和PartSort2可供选择
int key = PartSort1(arr, left, right);
//记录keyi的位置,递归到左右区间,记录的同时进行了一遍排序
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
• 🌰4.快排非递归方法
递归与非递归的区别
在快速排序的递归实现中,算法会不断地调用自己来处理更小的子数组,直到子数组的长度为1或0,这时就不需要再进一步排序了。这种自我调用的过程是通过函数调用栈来实现的,每当一个函数被调用时,它的返回地址和局部变量等信息会被压入栈中,当函数返回时,这些信息会被弹出栈,以恢复之前的状态。
非递归实现的挑战
非递归实现则不使用这种自动的函数调用栈。相反,我们需要自己手动管理一个数据结构来模拟这个栈的行为。在快速排序的上下文中,这个数据结构通常是栈(stack)。
非递归方法的基本步骤:
初始化辅助栈: 创建一个空栈。栈用于保存每个待排序子数组的起始索引(begin)和结束索引(end)。
开始排序: 将整个数组的起始和结束索引作为一对入栈。这对应于最初的排序问题。
- 迭代处理: 在栈非空时,重复下面的步骤:
- 弹出一对索引(即栈顶元素)来指定当前要处理的子数组。
- 选择子数组的一个元素作为基准值(key)进行分区(可以是第一个元素,也可以通过其他方法选择,下面我们还是用三数取中)。
- 进行分区操作,这会将子数组划分为比基准值小的左侧部分和比基准值大的右侧部分,同时确定基准值元素的最终位置。
- 处理子数组: 分区操作完成后,如果基准值元素左侧的子数组(如果存在)有超过一个元素,则将其起始和结束索引作为一对入栈。同样,如果右侧的子数组(如果存在)也有超过一个元素,也将其索引入栈
- 循环: 继续迭代该过程,直到栈为空,此时所有的子数组都已经被正确排序。
代码如下:
//引用栈的基本操作的头文件(自行书写)
#include "Stack"
//快排非递归
void QuickSortNonR(int* arr, int left, int right)
{
assert(arr);
//初始化栈
ST st;
STInit(&st);
//将区间端点弹栈
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
//迭代过程
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int keyi = PartSort2(arr, begin, end);
//确保区间存在
if (keyi + 1 < end)
{
STPush(&st, end);
STPush(&st, keyi + 1);
}
if (begin < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestroy(&st);
}
总结:快速排序的非递归实现与递归实现在时间复杂度上是一致的,都是O(nlogn)。并且,它们都是不稳定的排序算法。
🍋知识点二:归并排序
• 🌰1.归并排序介绍
归并排序(Merge Sort)是一种建立在归并操作上的有效、稳定的排序算法,它采用了分治法(Divide and Conquer)的策略。
归并排序的基本思想:
归并排序(Merge Sort)的基本思想是将一个数组分成两半,对这两半分别进行归并排序,然后将排序好的两半合并在一起。这个过程一直递归进行,直到数组被分割成只有一个元素的子数组(此时每个子数组都自然是有序的),然后开始合并过程,直到合并为一个完整的、有序的数组。
具体来说,归并排序的基本思想可以概括为以下几点:
-
分解:将当前区间一分为二,即求中点,将数组分成左右两个子数组。这个过程一直进行,直到子数组的大小为1,此时认为子数组已经是有序的了。
-
递归排序:递归地对这两个子数组进行归并排序。由于子数组的大小不断减半,这个过程最终会到达子数组大小为1的基准情况。
-
合并:将两个有序的子数组合并成一个有序的大数组。合并的过程中,通过比较两个子数组中的元素,按顺序将它们放入一个新的数组中,直到所有的元素都被合并。
归并排序的合并过程是算法的核心,它需要两个指针分别指向两个子数组的起始位置,以及一个指针指向新数组的起始位置。然后,通过比较两个子数组指针所指向的元素,将较小的元素放入新数组中,并移动相应的指针。这个过程一直进行,直到两个子数组中的所有元素都被合并到新数组中。
分而治之:
代码如下:
//子函数
void _MergeSort(int* arr, int* temp, int begin, int end)
{
if (begin >= end)
return;
//将区间二分
int mid = (begin + end) / 2;
//递归
_MergeSort(arr, temp, begin, mid);
_MergeSort(arr, temp, mid + 1, end);
//后续遍历操作
int i = begin;
//记录区间端点
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
while(begin1 <= end1 && begin2 <= end2)
{
//合并有序数组
if (arr[begin1] <= arr[begin2])
{
temp[i++] = arr[begin1++];
}
else
{
temp[i++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
temp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
temp[i++] = arr[begin2++];
}
memcpy(arr + begin, temp + begin, sizeof(int) * (end - begin + 1));
}
//归并排序
void MergeSort(int* arr, int length)
{
assert(arr);
//创建临时数组
int* temp = (int*)malloc(sizeof(int) * length);
if (temp == NULL)
{
perror("malloc operation failed");
return;
}
//调用子函数
_MergeSort(arr, temp, 0, length - 1);
free(temp);
temp = NULL;
}
为什么将区间[begin, end]分割成[begin, mid]、[mid + 1, end]而不分成[begin, mid - 1]、[mid, end]?
举例说明:当数组为[0, 9]一共9个元素时,如果按照第二种分割方法,首先会被分割成[0, 3]、[4, 9] ,而第一个区间[0, 3]又会被分割成[0, 0]和[1, 3],此时,[0, 0]默认为有序,而[1, 3]会被分割成[2, 1]和[2, 3],而[2, 3]会被分割成[2, 1]和[2, 3],之后无论怎么分割,[2, 3]始终存在,所以不能用这种方式进行分割。
归并排序的复杂度分析:
时间复杂度:归并排序不论是在最好情况(数组全部有序)还是最坏情况或是平均情况,归并排序的性能不受输入数据初始顺序的影响。即使输入数组已经是排序好的,归并排序仍然会将其分成两半,递归排序,然后合并,类似于二叉树的后序遍历,递归的深度是logn,每一层都是n,所以归并排序的时间复杂度是O(nlogn)。
空间复杂度:归并排序需要一个与原数组大小相同的辅助数组来存储合并过程中的数据。因此,归并排序的空间复杂度是O(n)。
稳定性分析:
在归并排序中,当合并两个已排序的子数组时,如果当前元素与下一个子数组的元素相等,它会保持当前元素在合并后的数组中的位置不变,即先遇到的元素会被先放在合并后的数组中。这个特性保证了归并排序的稳定性。
总结:归并排序的时间复杂度是O(nlogn),空间复杂度是O(n),是稳定的排序算法。
• 🌰2.归排非递归方法
归并排序的非递归方法,也称为迭代归并排序,它采用归并排序递归版本的逆向思维,即不是通过递归地划分序列直到序列长度为1,而是从序列长度为1开始,逐步将小序列归并成较大的有序序列,直到整个序列有序。
归并排序的非递归方法通过不断合并相邻的已排序序列,从而得到完全排序的序列。在这个过程中,使用了一个辅助数组(temp数组)来暂存合并过程中的数据。
归排非递归方法的基本步骤:
-
初始化:设置初始的归并段大小为1,即每个元素都被视为一个已排序的序列。
-
合并过程:
- 使用一个变量(如gap)来控制归并段的长度,初始值为1。
- 不断将gap的值翻倍,以控制归并的段数。
- 对于每一对相邻的归并段(长度为gap),将它们合并成一个长度为2*gap的有序序列。
- 重复这个过程,直到gap的大小超过了数组的长度,此时整个数组已经排序完成。
-
归并操作:
- 对于每一对相邻的归并段,使用双指针法将它们合并到辅助数组temp中。
- 合并完成后,将temp中的有序序列复制回原数组。
代码如下:
void MergeSortNonR(int* arr, int length)
{
assert(arr);
int* temp = (int*)malloc(sizeof(int) * length);
if (temp == NULL)
{
perror("malloc operation failed");
return;
}
int gap = 1;
while (gap < length)
{
//两两归并,每组gap,两两间隔2 * gap
for (int i = 0; i < length; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//如果第二组不存在,则这一组不需要归并
if (begin2 >= length)
{
break;
}
//如果第二组部分越界,则需要修正end2后再归并
if (end2 >= length)
{
end2 = length - 1;
}
int j = i;
//归并过程
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] <= arr[begin2])
{
temp[j++] = arr[begin1++];
}
else
{
temp[j++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
temp[j++] = arr[begin1++];
}
while (begin2 <= end2)
{
temp[j++] = arr[begin2++];
}
//每归并一次就拷贝到arr中
memcpy(arr + i, temp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(temp);
temp = NULL;
}
注意:归排非递归方法中的memcpy一定要在for循环中,也就是归并一次拷贝一次,否则会造成接下来比较的数据并不是有序的,且将上轮归并的数据覆盖。
🍋知识点三:排序算法总结
• 🌰1.复杂度即稳定性分析
• 🌰2.排序性能比较
可以用以下代码对各大排序的性能进行测试:
void TestOP()
{
//设置随机种子
srand((unsigned int)time(NULL));
const int N = 100000;
//创建N大小的数组
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);
for (int i = 0; i < N; i++)
{
//随机生成100000个数
a1[i] = rand() + i;
a2[i] = a1[i];
a3[i] = a2[i];
a4[i] = a3[i];
a5[i] = a4[i];
a6[i] = a5[i];
a7[i] = a6[i];
}
//计算时间差
int begin1 = clock();
BubbleSort(a1, N);
int end1 = clock();
int begin2 = clock();
InsertSort(a2, N);
int end2 = clock();
int begin3 = clock();
ShellSort(a3, N);
int end3 = clock();
int begin4= clock();
SelectSort(a4, N);
int end4 = clock();
int begin5 = clock();
HeapSort(a5, N);
int end5 = clock();
int begin6 = clock();
QuickSort(a6, 0, N - 1);
int end6 = clock();
int begin7 = clock();
MergeSort(a7, N);
int end7 = clock();
//打印排序所需要的时间,单位是毫秒ms
printf("BubbleSort:%d\n", end1 - begin1);
printf("InsertSort:%d\n", end2 - begin2);
printf("ShellSort:%d\n", end3 - begin3);
printf("SelectSort:%d\n", end4 - begin4);
printf("HeapSort:%d\n", end5 - begin5);
printf("QuickSort:%d\n", end6 - begin6);
printf("MergeSort:%d\n", end7 - begin7);
//释放空间
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
}
int main()
{
TestOP();
return 0;
}
结果如下:
可以发现,对随机生成的100000个数据进行排序,性能:快速排序 > 堆排序 ≈ 希尔排序 > 归并排序 > 插入排序 >> 选择排序 > 冒泡排序。可见,冒泡和选择排序的性能是非常低下的,实践中这两种排序是不常用的,而快排、堆排和归并排序、希尔排序这些可以相互搭配使用,例如处理小数据或部分有序的序列时用插入排序,深度较深时用堆排序等等。
• ✨SumUp结语
到这里本篇文章的内容就结束了,本节内容讲解了排序算法剩下的部分,快速排序和归并排序,并对排序这一章节的内容进行了总结。那么到这里初阶数据结构的内容就结束了,希望大家多多复习,接下来会继续更新C++的知识,请大家拭目以待~