int right = end;
int keyi = begin; //以最左边作为对照值记录,右边先走
while (left < right)
{
//右边找比keyi所在位置小的值,若是 >= 则忽略(加上=防止死循环)
while (left < right && a[right] >= a[keyi])
{ //left < right —— 防止特殊情况越界
right--;
}
//左边找比keyi所在位置大的值,若是 <= 则忽略
while (left < right && a[left] <= a[keyi])
{
left++;
}
//都找到了,则交换
swap(&a[left], &a[right]);
}
//最后当left和right相遇的时候将相遇位置的值与keyi位置的值交换
swap(&a[left], &a[keyi]);
keyi = left; //因keyi位置的值被交换到相遇点,因此更新准备分化递归
return keyi;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
int key = PartSort1(a, begin, end); //单趟排序获取key值
//左右区间分化递归
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
---
* 其他的都不会有很大问题,我们主要来讲讲内部的循环逻辑
* 看到内部的这两段循环就是来控制L与R进行寻找走动的代码,也是快速排序的核心❤️所在
//右边找比keyi所在位置小的值,若是 >= 则忽略(加上=防止死循环)
while (left < right && a[right] >= a[keyi])
{ //left < right —— 防止特殊情况越界
right–;
}
//左边找比keyi所在位置大的值,若是 <= 则忽略
while (left < right && a[left] <= a[keyi])
{
left++;
}
* 但是有的同学在一上来可能就会写成这样,也是很多同学的通病(没有考虑到特殊情况)
//右边找比keyi所在位置小的值,若是 >= 则忽略(加上=防止死循环)
while (a[right] > a[keyi])
{ //left < right —— 防止特殊情况越界
right–;
}
//左边找比keyi所在位置大的值,若是 <= 则忽略
while (a[left] < a[keyi])
{
left++;
}
* 原因就是以下两点
⚠**没控制端点导致越界风险**
⚠**没考虑相等情况导致死循环**
![在这里插入图片描述](https://img-blog.csdnimg.cn/252842aa937943719ccd3a1b831b0019.jpeg#pic_center)
* 接下去要说的就是第一次交换结束后的分化递归
* 我要特别强调的就是这一句,也就是**重置这个keyi,然后将其返回**,这个keyi不是新的keyi值,而是因为keyi被交换到了left所在位置,因此我们要做个标记方面后面的递归可以进行左右划分,当然你直接使用left或者right也是可以的,就是不要使用keyi就行了,否则这个区间就会异常🈲
keyi = left; //因keyi位置的值被交换到相遇点,因此更新准备分化递归
* 接着在获取到本轮的keyi后去进行左右递归时,只需要修改一下**左区间的右边界值和右区间的左边界值**就可以了
//左右区间分化递归
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
* 既然要递归,那么一定需要递归的结束条件,也就是这个
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
* 来画画递归展开图进一步了解快排的过程吧,尝试着自己动手试试✒️
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/9eba0054e5674823bd9ddf8ca8d119ce.jpeg#pic_center)![在这里插入图片描述](https://img-blog.csdnimg.cn/f7cadbce7b5d4d95bef69d080e891a44.jpeg#pic_center)
>
>
>
### ⌚快速排序复杂度分析
**【时间复杂度】:O(NlogN)
【空间复杂度】:O(logN)**
>
> 看了Hoare版本的快排后,我们立马来分析一下其时间复杂度,因为后面要对其进行优化,所以要提前讲一下复杂度这一块🔍
>
>
>
* 理解了快速排序的总体流程后,你应该了解了它是每次通过左右两个指针的遍历和key进行一个比较然后进行交换,因而将其列入【交换类排序】,但是呢在找出第一个key之后,就需要进行一个左右分化递归,最后直至左右区间均有序之后,整体才算有序。因此对于快排来说,也可以算作是一种**分治类排序**
* 通过这个思维我们来看看快排的时间复杂度该如何描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/0755aaebb3474ea09f7c6e84c452970f.jpeg#pic_center)
* 可以看到,`横向是每一次要搜寻遍历的数字个数`,随着key值不断地确定,在递归的过程中便慢慢减少,但这些在【大O渐进法】看来只是常数级的,因此可以忽略,那么每一次的遍历次数就是N
* `竖向是要遍历的轮数`,那需要遍历多少回呢,这就要看递归的深度了,假设我们每次找到的key值都在中间,那每一次的递归就都是一个二分,也可以看成是一个二叉树的样子,那根据【堆排序】来看很明显可以知晓这个次数是【logN】;
![在这里插入图片描述](https://img-blog.csdnimg.cn/a9055f68837c4f8fa71af74f9d106842.jpeg#pic_center)
* 最后就可以得出快排的时间复杂度为O(NlogN)。
#### 快排缺陷1——待排序列呈现有序状态
* 但是在学习了[时、空复杂度](https://bbs.csdn.net/topics/618545628)之后知道对于时间复杂度有最好、最坏和平均只分,O(NlogN)只是快速排序的最好情况,**从上图可以看出这是一棵满二叉树**,但是呢,在一些场合下这个数据的分步是非常混乱而且不合理的,因此有些情况下很难达到**O(NlogN)**
* 为什么这么说呢,因为对于快排来说有一种致命的数据序列,那就是**有序**,无论是【顺序】还是【逆序】,它都招架不过来
+ 假设你在`左边选到了一个最大的数做key`,此时这个序列还是逆序。但是呢要将比它小的数都放到它左边,此时不仅需要遍历N次,而且还要交换N此,妥妥的**O(N2)**
+ 假设你选取的key值是在`最右边,选择了一个最小的数做key`,此时这个序列还是顺序,那么就需要将它左边的所有数都放到这个key值的右边,也是**O(N2)**
![在这里插入图片描述](https://img-blog.csdnimg.cn/17d508f7023c4fb7ab09c0e37b0702b4.jpeg#pic_center)
* 这么看来快速排序就退化成了\*\*O(N2)\*\*的排序算法,那有同学问:那快排还有什么用,都要退化了?因为快速排序很早就被发明出来了,被使用得很广泛,但是它也有一些自身的缺陷,所在在经过后人不断地修正过程中,也想出了一些对其进行优化的办法,例如像`[三数取中]、[小区间优化]`这些。还有人直接根据Hoare的这种思路,改进了快排,想出了类似于快排的思想,但是又有着不一样的地方的一些方法,就是我们下面要说到的两种:【挖坑法】、【前后指针法】
#### 快排缺陷2——递归层数过深导致栈溢出
* 上面说到,快速排序除了对【有序序列】进行排序会退化之外,还有一点就是它需要进行层层递归,那**既然是递归,就需要调用函数;既然要调用函数,那就需要建立栈帧**。但是我们知道内存中的栈空间容量是有限的,在VS中默认大小只有1M,若是建立过多的栈帧,就会像下面这样产生栈溢出的风险
![在这里插入图片描述](https://img-blog.csdnimg.cn/acaea78fcc5d406dbe0116b4b4828453.jpeg#pic_center)
* 以上的这个栈溢出我是放在DeBug里面运行的,若是将运行版本修改为Release,那可能就不会出现这样的报错了,因为**Release会对递归的调用次数进行一个优化**,DeBug版本是专门用来调试用的,会有很多调试信息,因此内容多了,就会产生栈溢出
* 看到下面这张图,使我们刚才在分析时间复杂度的时候看到的,可以看到最后面这个二叉树是不断地在进行分叉,直至1为止然后才递归结束进行回调,但此时的递归深度可能已经很深了,已经是无法挽回了,那此时我们应该怎么办呢?不用怕,上面我们说到过了一种方法,就是专门针对这个的,也就是**小区间优化法**,在下一模块进行讲解
![在这里插入图片描述](https://img-blog.csdnimg.cn/7b1f7fb6fbe245e89bb657dd7d362cf9.jpeg#pic_center)
* 漏了一个空间复杂度忘说,对于快速排序和上面的五个排序不一样,它的空间复杂度不是O(1),而是**O(logN)**。那有同学对这个复杂度就没有一个概念了,我们一起来探讨一下🔍
* 对于快速排序而言,不是在找出第一个keyi的位置之后就好了,还要其递归其左右区间,上面说到**递归就要调用函数,函数就需要建立栈帧**,对于栈帧来说,虽然不是什么很大的东西,但是其对比普通的变量而言还是会有一些消耗
* 所以在不断向下递归的过程中,就会产生许多栈帧,**不过在回调的时候还是会去重复利用栈帧的**,这么看来就像是一棵二叉树的样子,那就显而易见是的O(logN)了 ,但是当这个序列出现大量重复数据的时候,而且这个keyi还是重复数据,那么快速排序就会退化成像冒泡排序那样的O(N2),那么此时空间复杂度也会增加,呈现的便是一棵单边树的样子
---
### 🐇快速排序优化
>
> 在分析完了快排的时间复杂度之后,也知晓了面对两种缺陷可以使用的优化方法,接下去我们来讲讲这两种方法
>
>
>
#### 【三数取中法】—— 高性能优化
**针对待排序列呈现有序状态**
##### ① 代码&算法图逻辑分析
* 对于【三数取中法】,字面意思,就是从三个数中取出中间的那个数,我们设左边的数为left,设右边的数为right,然后它们的中间值为mid
* 我们通过算法图和代码一步步地来看一下
* 首先要先取出它的中间值,这里得【>>】是[右移运算符](https://bbs.csdn.net/topics/618545628),意思就是在二进制位上将其往右移动一位,对于二进制来说就是缩小两倍,也就是/2的意思
int mid = (left + right) >> 1;
* 然后我们给出第一层判断逻辑
int mid = (left + right) >> 1;
if (a[mid] > a[left])
{
if (a[mid] < a[right])
{ //left mid right
return mid;
}
//另外两种mid一定是最大的,比较left和right
else if (a[left] > a[right])
{ //right left mid
return left;
}
else
{ //left right mid
return right;
}
}
* 首先看到第一种,就是这个**mid所在位置的值大于left所在位置的值**的时候,继续进入判断,若是**mid所在位置的值又小于right所在位置的值**,那这个mid就处于中间位置了,直接返回即可
![在这里插入图片描述](https://img-blog.csdnimg.cn/5badb69b90e24681806a64a0bebb3155.jpeg#pic_center)
* 接着若是这个mid值不是小于right,那么它就一定处于最右侧,是最大的,这个时候我们内层的逻辑就是要去判断left和right的大小了
![在这里插入图片描述](https://img-blog.csdnimg.cn/e5d13656c6334814bbe978d7c5e45143.jpeg#pic_center)
* 若是**a[right] > a[left]** ,那么right此时便在中间,返回right即可
* 若是**a[left] > a[right]** ,那么left此时便在中间,返回left即可
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/9a735c69bda94c59b963b2bf13b867e7.jpeg#pic_center)
>
>
>
---
* 看完了第一层逻辑,我们再来看第二层逻辑
* 也就是当这个left所在位置的值要比mid所在位置的值要大的时候,也是有三种情况需要判断,若是mid所在位置大于right所在位置,则表示a[mid]为中间值,返回mid即可
* 若是mid所在位置的值小于等于right,那么a[mid]一定是最小的,此时同理,**只需去比较a[left]和a[right]即可**
else //a[left] >= a[mid]
{
if (a[mid] > a[right])
{ //right mid left
return mid;
}
//另外两种mid一定是最小的,比较left和right
else if (a[left] < a[right])
{ //mid left right
return left;
}
else
{ //mid right left
return right;
}
}
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/0ea6db7f2e53474f8e33f814c08a62c3.jpeg#pic_center)
>
>
>
---
* 分析完之后一定要来看看这一块,涉及接口的调用
* 下面这两句直接写在单趟循环的逻辑中即可,若是在区间还存在的情况下,进到单趟分割进行比较可以直接选出一个中间值和最前面的begin值进行一个交换,
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]); //交换begin上的值和找出的中间值
* **此时begin位置的值就是最合适做key的**,再去记录下这个key即可
int left = begin;
int right = end;
int keyi = begin; //以最左边作为对照值记录,右边先走
>
> 为何说找出来的`mid`是最适合做key的?
>
>
>
* 若是单单去选key,那么选出来的key值就有可能取到最大或者最小值,此时就会造成**快排性能退化**的情况,但若找出来的是一个中间值的话,就不会发生这种情况,那么整个快排的过程呈现的便是一颗`二叉树`的样子
##### ② 性能测试
* 上面了解了如何去优化快速排序,接下来我们来测试一下这个代码逻辑是不是真的可以实现性能优化
优化前
![在这里插入图片描述](https://img-blog.csdnimg.cn/1c15729a0961485e95ccac7dcee68b25.jpeg#pic_center)
优化后
![在这里插入图片描述](https://img-blog.csdnimg.cn/0db2bd770cfc4e50832fb6cd79086cb3.jpeg#pic_center)
* 为什么说这个优化是**高性能的优化**,应该清楚了吧,有同学问为什么优化程度可以这么大呢?你带入一些特殊值放进O(N2)和O(NlogN)就知道为什么,完全不是一个级别的,所以对于序列有序这一点来说若是不加这个优化那**对于快排是非常致命的**
* 仔细观察上面两种图,是不是发现对于如此大的数据,但是【插入排序】和【冒泡排序】可以比其他O(NlogN)的排序还要快呢,这就是我们在上一模块中说到的
+ 对于直接插入排序来说,序列接近有序性能达到最优
+ 对于冒泡排序来说,序列有序性能达到最优
#### 【左右小区间法】—— 小型优化
**针对递归层数过深导致栈溢出**
>
> 运用三数取中法,对快速排序进行了一个优化,接下去我们再来将一种优化方式,叫做左右小区间法
>
>
>
* 对于三数取中法,是在开头优化;对于左右小区间法,则是在结尾优化
* 好,这里给大家【简单】画了一张图,其实随着这个递归次数的增加,递归的层层深入,这个数据量也会被倍增,那么这个程序所需要消耗的内存就会越多,那我们有没有办法将最后的这几层递归消除呢?
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/1fc7bd737c18449283758aae68aab254.jpeg#pic_center)
>
>
>
* 这就要用到这个【左右小区间法】,什么叫左右小区间法呢?也就是随着这个区间被不断地划分,到了最后的那么几个区间,比如说每个区间只剩十个数的时候,我们就考虑将这个区间内的数再进行一个排序
* 那这个时候还是用快排吗,当然不是?**如果用快排的话那和继续递归下去就没什么两样了**,我们要使用其他的、用到此处最合适的排序算法
+ 首先排除`冒泡、选择`,O(N2)的肯定不要
+ `堆排序`还要建堆,虽然性能可观,但只会增加繁琐度。
+ 用`希尔`吗?不,这个地方不能用希尔,因为这个地方的数据量大概只有10个左右,并不多,**希尔排序**的话用在这里真的可以说是杀鸡🐥用牛刀🔪了,因为就这么十个数进不进行不排序完全没区别,因此也不合适
+ 一个个排除下来,最后只剩下**直接插入排序**了,对,就是用它,虽然在有些场合下直接插入排序的性能不是很优,但是在此处只有10个数的情况,我们用直接插入排序最为合适
* 代码很简单,只需要判断一下当前递归进来的区间的数据个数是否 < 15,若是则直接来直接使用插入排序即可
* 这一块代码要写在总的快排代码中即可,而不是写在单趟中,因为是对不断递递归进来的区间个数进行的一个判断
//小区间优化
if ((end - begin + 1) < 15)
{
//在数据量少的时候改用直接插入排序
InsertSort(a + begin, end - begin + 1);
}
### 2、挖坑法
#### 2.1 动图演示
![在这里插入图片描述](https://img-blog.csdnimg.cn/d80962b6b98e40aa9e5097502e0a5dc4.gif#pic_center)
#### 2.2 基本思路简析
* 对于快速排序,还有第二种【挖坑法】,这种方法和左右指针法不同的地方在于它没有那么多缺陷,而且比较好理解
* 下面是它的排序步骤
①直接将最左端的值选出来作为key值,然后【右边找小】,放入坑位,然后更新坑位值为**右侧找到的那个数所在的下标**;
②出现了新的坑位后,【左边找大】,找到之后将数字放到新的坑位中,然后继续更新坑位。
③循环往复上面的步骤,直到两者相遇为止,**更新相遇处为最新的坑位**,然后将key值放入坑位即可,保证左边比key小,右边比key大
#### 2.3 算法分解图
以下是动图的算法分解图,可以对照理解一下
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/64e71d8537b54cc1803d970b73c604b9.jpeg#pic_center)
>
>
>
#### 2.4 代码展示与详解
* 首先展示一下代码
/*快速排序 —— 挖坑法*/
int PartSort2(int* a, int begin, int end)
{
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int left = begin, right = end;
int hole = left; //坑位
int key = a[hole]; //记录坑位上的值
while (left < right)
{
//右边找小,放入坑位,然后更新坑位
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
//左边找大,放入坑位,然后更新坑位
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
* 我们来分析一下重点部分,也就是下面这块不断找小、找大并且填坑的逻辑
* `a[hole] = a[right]`与`a[hole] = a[left]`就是在找到符合条件的数之后将其扔入坑中的过程
* `hole = right`与`hole = left`就是在填完上一个坑位之后更新坑位的过程
//右边找小,放入坑位,然后更新坑位
while (left < right && a[right] >= key)
{
right–;
}
a[hole] = a[right];
hole = right;
//左边找大,放入坑位,然后更新坑位
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
* 最后的这一块`a[hole] = key`就是在最后两者相遇后将原先记录的key值放入这个相遇的坑位。最后`return hole`这个坑位坐标即是找到的那个对照值所在的位置【无需再更新当前的坑位值,与Hoare法不同的是坑位在寻找的过程中同步动态更新】
### 3、前后指针法
#### 3.1 动图演示
![在这里插入图片描述](https://img-blog.csdnimg.cn/d66ed14bbdae47b1af59d2cf39458823.gif#pic_center)
#### 3.2 基本思路简析
* 快速排序的最后一种方法是【前后指针法】,这种方法比较高效,但是有点晦涩难懂,因此我会分析得详细一些
* 下面是它的排序步骤
①定义一个prev指针位于起始端,再定义一个**cur指针就位于它的后方**,记录当前位置上的key值
②**cur指针向后找比key小的值**,若是找不到,则一直++;若是cur找到了比key小的值,prev++,然后**交换二者的值之后cur再++**
③直到cur超过右边界之后,退出循环逻辑,将此时**prev位置上的值与key值做一个交换**,保证左边比key小,右边比key大
#### 3.3 算法分解图
* 一样也给出算法分解图帮助理解
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/7db409a5bbb64800b088fe527b628ae4.jpeg#pic_center)
>
>
>
#### 3.4 代码展示与详解
* 首先展示一下代码
/*快速排序 —— 前后指针法*/
int PartSort3(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int prev = begin;
int cur = prev + 1;
int keyi = prev;
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
swap(&a[prev], &a[cur]);
}
cur++; //cur无论如何都要++,因此直接写在外面
}
//cur超出右边界后交换prev处的值和key
swap(&a[keyi], &a[prev]);
return prev;
}
* 然后来讲解一下,主要的就是内部的这一个循环比较的逻辑。也就是将我们上面的思路转化为代码的形式,但是一定有同学疑惑这里为什么要加上`&& ++prev != cur`呢,此时你可以返回去看上面的算法分解图,可以看到第一、二两次在交换`swap(&a[++prev], &a[cur]);`的时候,完全就没有动,因为它们是相同的,此时我们其实可以去做一个优化,那就是判断`++prev`之后的位置是否与`cur`是相同的,若是则不进行交换
* 而对于这里的比较是否大于进去以后交换的价值呢?因为对于这种比较的话其实**CPU是可以直接操作**的,运用一些与门、非门之类的;但是这里的交换呢,却需要使用到交换函数,我们知道**调用函数就需要建立栈帧,而且在函数内部还要定义变量**,此时我觉得这里是存在一些小小优化的,但是价值不大
* 但是有同学觉得这样的判断其实并没有很大的优化,确实是这样,但是呢这个思想我们要知道,因为在现今随着计算机的飞速发展,编译器对代码的优化已经达到了一个很大的程度,所以我们在Release版本下运行的时候其实是作了很大的优化。
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
swap(&a[prev], &a[cur]); //if判断时prev已经++,内部无需在++
}
cur++; //cur无论如何都要++,因此直接写在外面
}
* 最后的话当cur超出右边界之后交换prev和keyi上的值即可 `swap(&a[keyi], &a[prev]);`然后将当然的prev位置作为keyi返回
### 4、三路划分法【拓展】
>
> 上述三种是快速排序比较经典的方法,有关【三路划分法】某些高手针对快速排序的缺陷发明出来的一种方法,很是巧妙,所以这里给大家介绍一下
>
>
>
#### 4.1 快速排序缺陷3——key值大量重复
* 在上面我们说到了快速排序存在缺陷的两个缺陷,现在再来说说它的另一个缺陷。具体是什么呢?我们首先来分析一下
![在这里插入图片描述](https://img-blog.csdnimg.cn/d0247c6ed156417dbcaeb83c9c422b85.jpeg#pic_center)
* 从上图可以看出,对于一般情况我们上面所介绍的三种方法都不会出现问题,但是对于一些特殊的极端情况而言,快速排序的效率就会明显下降,就如下面这样
* 对于数组中的数据都是重复的时候,此时在经历一次排序后key就会变得很极端,此时再对右区间去进行递归排序的时候又是一组很大的数据,在下一次选出key后就是1的位置,然后再接着递归下去就是一个很明显的O(N2)
![在这里插入图片描述](https://img-blog.csdnimg.cn/416d8d9c12d64ca7bd926f98761efc27.jpeg#pic_center)
#### 4.2 算法思路简析
* 这就是快排的又一大缺陷,但是面对这种缺陷难道没办法了吗❓,那当然不会,民间存在很多的算法高手,有人就研究出来一种叫做【三路划分】的方法,修改了原本快速排序的结构,将原本的【两路划分】变成了三段小区间
* 很明显可以看出,其实就是因为这种两路划分后key的左边和右边都可能存在和它相等的数,因此让这个key值飘忽不定。所以就有人想出了将与key相等的值都包围住,单独作为一个区间,如下图所示👇
![在这里插入图片描述](https://img-blog.csdnimg.cn/09676faa7621420ea0cc6e51009aec8e.jpeg#pic_center)
对于这一种划分的算法逻辑是怎样的呢?
* 此处我们需要三个指针,一个【left】指向首端,一个【right】指向尾端,再一个【cur】指向left的后一个位置,对于【key值】也是一样取**首端所在位置的值**,因为我们会进行一个三数取中的操作
* 我们要做的就是通过将`a[cur]`上的值与【left】和【right】上的值进行一个对比,然后**将比key小的值扔到前面,将比key大的值扔到后面,若是和key相同则放到中间**。在【left】与【cur】不断后移,【right】不断前移的过程中,三个区间就会很明显地被分化出来,直到`cur > right`时,便终止比较。接下去中间的这一块与key相等的值不需要去管他,我们只需要再去递归其左右区间即可,这就可以防止重复key值带来的多次递归的风险
#### 4.3 代码详解与过程分解
代码展示
void QuickSortThreeDivisioin(int* a, int begin, int end)
{
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
//小区间优化
if ((end - begin + 1) < 15)
{
//在数据量少的时候改用直接插入排序
InsertSort(a + begin, end - begin + 1);
}
else
{
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int key = a[begin];
int left = begin;
int cur = left + 1;
int right = end;
while (cur <= right)
{
if (a[cur] < key)
{
swap(&a[left++], &a[cur++]);
}
else if (a[cur] > key)
{
swap(&a[cur], &a[right--]);
//此时cur不变化是因为从right换到中间的值可能还是比key大,为了在下一次继续进行比较
}
else
{
cur++;
}
}
//[begin, left - 1][left, right][right + 1, end]
QuickSortThreeDivisioin(a, begin, left - 1);
QuickSortThreeDivisioin(a, right + 1, end);
}
}
* 可以看到,大部分逻辑还是没有变化,就是修改了一下内部的排序过程而已。有了思路,写代码并不是什么难事
排序过程分解图
![在这里插入图片描述](https://img-blog.csdnimg.cn/ba4378981c134a8a9915381f45191259.jpeg#pic_center)
---
### 💪快速排序方法的“非递归写法”【校招要求✍】
#### 📕递归的缺陷分析
>
> 看完了上面三种快速排序的,我们可以看出,都是使用递归的方法去实现的,也就是通过一层的遍历找出一个中间值,然后根据这个中间值进行一个左右划分,分别去进行分治递归。可以看出三种方法虽然类似,但都有自己的独特之处
>
>
>
>
> 但是大家肯定有一个疑问,既然都已经学了四种方法了,那为什么还要再去学习非递归的写法呢?我们来探究一下🔍
>
>
>
* 刚才在分析快排的时间复杂度时,**讲到了递归的缺陷**,因为随着递归的层层深入,会建许多的栈帧,但若是建立的栈帧数量超出了编译器预留的栈空间大小,此时就会导致栈溢出【Stack Overflow】,这是栈溢出时最有可能的原因之一,相信大家在平时写代码的时候都有遇到过,不过这在平常我们做代码练习的时候还好,若是放到一些**大的工程项目**中可能就会导致一些大的问题。所以递归用起来虽然很香,但是呢很容易导致【**爆栈**】
* 递归的话是一层嵌套一层,一直递归到结束条件为止然后步步回调,看起来是很有思维逻辑,但是随着这个数据量的增大,递归的深度也会逐渐地加深。而且递归它是需要在栈空间中开辟栈帧的,在内存中,这个栈空间是很小的,大家可以进VS里看一下,**默认的栈空间只有1M**
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/f6ed24a1560945a7b11da167d2c906ae.jpeg#pic_center)
>
>
>
* 所以如果这个数据量一旦增大的话,递归的深度也会不断加深,然后导致栈空间不够用,就导致了我们经常遇到的栈溢出问题【Stack Flow】—— 这就是递归的缺陷
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/084d56bda77647628277f6c4b25457ee.jpeg#pic_center)
>
>
>
* 所以,为什么说校招要考非递归这一块呢,就是想让你进企业后在有些数据量大的地方可以使用非递归来实现,因为在企业中开发的项目通常是很大的一个工程,都是直接面向用户的,所以这个数据量是很庞大的,如果我们用递归来实现,可能会给项目中安放一些致命的危险
#### 📕非递归代码实现
>
> 那非递归改递归这一块要怎么实现呢?我们来看一下
>
>
>
* 下面的递归转非递归是借助【数据结构栈】模拟递归的过程,其本质上还是递归的思想,只是换了个方式表达而已,非递归这一块的代码还是很重要的,可以先看一下
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
InitStack(&st);
//首先将整体区间入栈
PushStack(&st, begin);
PushStack(&st, end);
while (!StackEmpty(&st))
{
//出栈分别获取右左两个端点
int right = StackTop(&st);
PopStack(&st);
int left = StackTop(&st);
PopStack(&st);
//求解keyi的位置
int keyi = PartSort3(a, left, right);
//先入右
if (keyi + 1 < right)
{ //若是区间的值 > 1,则继续入栈
PushStack(&st, keyi + 1);
PushStack(&st, right);
}
//后入左
if (left < keyi - 1)
{ //若是区间的值 > 1,则继续入栈
PushStack(&st, left);
PushStack(&st, keyi - 1);
}
}
DestroyStack(&st);
}
* 我们再通过画图来看看
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/215a535b7b9b43658ef183cedefbad33.jpeg#pic_center)
>
>
>
接下来稍微描述一下,主要是讲给还没有理解的同学
* 这里的内部循环始终要遵循的一条原则是栈的【先进后出】,此时看到要先将左右两个端点先入栈,然后在循环中,先取出栈顶的两个数据然后出栈,**后入的便是右区间端点,先入的是做区间端点**,分别用`right`和`left`进行保存
* 然后将这个两个端点值通过上面所说到的三种快速排序的方法,使用单趟的一个逻辑,去求出每一次进来之后的keyi位置,然后再利用这个keyi进行一个左右区间的分化,也是一样的规则,先入右,后入左,但是在入栈之前要先判断一下当前这个区间的值的个数,**若是只有一个数或者是这个区间根本就不存在的话**,那就不需要再入了,若是区间的值> 1 就将这个区间入栈即可
* 最后循环往复执行这段逻辑,也就是一个递归的思维,直到递归完左区间之后递归右区间,最终到栈空为止表示没有区间需要在进行排序了
## 七、归并排序【⭐重点掌握⭐】
### 1、动图演示
![在这里插入图片描述](https://img-blog.csdnimg.cn/a69e9bc2f78f4832be707f546c4b22f1.gif#pic_center)
### 2、算法思路简析
【核心思路】:分治思想。使原有的子序列合并,得到完全有序的序列,即**先使每个子序列有序,再使子序列段间有序**;
* 以下是有关归并排序的算法分解图,帮助理解
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/c7a2eaa691434e17b4a0872d300dd215.jpeg#pic_center)
>
>
>
* 简要分析一下,上图是通过将原有的序列【10 6 7 1 3 9 4 2】分割成两部分,然后将这两部分再继续进行划分,再分割成两部分,一直划分直到区间的数组<=1为止,这就相当于是一个【递归】的过程,观察上半部分图,就是一棵**二叉树**,因此可理解为一直分解直到叶子结点为止
* 分解完成开始合并,原序列**需要左区间有序,右区间有序**,才可以对左右区间进行一个归并,因此从**单个的小区间开始**,慢慢向上回调进行归并(这里是画成了向下,可以参考二叉树的遍历),层层回调上来后左右区间再进行一个归并,最后得到一个有序的序列,这很像二叉树中的后序遍历,要其左子树、右子树都遍历完毕了,才去遍历根
---
**有关归并排序会讲解【递归】和【非递归】两种解法,因为在校招中都会涉及**
### 3、递归实现
#### 3.1 思路分析
>
> 首先来说一下有关递归版本的实现思路分析
>
>
>
* 首先对于归并,无论是递归还是非递归,**都需要去维护一个数组**,因为在小区间进行归并的时候需要将归并完的数据暂时存放在一个tmp数组中,因此要维护一个数组是必不可少的。
* 因为归并和快排一样,都是需要进行不断递归的,所以对于不断分解和归并的过程需要**单独封装为一个函数**,然后需要传入边界值和两个数组(原数组、临时数组),接着在这个函数内部进行不断地递归划分直至这个序列有序。具体的我们放到代码中去讲解
#### 3.2 代码详解
* 下面是MergeSort主函数,可以看到需要在堆区中开辟出一块空间去得到一个临时数组
* 中间则是分解归并的单独函数封装
* 最后则是对于这个临时开辟数组的释放(防止内存泄漏)以及指针置空(防止野指针)
void MergeSort(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror(“fail malloc”);
exit(-1);
}
\_MergeSort(a, 0, n - 1, tmp);
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
* 接下去子函数的讲解,下面是函数体的定义
void _MergeSort(int* a, int begin, int end, int* tmp)
* 接下去就是要通过每次传进来的区间端点值求解中间值,然后再通过这个中间值进行区间左右不断划分,进行递归调用,最后再进行一个回调
int mid = (begin + end) >> 1;
/*
* [begin, mid][mid + 1, end]
* —>继续进行子区间归并,相当于后序遍历【左,右,根】
*/
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
* 有递归,那一定要有递归出口,那就是当这个区间不存在的时候
if (begin >= end)
{
return;
}
* 当一个区间的左右子区间都递归完成后,就要对这两个区间进行一个归并的逻辑,首先看到定义的【i】,这个是用来遍历tmp这个临时数组的,因为tmp会一直存放数据,因此在下一次还要存放数据的时候就要`从传进来的begin开始放置`,因为begin是在不断变化的
* 然后就要去记录左右两个区间的端点值,因为这个**mid是每次在变化的,加加减减不太方便**,因此需要将四个端点值分别第一出来,这样去判断的时候就方便多了
* 接下去的话就是两个区间的数据不断比较的过程,放在一个while循环里,结束条件就是当一个区间已经遍历完毕之后就退出,因为当一个区间遍历完后,一个区间的所有数据和另一个区间一定全都比较过了,因此另一个区间剩下来的数据一定是大的那些数,所以在跳出循环后直接将某一段区间中还剩下的数据接到tmp数组之后就行
若是上面这段逻辑没有听懂,可以看看这篇文章,也是讲解有关归并逻辑的题解 ----> [合并两个有序数组](https://bbs.csdn.net/topics/618545628)
int i = begin; //i要从每次递归进来的begin开始放,可能是在右半区间
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
* 如果说以上的步骤是最核心的一步,那么下面的这句就是最关键的一步🔑,因为每一次归并完后的数据都是存放在tmp临时数组里的,所以我们要将这些数据拷贝回原数组
* **因为我们每一次归并完都要去进行一个回拷的逻辑,所以每一次数组拷贝的起始位置和拷贝大小都是不一样的**,这要根据分解的区间而定,可能是1个、2个、3个、4个不等,所以我们可以通过每次变更的【begin】和【end】进行一个控制,而对于【`end - begin + 1`】就是本次需要回拷的数组个数即数组大小
//最后将归并完后后的数据拷贝回原数组
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
* 这是有关memcpy()这个函数的讲解,若是有不了解的小伙伴可以看一看
![在这里插入图片描述](https://img-blog.csdnimg.cn/87274d9b03cd45a2bc828f6b68c0242f.jpeg#pic_center)
---
* 下面是整体代码的展示
void _MergeSort(int* a, int begin, int end, int* tmp)
{
//递归出口
if (begin >= end)
{
return;
}
int mid = (begin + end) >> 1;
/\*
* [begin, mid][mid + 1, end]
* —>继续进行子区间归并,相当于后序遍历【左,右,根】
*/
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int i = begin; //i要从每次递归进来的begin开始放,可能是在右半区间
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//最后将归并完后后的数据拷贝回原数组
memcpy(a + begin, tmp + begin, sizeof(int) \* (end - begin + 1));
}
/*归并排序*/
//时间复杂度:O(NlogN)
//空间复杂度:O(N)
void MergeSort(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror(“fail malloc”);
exit(-1);
}
\_MergeSort(a, 0, n - 1, tmp);
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
#### 3.3 算法图解展示 && DeBug视频讲解【💻】
以下是递归分解算法图
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/6e284dcca5ac400fbec609dd9e221e2b.jpeg#pic_center)
>
>
>
以下是我录制的讲解视频(如果模糊的话请到b站观看)
归并排序递归版DeBug调试教学
### 4、非递归实现【校招要求✍】
>
> 说完递归,我们再来讲讲对于归并排序的非递归写法,重点在于理解递归写法,但是非递归校招也有要求,所以我会讲
>
>
>
#### 4.1 思路分析
* 首先来看一下这一块的思路该如何实现,在快排那一部分我们是借助栈实现的非递归,那这里我们也可以使用栈吗❓,但是在这里使用栈或者队列这些数据结构都是很难实现的,需要去控制区间的出入。假设若是借助队列来实现的话,入了左区间、右区间之后,要获取到【左区间】的子区间,那就要进行出队,然后再入队,但是呢此时低头就变成了【右区间】。若是想获取到【左区间】的子区间,就需要将【右区间】出队然后才能获取到,此时若是还要在拿到其左右区间的话又要出队入队,此时就会变得非常复杂,代码写起来也是非常得难控制,所以我们果断舍弃这种写法
* 其实对于归并的非递归来说有一种很简单的思路,也不需要利用额外的数据结构,只需要使用一个变量每次取控制每次归并的区间大小即可,因为对于归并排序不像快速排序那样每次key值的位置都是随机的,对于归并排序来说**每次归并的数量都固定的**,要么是两两一归并、或者四四一归并,但是在后面的特殊情况中我们还需要单独考虑
* 所以我们可以像下面这样去考虑,分为三次归并,首先是一一归并,使一个小区间中的两个数有序;然后两两归并,使一个小区间中的四个数有序;接下去四四归并,也就是使得正确区间有序。每一大组的归并放进一个循环中。但是每次要怎么使得刚好可以一一、两两、四四进行归并呢,**这就需要使用到一个【rangeN】的变量来控制每次归并区间的大小了**,具体如何控制我们放到代码中讲解
![在这里插入图片描述](https://img-blog.csdnimg.cn/a765efbeab124d2793774435462f21f0.jpeg#pic_center)
#### 4.2 普通情况展示【可恶的随机数👻】
##### 代码详解
* 接下去我们来说说该如何去实现上面这种写法呢,下面是单层循环的逻辑
int rangeN = 1;
for (int i = 0; i < n; i += 2 * rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
//归并…
//归并完一小组,拷贝一小组
memcpy(a + i, tmp + i, sizeof(int) \* (end2 - i + 1));
// + i表示每次归并的组在发生变化 //因为end是最后落的位置,i是初始化位置,不会改变
}
* 当然最主要的还是这四个边界值,也就是每次归并的左右两个小区间的确定,需要通过本次的循环和标记值一起来确定
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
* 这一块还是要自己手动去计算一下:
+ **当rangeN = 1时算出来左右两个区间中是否只有一个数,也就是左右端点都相同;**
+ **当rangeN = 2时算出来左右两个区间中是否只有两个数,也就是左右端点相差1;**
+ **当rangeN = 4时算出来左右两个区间中是否只有四个数,也就是左右端点相差3;**
* 以下是我做的一些计算
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/7deb4ef25c00466f83ee0afdf18987c0.jpeg#pic_center)
>
>
>
* 有了左右两个区间之后,就可以去进行【归并】了,这一块的逻辑我们在上面讲到过,因此直接复用即可。就是要重新换一个变量去接收每一趟循环中的i到哪里了,然后tmp数组也从哪里开始放置
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
* 归并完后还是一样,要去进行一个回拷,因为这是内部单趟逻辑的回拷,因此也和递归那里一样,需要利用当前已知的变量进行一个精确定位,**变量【i】即是当前开始的位置,而end2呢则是右区间结束的位置,也就是归并完成后的右端点位置**
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
* 然后上面只是一个单组的逻辑,我们需要将其放在一个循环中,这个循环是用来控制rangeN值的
while (rangeN < n)
{
//单组的归并回拷逻辑
rangeN *= 2;
}
* 以下是上述所讲解的整体代码。这样程序就可以跑起来了
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 * rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
printf(“[%d,%d][%d,%d]\n”, begin1, end1, begin2, end2);
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//归并完一小组,拷贝一小组
memcpy(a + i, tmp + i, sizeof(int) \* (end2 - i + 1));
// + i表示每次归并的组在发生变化 //因为end是最后落的位置,i是初始化位置,不会改变
}
rangeN \*= 2;
}
//这两句别忘了加
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
---
##### DeBug详细断位分析
* 然后我们使用这一段代码去进行一个测试,这里我放置了两个案例,一个是8个数,一个是10个数。为了可以精确地观测到左右区间的四个端点值,我们加上这一句话将单次的循环打印出来
printf(“[%d,%d][%d,%d]\n”, begin1, end1, begin2, end2);
* 可以看到对于下面这种测试案例没有问题。接着我们再来看一个
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/e6e7cdc3d5ff4a9ab9782908e8f38bc4.jpeg#pic_center)
>
>
>
* 可以看到,对于10个数的情况,出现了一些越界的端点值,所以程序就崩溃了
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/fb523fa0e96e4a40bc4902b1e93eb002.jpeg#pic_center)
>
>
>
* 我们再通过DeBug调试来看看
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/07cce57338d04aeeb5f007eea20048b7.jpeg#pic_center)
>
>
>
* 可以看到当range为1的时候,也就是一一进行归并时,不会出现越界的问题
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/ea6bb9898061445f9fb01f1a53fa9391.jpeg#pic_center)
>
>
>
* 此时我们进入下一层归并的逻辑,让rangeN变为2
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/f990aed9b862431792eb9bf027cefca8.jpeg#pic_center)
>
>
>
* 但是接下来再运行,就会出现问题了
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/1649bc8e0fac46fea87768f627c34c78.jpeg#pic_center)
>
>
>
* 然后再下去,就一发不可收拾了🚗
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/8d8e92c6bda64b99b0882b680b0601b4.jpeg#pic_center)
>
>
>
* 可以看出,越界是一个很大的问题,在下一模块中,我将针对越界的情况做一个讲解📖
#### 4.3 越界的特殊情况考虑
>
> 通过上一模块中的层层的DeBug调试,我们看到了 当rangeN = 2,也就是两两一归并的时候出现了越界的情况,为什么会发生这样的事情呢?记得我在一开始的时候有说过,对于归并排序,和快速排序不一样的地方在于,其**每次两个区间大小是确定的**,若是左半区间有四个数,**那么右半区间的大小也必须要能够存放得下四个数**
>
>
> 所以当待排序的数字有十个的时候,两两一归并,当前面八个归并完成后,后面的两个自动作为左半区间,那么此时右半区间就会产生越界的情况,在数组章节我们有讲到过,若是出现越界的情况就会出现随机值
>
>
>
**解决方案 —> 一 一分析越界的几种情况,然后对其分别做考虑判断**
![在这里插入图片描述](https://img-blog.csdnimg.cn/a6c997b4611046d899d4131e79e265e0.jpeg#pic_center)
##### 4.3.1 方法一【不修边界——部分拷贝】
* 下面这种是处理越界的第一种方法,就是不做边界修正,若是碰见有越界的情况直接break出当前小组的循环,也就是不进行归并的逻辑
/*
* 处理越界的情况
*/
if (end1 >= n) {
break;
}
else if (begin2 >= n) {
break;
}
else if (end2 >= n) {
end2 = n - 1; //end2越界要修正一下,不可以break,因为最后需要求整个数组的长度
}
* 此时我们需要去做一个部分的拷贝,也就是归并一部分拷贝一部分,若是做整体拷贝的话当越界的情况break出来进行拷贝回去之后原先a数组中的值就会被覆盖掉
//归并完一小组,拷贝一小组
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
// + i表示每次归并的组在发生变化 //因为end是最后落的位置,i是初始化位置,不会改变
* 也就是我们上面在分析普通的情况的时候归并完立马回拷的这个逻辑。现在再来看到话应该是很清晰了
![在这里插入图片描述](https://img-blog.csdnimg.cn/ff75e973ac33432da507aae97598eb2f.jpeg#pic_center)
* 然后我们来看一下处理越界情况后的运行结果💻
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/bff559e258e3482eb27bc2037608450f.jpeg#pic_center)
>
>
>
##### 4.3.2 方法二【修边界——整体拷贝】
* 第二种是整体拷贝,当一层的逻辑执行完后,所有的小组都归并完成了,此时再去进行一个整体回拷的逻辑,因为这个时候我们在遇到越界的问题时需要对边界去做一个修正,当修正完之后也就不会出现问题了
* 我们来看一下这种情况该如何修正,很简单:
+ **若是就右端点越界的话只需要将其修正为数组下标的最后一个位置即可;**
+ **若是整个区间都越界了,那就修改一下【begin】 > 【end】即可;**
/*
* 处理越界的情况
*/
if (end1 >= n) {
end1 = n - 1;
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else if (begin2 >= n) {
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else if (end2 >= n) {
end2 = n - 1;
}
* 若是要整体回拷的话`memcpy(a, tmp, sizeof(int) * n);`就要放在单层循环外了,可以看到每次拷贝的区间大小都是固定的
* 来看一下这种情况的算法分解图。可以看到,就是当当层的逻辑全部走完之后再进行的一个整体回拷
![在这里插入图片描述](https://img-blog.csdnimg.cn/d0932ff819b44d1797dc8e3623169cbc.jpeg#pic_center)
* 接下去来看看修正后的运行结果💻
![在这里插入图片描述](https://img-blog.csdnimg.cn/8dea799eeafa49158562d8aae89e4d13.jpeg#pic_center)
#### 4.4 小结
>
> 可以看到,对于归并排序来说,无论是递归还是非递归,都是存在一定难度的,特别是对于归并的非递归这一块,光边界修正这一块就让人头大(((φ(◎ロ◎;)φ))),但是大家还是要有所掌握,上面说过,对于一些场景使用非递归要比递归来的安全很多 🛡
>
>
>
---
**上述的七个排序均为比较类排序,接下去我们来介绍三种非比较类排序**
## 八、计数排序【还是不错的】
### 1、动图演示
![在这里插入图片描述](https://img-blog.csdnimg.cn/6accaf7c41fd4327af6919d3f2ba74fb.gif#pic_center)
### 2、算法思路简析
【核心思路】:通过统计相同元素出现次数,存放到一个数组中,然后再根据统计的结果将序列回收到原来的序列中
* 然后我们来看一下有关计数排序的大体步骤,我是分为以下三步
![在这里插入图片描述](https://img-blog.csdnimg.cn/ff14f5f8e70a4db9b8a3fa73fafe36b7.jpeg#pic_center)
### 3、具体代码分析
>
> 接下去讲刚才分析的思路转化为代码
>
>
>
**①开辟统计次数的数组**
* 因为我们要开辟统计次数的数组,但是不知道开多大,所以应该先去找出数组中的最大值与最小值,然后求出range【范围】
//1.找出数组中的最大值和最小值,然后开辟统计数组
int min = a[0];
int max = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
* 有了最大最小值之后,就可以去求解范围了,这个范围即是数组的大小。
* 在开出数组之后,记得对其进行一个初始化,也就是使用[memset](https://bbs.csdn.net/topics/618545628)将所有位上的数都设置为0
int range = max - min + 1; //数据范围
int* CountArray = (int*)malloc(sizeof(int) * range);
memset(CountArray, 0, sizeof(int) * range); //初始化数组均为0
**②遍历原数组,统计出每个数字出现的次数,将其一一映射到CountArray数组中**
//2.统计数组中每一个数出现的个数,映射到CountArray数组中
for (int i = 0; i < n; ++i)
{
CountArray[a[i] - min]++;
//a[i] - min 表示找出相对位置
}
* 重点说一下这个`a[i] - min`是什么意思,对于我在上一模块展示的只是一个特殊情况,最小值是从0开始的,刚好可以和CountArray数组中的下标对上,但是对于大多数的情况来说,最小值不会是0,所以在一一映射的过程中就需要做一些变化
![在这里插入图片描述](https://img-blog.csdnimg.cn/dab282019aea4eb28b3ee8b34c592d5f.jpeg#pic_center)
* 可以看到,上面这种情况的最小值就为1000, range的求解还是一个套路,但是对于映射就不一样了,因为我们开出的数组下标只有0~6,所以此时我们要使用一个【相对位置】去算出每个数字对应的位置,使用`a[i] - min`即可
**③根据每个数字统计完后的次数,一一放回原数组**
* 刚才使用`a[i] - min`映射到了CountArray数组,现在我们要将其再一一放回去,此时你就会发现数组便会呈现有序
* 但是这要怎么放回去呢,首先外层先去遍历这个CountArray数组,内层的while循环表示每一个数字对应的要放回几次
* 接下去要考虑的问题就是如果通过这个下标去还原回原先的数字,刚才我们减了min,现在加回去即可`j + min`
//3.将统计数组中的数写回原数组中,进行排序
int index = 0;
for (int j = 0; j < range; ++j)
{
//根据统计数组去找出每个数要写回几次
while (CountArray[j]–)
{
a[index++] = j + min;
//每次循环中的j不会发生变化,加上最小值可以找回原来的数字
}
}
整体代码展示
/*计数排序*/
void CountSort(int* a, int n)
{
//1.找出数组中的最大值和最小值,然后开辟统计数组
int min = a[0];
int max = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int range = max - min + 1; //数据范围
int\* CountArray = (int\*)malloc(sizeof(int) \* range);
memset(CountArray, 0, sizeof(int) \* range); //初始化数组均为0
//2.统计数组中每一个数出现的个数,映射到CountArray数组中
for (int i = 0; i < n; ++i)
{
CountArray[a[i] - min]++;
//a[i] - min 表示找出相对位置
}
//3.将统计数组中的数写回原数组中,进行排序
int index = 0;
for (int j = 0; j < range; ++j)
{
//根据统计数组去找出每个数要写回几次
while (CountArray[j]--)
{
a[index++] = j + min;
//每次循环中的j不会发生变化,加上最小值可以找回原来的数字
}
}
}
运行结果
* 刚才说到的两种情况都展示一下
![在这里插入图片描述](https://img-blog.csdnimg.cn/281c570b71ef4c5b8b7b18a638f8d134.jpeg#pic_center)
![在这里插入图片描述](https://img-blog.csdnimg.cn/12dea262c80c44dab37b814d3978e14f.jpeg#pic_center)
### 4、复杂度分析
>
> 来看看计数排序的时空复杂度
>
>
>
**【时间复杂度】:O(N + range)
【空间复杂度】:O(range)**
* 首先对于时间复杂度而言,有同学就看不懂这个【N + range】是什么意思,观看代码看出我们遍历了两遍原数组和一遍统计个数的数组,对于2N可以简化成N,因此就是【N + range】,完全是要看这个CountArray数组的大小是多少,若是
+ range <= N ——> O(N + N) = **O(N)**
+ range > N ——>O(N + range) = **O(range)**
* 对于空间复杂度也是取决于这个CountArray数组的大小,便是**O(range)**
对于计数排序而言,因为需要统计数据出现的次数,所以只能用与整型的数据,如果是浮点数或字符串排序还得用**比较排序**
## 九、桶排序【局限性大】
### 1、动图演示
![在这里插入图片描述](https://img-blog.csdnimg.cn/a9ff6f9ebe9a4bc0bea9a6fc8874af28.gif#pic_center)
### 2、算法思路简析
>
> 看了上面的通动图之后,你应该对桶排序有了以及基本的认识,接下去我们一起来了解一下如何使用【桶】来进行排序
>
>
>
* 排序的思路很简单,就是将待排序的数组中每一个数根据他们的范围一一放入对应的桶中,然后在每一个桶的内部分别对其进行排序,在每个桶都排完序之后,再从第一个桶开始,将其中的数据一一放回原数组即可
* 以下是算法分解图
![在这里插入图片描述](https://img-blog.csdnimg.cn/dce06916379746da843d25976096947a.jpeg#pic_center)
### 3、代码详解与分析
* 首先我们需要定义出桶以及每个桶中的计数器
int bucket[5][5]; // 分配五个桶。
int bucketsize[5]; // 每个桶中元素个数的计数器。
* 接下去使用memset()对其进行初始化
// 初始化桶和桶计数器。
memset(bucket, 0, sizeof(bucket));
memset(bucketsize, 0, sizeof(bucketsize));
* 接下来的工作是最重要的一步,那就是将数组a中的内容数据一一放入对应的桶中
* 这里的桶我开的是一个二维数组,行记录的是数据,列记录的是该桶中数据的个数,分别对每个数据进行整除10就可以刚好让其落入对应的桶中,你可以自己去算算
// 把数组a的数据按照范围放入对应桶中
for (int i = 0; i < n; ++i)
{
bucket[a[i] / 10][bucketsize[a[i] / 10]++] = a[i];
}
* 接下去的话就是对每个桶中的数据进行排序了,我们可以使用上面学过的一些内部排序,这里我选择使用【快速排序】
// 分别对每个桶中的数据进行排序
for (int i = 0; i < 5; ++i)
{
QuickSort(bucket[i], 0, bucketsize[i] - 1);
}
* 最后一步的话就是将每个桶中对应的数据依次放回数组中即可
// 将把每个桶中的数据依次放回数组a中
int index = 0;
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < bucketsize[i]; ++j)
{
a[index++] = bucket[i][j];
}
}
整体代码展示
/*桶排序*/
void BucketSort(int* a, int n)
{
int bucket[5][5]; // 分配五个桶。
int bucketsize[5]; // 每个桶中元素个数的计数器。
// 初始化桶和桶计数器。
memset(bucket, 0, sizeof(bucket));
memset(bucketsize, 0, sizeof(bucketsize));
// 把数组a的数据按照范围放入对应桶中
for (int i = 0; i < n; ++i)
{
bucket[a[i] / 10][bucketsize[a[i] / 10]++] = a[i];
}
// 分别对每个桶中的数据进行排序
for (int i = 0; i < 5; ++i)
{
QuickSort(bucket[i], 0, bucketsize[i] - 1);
}
// 将把每个桶中的数据依次放回数组a中
int index = 0;
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < bucketsize[i]; ++j)
{
a[index++] = bucket[i][j];
}
}
}
运行结果
![在这里插入图片描述](https://img-blog.csdnimg.cn/4b4e9b7563674844a80654621e717fd9.jpeg#pic_center)
### 4、DeBug调试分析
>
> 这一环节我们跟着调试信息一步步地;来分析一下
>
>
>
* 首先是对于数组的初始化
![在这里插入图片描述](https://img-blog.csdnimg.cn/e735a547ca3f49cea0beb1e4134fd059.jpeg#pic_center)
* 接下去展示数据放入对应的桶中,这里我们通过视频来观看
* 接下去就是对每个桶中的数据进行排序
![在这里插入图片描述](https://img-blog.csdnimg.cn/693aaab572a5494a939af1dccb4c8175.jpeg#pic_center)
![在这里插入图片描述](https://img-blog.csdnimg.cn/3fd7f624ecb94baa8de5722bb3c2e7bf.jpeg#pic_center)
* 最后将每个桶中的数据一一放回去即可
![在这里插入图片描述](https://img-blog.csdnimg.cn/acb1f1f93026498c9007695daf6c4174.jpeg#pic_center)
### 5、复杂度分析
**【时间复杂度】:O(K + N)
【空间复杂度】:O(K + N)**
* 这一块后面再做更新。。。还在搜寻资料中
## 十、基数排序【不常用】
### 1、动图演示
![在这里插入图片描述](https://img-blog.csdnimg.cn/3a59d10987704ede879c01884549a304.gif#pic_center)
### 2、算法思路分析
>
> 是的,你没有看错,除了【计数排序】之外,还有一个叫做【基数排序】的,不过它和计数排序可完全不同。它是**桶排序的升级版**
>
>
>
* 对于基数排序而言,它的原理就是【基于数位来排序】,什么是数位呢?也就是我们常说的个位、十位、百位
* 首先我们要去取到每个数的个位,通过观测它们的个位,将一 一放入对应的桶中,那此时我们就需要一些桶。因为每次是都是取一个数,所以我们需要十个桶【0~9】,但是这些桶不是一般的桶,而是要为一个链表或者队列,因为要实现先分发入桶中的数据先回收出来,因为是一个【FIFO】的原理,所以这里我们优先采用队列,当然你想要使用链表也是可以的了,那么 **【分发数据】就是【尾插】,【回收数据】就是【头删】**
* 有了这些桶之后,那就方便多了,此时只需要看个位的数字是多少,然后一一链接上去即可。接下去就是去比较各个数字十位或者百位的数据,当然这要取决于整组数据中位数最大的那个数字,所以在分发数据前我们应该先去求出这些数中最大的那个数,然后再求出这个数的位数,那它的位数多少位,那就需要比较多少论
### 3、代码分析与算法图解
* 有了基本的思路之后,接下去我们先来看看大体的运行流程
**①第一轮(个位的比较)**
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/c113fce560c741038890c0a59897f890.jpeg#pic_center)
>
>
>
**②第二轮(十位的比较)**
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/74bfde574d3b46b6a16963e156fca259.jpeg#pic_center)
>
>
>
**③第三轮(百位的比较)**
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/77274d15aa4d4a4c9449e88d7cbbf44b.jpeg#pic_center)
>
>
>
* 好,看了这三轮比较之后,相信你对基数排序的大体流程有了一个更加全面的了解,接下去我们将上面所说的内容转化为代码的形式
---
* 首先要去做一些准备工作。也就是把桶定义出来,桶的个数我们固定为10个,因为【0~9】这个十个数字都有可能会出现。这桶的底层结构使用的是C++STL中的队列容器,如果不了解的看这个 --> [C++STL容器详解](https://bbs.csdn.net/topics/618545628)
#include
#define RADIX 10 //表示基数的个数
queue qu[RADIX]; //定义桶(每个桶均为一个队列)
* 接下去就是主体代码
void RadixSort(int* a, int n)
{
//首先求出数组中的最大值
int max = GetMax(a, n);
//求出最大值的位数
int k = GetDigit(max);
//进行k次的数据分发和回收
for (int i = 0; i < k; ++i)
{
//分发数据
Distribute(a, n, i);
//回收数据
Collect(a);
}
}
求解数组中的最大值
//求解数组中的最大值
int GetMax(int* a, int n)
{
int max = a[0];
for (int i = 0; i < n; ++i)
{
if (a[i] > max)
max = a[i];
}
return max;
}
求解最大值的位数
//求解最大值的位数
int GetDigit(int num)
{
//num : 10000
int count = 0;
while (num > 0)
{
count++;
num /= 10;
}
return count;
}
获取位数位逻辑
//value: 789
// k: 0
int GetKey(int value, int k)
{
int key = 0;
while(k >= 0)
{
key = value % 10;
value /= 10;
k–;
}
return key;
}
分发数据逻辑
获取key之后,`q[key]`即为每一个桶。使用push往里入数据
//分发数据
void Distribute(int* a, int n, int k)
{
for (int i = 0; i < n; ++i)
{
int key = GetKey(a[i], k);
qu[key].push(a[i]);
}
}
回收数据逻辑
接着一一获取每个桶中的头部数据,放回数组中,然后出队该数据即可
//回收数据
void Collect(int* a)
{
int index = 0;
for (int i = 0; i < RADIX; ++i)
{
while (!qu[i].empty())
{
a[index++] = qu[i].front();
qu[i].pop();
}
}
}
运行结果
* 可以灵活地找出数组中的最大值后,我们可以测试所有的案例
![在这里插入图片描述](https://img-blog.csdnimg.cn/2dfb471045d64fb4bf6ae849bb46b618.jpeg#pic_center)
![在这里插入图片描述](https://img-blog.csdnimg.cn/efb53906200b49e092620470a244cdfd.jpeg#pic_center)
![在这里插入图片描述](https://img-blog.csdnimg.cn/1e097afbdcc148cc829d35fb0d0833d8.jpeg#pic_center)
### 4、复杂度分析
**【时间复杂度】:O(K \* N)
【空间复杂度】:O(K + N)**
* 可以看到,对于基数排序的时间复杂度而言,我们通过观察代码可以看出,主函数有一个K层的外循环,然后里面套了两层循环,对于【分发数据】中,需要去遍历一遍原数组,这里就有O(N)了,然后执行K次去找到那个Key值,这个为常数次可以忽略,接下去对于【回收数据】而言,只运行了RADIX次,也为常数阶可以忽略,这么看下来只剩外层的一个K层循环套一个内层的O(N),因此可以看出其时间复杂度为**O(K \* N)**
* 对于基数排序空间复杂度应该为**O(K + N)**
---
## 📚拓展:文件外排序【了解一下】
>
> 排序这一块,除了对内部的数据进行排序,很多场合下还会对文件中的数据去进行一个排序,而对于文件外排序这一块,我们主要使用上面所学的归并排序来完成
>
>
>
### 1、前言
* 对于文件中的数据,一般都是很大的,不像我们上面所讲的十二十个数,可能会有**成千上百的数据**需要我们去排序,此时效率最高的就是【归并排序】了,因为面对海量的数据而言,像效率较高的【快速排序】需要克服三数取中的困难,还有像【堆排序】【希尔排序】这些,都无法支持随机访问,所以很难去对大量的文件进行一个排序,速度会非常之慢。即使是有文件函数【fseek()】这样的函数可以使**文件指针偏移**,还是很难做到高效。因为磁盘的速度比起内存差了太多太多了,具体的我不太清楚大概有差个几千倍这样,
* 所以我们就想到了【归并排序】,它既是内排序,也是外排序,而且性能也不差,算是速度较快的几个排序之一了。但是要如何进行归并呢?
### 2、思路解析
![在这里插入图片描述](https://img-blog.csdnimg.cn/a3148e41d43e450496e1688853ea46f6.jpeg#pic_center)
* 回忆一下归并排序的原理,就是两个有序区有序,然后两两一归才使得整体可以有序,如果左右都无需,那么继续对其进行左右分割归并
* 但是本次,我要教给你的你是另外一种思路:
**将一个大文件平均分割成N份,保证每份的大小可以加载到内存中,然后使用快排将其排成有序再写回一个个小文件,此时就拥有了文件中归并的先决条件**
* 具体示意图如下
![在这里插入图片描述](https://img-blog.csdnimg.cn/12088b16911541c3b2bd55f791354251.jpeg#pic_center)
这里我设置一个这样的规则,令文件1为【1】,文件2位【2】,它们归并之后即为【12】,然后再让【12】和文件3即【3】归并变成【123】,以此类推,所以最后归出的文件名应该是【12345678910】
### 3、代码详解
>
> 下面是大文件分割成10个小文件的逻辑,首先来讲解一下这块,代码中很多内容涉及到文件操作,如果有文件操作还不是很懂的小伙伴记得再去温习一下
>
>
>
* 整体的逻辑就在于从文件中读取100个数据,但是分批进行读取,每次首先去读9个数,然后当读到第十个数的时候,先将其加入数组中,然后再对数组中的这10个数进行排序。排完序后就将这个10个数通过文件指针再写到一个小文件中
* 接着当第二次循环上来的时候,就开始读第11~20个数;以此往复,直到读完这个100个数为止,那此时我们的工程目录下就会出现10个小文件,就是对这100个数的分隔排序后的结果
void MergeSortFile(const char* file)
{
FILE* fout = fopen(file, “r”);
if (!fout)
{
perror(“fopen fail”);
exit(-1);
}
int num = 0;
int n = 10;
int i = 0;
int b[10];
char subfile[20];
int filei = 1;
//1.读取大文件,然后将其平均分成N份,加载到内存中后对每份进行排序,然后再写回小文件
memset(b, 0, sizeof(int) \* n);
while (fscanf(fout, "%d\n", &num) != EOF)
{
if (i < n - 1)
{
b[i++] = num; //首先读9个数据到数组中
}
else
{
b[i] = num; //再将第十个输入放入数组
QuickSort(b, 0, n - 1); //对其进行排序
sprintf(subfile, "%d", filei++);
FILE\* fin = fopen(subfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
//再进本轮排好序的10个数以单个小文件的形式写到工程文件下
for (int j = 0; j < n; ++j)
{
fprintf(fin, "%d\n", b[j]);
}
fclose(fin);
i = 0; //i重新置0,方便下一次的读取
memset(b, 0, sizeof(int) \* n);
}
}
* 我们来看一下排序的结果
![在这里插入图片描述](https://img-blog.csdnimg.cn/6bc0b21e9aaa4c3eb69b3c193b990724.jpeg#pic_center)
* 将大文件分成10个小文件后,接下去就是要对这个10个小文件进行归并,具体规则我上面已经说了
* 下面就是单趟归并的逻辑的,就和我们上面说到的归并排序的代码是很类似的,只不过这里是文件的操作而已。**要注意的是对于文件来说是有一个文件指针的**,若是你读取了一个之后那么文件指针这个结构体中的数据标记就会发生变化,标记为当然所读内容的下一个了
* 所以我们**不能将读取读取小文件中的数据的操作放在while循环中,应该单独将其抽离出来进行判断才才对**。若是哪个文件中的数小,那么就将这个数写到新的【mfile】文件中去,然后继续读取当前文件的后一个内容
//文件归并逻辑
void _MergeSortFile(const char* file1, const char* file2, const char* mfile)
{
FILE* fout1 = fopen(file1, “r”);
if (!fout1)
{
perror(“fopen fail”);
exit(-1);
}
FILE\* fout2 = fopen(file2, "r");
if (!fout2)
{
perror("fopen fail");
exit(-1);
}
FILE\* fin = fopen(mfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
int num1, num2;
//返回值拿到循环外来接受
int ret1 = fscanf(fout1, "%d\n", &num1);
int ret2 = fscanf(fout2, "%d\n", &num2);
while (ret1 != EOF && ret2 != EOF)
{
if (num1 < num2)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
else
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
}
while (ret1 != EOF)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
while (ret2 != EOF)
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
fclose(fout1);
fclose(fout2);
fclose(fin);
}
**最后在打开文件后不要忘了将文件关闭哦,不然就白操作了**
* 当然上面是一个单趟的逻辑,我们还要对【file1】【file2】【mfile】进行一个迭代
//利用互相归并到文件,实现整体有序
char file1[100] = “1”;
char file2[100] = “2”;
char mfile[100] = “12”;
for (int i = 2; i <= n; ++i)
{
_MergeSortFile(file1, file2, mfile);
//迭代
strcpy(file1, mfile);
sprintf(file2, "%d", i + 1);
sprintf(mfile, "%s%d", mfile, i + 1);
}
* 大概就是这么一个迭代的过程
>
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/41007eda89814f0fb404af600415077e.jpeg#pic_center)
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/4890fd12db80468a8ce070b06698902c.jpeg#pic_center)
>
>
>
整体代码展示
//文件归并逻辑
void _MergeSortFile(const char* file1, const char* file2, const char* mfile)
{
FILE* fout1 = fopen(file1, “r”);
if (!fout1)
{
perror(“fopen fail”);
exit(-1);
}
FILE\* fout2 = fopen(file2, "r");
if (!fout2)
{
perror("fopen fail");
exit(-1);
}
FILE\* fin = fopen(mfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
int num1, num2;
//返回值拿到循环外来接受
int ret1 = fscanf(fout1, "%d\n", &num1);
int ret2 = fscanf(fout2, "%d\n", &num2);
while (ret1 != EOF && ret2 != EOF)
{
if (num1 < num2)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
else
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
}
while (ret1 != EOF)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
while (ret2 != EOF)
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
fclose(fout1);
fclose(fout2);
fclose(fin);
}
/*文件外排序*/
void MergeSortFile(const char* file)
{
srand((unsigned int)time(NULL));
FILE* fout = fopen(file, “r”);
if (!fout)
{
perror(“fopen fail”);
exit(-1);
}
//先写100个随机数进文件
//for (int i = 0; i < 100; ++i)
//{
// int num = rand() % 100;
// fprintf(fout, "%d\n", num);
//}
int num = 0;
int n = 10;
int i = 0;
int b[10];
char subfile[20];
int filei = 1;
//1.读取大文件,然后将其平均分成N份,加载到内存中后对每份进行排序,然后再写回小文件
memset(b, 0, sizeof(int) \* n);
while (fscanf(fout, "%d\n", &num) != EOF)
{
if (i < n - 1)
{
b[i++] = num; //首先读9个数据到数组中
}
else
{
b[i] = num; //再将第十个输入放入数组
QuickSort(b, 0, n - 1); //对其进行排序
sprintf(subfile, "%d", filei++);
FILE\* fin = fopen(subfile, "w");
if (!fin)
{
perror("fopen fail");
exit(-1);
}
//再进本轮排好序的10个数以单个小文件的形式写到工程文件下
for (int j = 0; j < n; ++j)
{
fprintf(fin, "%d\n", b[j]);
}
fclose(fin);
i = 0; //i重新置0,方便下一次的读取
memset(b, 0, sizeof(int) \* n);
}
}
//利用互相归并到文件,实现整体有序
char file1[100] = "1";
char file2[100] = "2";
char mfile[100] = "12";
for (int i = 2; i <= n; ++i)
{
\_MergeSortFile(file1, file2, mfile);
//迭代
strcpy(file1, mfile);
sprintf(file2, "%d", i + 1);
sprintf(mfile, "%s%d", mfile, i + 1);
}
}
运行结果展示
![在这里插入图片描述](https://img-blog.csdnimg.cn/29e9eba45b024450bceb5e737afeb23d.jpeg#pic_center)
## 💻整体代码展示
>
> 本次的代码较以往都多了许多,因为是集结了所有的排序算法还有案例测试、性能测试相关的代码,都给到大家
>
>
>
sort.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include
#include <time.h>
#include
#include
using namespace std;
/*直接插入排序*/
void InsertSort(int* a, int n);
/*希尔排序*/
void ShellSort(int* a, int n);
/*传统选择排序*/
void SelectedSort(int* a, int n);
/*简单选择排序*/
void SelectSort(int* a, int n);
/*堆排序*/
void HeapSort(int* a, int n);
/*冒泡排序*/
void BubbleSort(int* a, int n);
/*hoare版本(左右指针法)*/
int PartSort1(int* a, int begin, int end);
/*挖坑法*/
int PartSort2(int* a, int begin, int end);
/*前后指针法*/
int PartSort3(int* a, int begin, int end);
/*三数取中*/
int GetMid(int* a, int left, int right);
/*快速排序*/
void QuickSort(int* a, int begin, int end);
/*快速排序——三路划分法*/
void QuickSortThreeDivisioin(int* a, int begin, int end);
/*快速排序——非递归*/
void QuickSortNonR(int*, int begin, int end);
/*归并排序*/
void MergeSort(int* a, int n);
/*归并排序——非递归*/
void MergeSortNonR1(int* a, int n); //不修边界——部分拷贝
void MergeSortNonR2(int* a, int n); //修边界——整体拷贝
/*文件外排序*/
void MergeSortFile(const char* file);
/*计数排序*/
void CountSort(int* a, int n);
/*基数排序*/
void RadixSort(int* a, int n);
/*桶排序*/
void BucketSort(int* a, int n);
/*打印*/
void PrintArray(int* a, int n);
/*交换函数*/
void swap(int* x1, int* x2);
/*向下调整算法*/
void Adjust_Down(int* a, int n, int parent);
sort.c
#define _CRT_SECURE_NO_WARNINGS 1
#include “sort.h”
#include “stack.h”
//#define K 3 //表示数字的位数
#define RADIX 10 //表示桶的个数(固定)
queue qu[RADIX]; //定义基数(每个基数均为一个队列)
/*打印*/
void PrintArray(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
printf(“%d “, a[i]);
}
printf(”\n”);
}
/*交换*/
void swap(int* x, int* y)
{
int t = *x;
*x = *y;
*y = t;
}
/*直接插入排序*/
void InsertSort(int* a, int n)
{
//不可以< n,否则最后的位置落在n-1,tmp访问end[n]会造成越界
for (int i = 0; i < n - 1; ++i)
{
int end = i;
int tmp = a[end + 1]; //将end后的位置先行保存起来
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end]; //比待插值来得大的均往后移动
end–; //end前移
}
else
{
break; //若是发现有相同的或者小于带插值的元素,则停下,跳出循环
}
}
a[end + 1] = tmp; //将end + 1的位置放入保存的tmp值
}
}
//void ShellSort(int* a, int n)
//{
// int gap = n / 2;
//
// for (int j = 0; j < gap; ++j)
// {
// gap /= 2;
// for (int i = j; i < n - gap; i += gap)
// { //一组一组走
// int end = i;
// int tmp = a[end + gap];
// while (end >= 0)
// {
// if (tmp < a[end])
// {
// a[end + gap] = a[end];
// end -= gap;
// }
// else
// {
// break;
// }
// }
// a[end + gap] = tmp;
// }
// }
//}
/*希尔排序*/
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
/*
* gap > 1 —— 预排序
* gap == 1 —— 直接插入排序
*/
//gap /= 2;
gap = gap / 3 + 1; //保证最后的gap值为1,为直接插入排序
for (int i = 0; i < n - gap; i++)
{ //一位一位走
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
PrintArray(a, n);
}
}
/*传统选择排序*/
void SelectedSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int k = i;
for (int j = i + 1; j < n; ++j)
{
if (a[j] < a[k]) k = j;
}
if (a[k] != a[i])
{
swap(&a[k], &a[i]);
}
}
}
/*简单选择排序*/
void SelectSort(int* a, 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 (a[i] < a[mini])
mini = i;
if (a[i] > a[maxi])
maxi = i;
}
swap(&a[begin], &a[mini]);
if (maxi == begin) //若是最大值和begin重合了,则重置一下交换后的最大值
maxi = mini;
swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
/*向下调整算法*/
void Adjust_Down(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)
{
Adjust_Down(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]); //首先交换堆顶结点和堆底末梢结点
Adjust\_Down(a, end, 0); //一一向前调整
end--;
}
}
/*冒泡排序*/
void BubbleSort(int* a, int n)
{
//[0,n - 1)
for (int i = 0; i < n - 1; ++i)
{
int changed = 0;
for (int j = 0; j < n - 1 - i; ++j)
{
if (a[j] > a[j + 1])
{
swap(&a[j], &a[j + 1]);
changed = 1;
}
}
if (changed == 0)
break;
//PrintArray(a, n);
}
//[1,n)
//for (int i = 1; i < n; ++i)
//{
// for (int j = 0; j < n - i; ++j)
// {
// if (a[j] > a[j + 1])
// swap(&a[j], &a[j + 1]);
// }
//}
}
/*三数取中*/
int GetMid(int* a, int left, int right)
{
/*
* 不是取中间的那个值,而是取三个数中不是最大也不是最小的那个
* —>进来可能是一个随机或有序序列,保证key最大或者最小就行
*/
int mid = (left + right) >> 1;
//int mid = left + rand() % (right - left);
if (a[mid] > a[left])
{
if (a[mid] < a[right])
{ //left mid right
return mid;
}
//另外两种mid一定是最大的,比较left和right
else if (a[left] > a[right])
{ //right left mid
return left;
}
else
{ //left right mid
return right;
}
}
else //a[left] >= a[mid]
{
if (a[mid] > a[right])
{ //right mid left
return mid;
}
//另外两种mid一定是最小的,比较left和right
else if (a[left] < a[right])
{ //mid left right
return left;
}
else
{ //mid right left
return right;
}
}
}
/*快速排序 —— hoare版本(左右指针法)*/
int PartSort1(int* a, int begin, int end)
{
/*
* 找小 —— 是找真的比我小的
* 找大 —— 是找真的比我大的
* —>相等均要略过
*
* 还要防止越界【left < right】
*/
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]); //交换begin上的值和找出的中间值
int left = begin;
int right = end;
int keyi = begin; //以最左边作为对照值记录,右边先走
while (left < right)
{
//右边找比keyi所在位置小的值,若是 >= 则忽略(加上=防止死循环)
while (left < right && a[right] >= a[keyi])
{ //left < right —— 防止特殊情况越界
right--;
}
//左边找比keyi所在位置大的值,若是 <= 则忽略
while (left < right && a[left] <= a[keyi])
{
left++;
}
//都找到了,则交换
swap(&a[left], &a[right]);
}
//最后当left和right相遇的时候将相遇位置的值与keyi位置的值交换
swap(&a[left], &a[keyi]);
keyi = left; //因keyi位置的值被交换到相遇点,因此更新准备分化递归
return keyi;
}
/*快速排序 —— 挖坑法*/
int PartSort2(int* a, int begin, int end)
{
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int left = begin, right = end;
int hole = left; //坑位
int key = a[hole]; //记录坑位上的值
while (left < right)
{
//右边找小,放入坑位,然后更新坑位
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
//左边找大,放入坑位,然后更新坑位
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
/*快速排序 —— 前后指针法*/
int PartSort3(int* a, int begin, int end)
{
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int prev = begin;
int cur = prev + 1;
int keyi = prev;
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
swap(&a[prev], &a[cur]);
}
cur++; //cur无论如何都要++,因此直接写在外面
}
//cur超出右边界后交换prev处的值和key
swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
//小区间优化
if ((end - begin + 1) < 15)
{
//在数据量少的时候改用直接插入排序
InsertSort(a + begin, end - begin + 1);
}
//int key = PartSort1(a, begin, end); //左右指针法
//int key = PartSort2(a, begin, end); //挖坑法
int key = PartSort3(a, begin, end); //前后指针法
//左右区间分化递归
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
/*快速排序——三路划分法*/
void QuickSortThreeDivisioin(int* a, int begin, int end)
{
if (begin >= end)
return; //最后递归下去区间不存在了,进行递归回调
//小区间优化
if ((end - begin + 1) < 15)
{
//在数据量少的时候改用直接插入排序
InsertSort(a + begin, end - begin + 1);
}
else
{
//三数取中
int mid = GetMid(a, begin, end);
swap(&a[begin], &a[mid]);
int key = a[begin];
int left = begin;
int cur = left + 1;
int right = end;
while (cur <= right)
{
if (a[cur] < key)
{
swap(&a[left++], &a[cur++]);
}
else if (a[cur] > key)
{
swap(&a[cur], &a[right--]);
//此时cur不变化是因为从right换到中间的值可能还是比key大,为了在下一次继续进行比较
}
else
{
cur++;
}
}
//[begin, left - 1][left, right][right + 1, end]
QuickSortThreeDivisioin(a, begin, left - 1);
QuickSortThreeDivisioin(a, right + 1, end);
}
}
/*快速排序——非递归*/
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
InitStack(&st);
//首先将整体区间入栈
PushStack(&st, begin);
PushStack(&st, end);
while (!StackEmpty(&st))
{
//出栈分别获取右左两个端点
int right = StackTop(&st);
PopStack(&st);
int left = StackTop(&st);
PopStack(&st);
//求解keyi的位置
int keyi = PartSort3(a, left, right);
//先入右
if (keyi + 1 < right)
{ //若是区间的值 > 1,则继续入栈
PushStack(&st, keyi + 1);
PushStack(&st, right);
}
//后入左
if (left < keyi - 1)
{ //若是区间的值 > 1,则继续入栈
PushStack(&st, left);
PushStack(&st, keyi - 1);
}
}
DestroyStack(&st);
}
void _MergeSort(int* a, int begin, int end, int* tmp)
{
//递归出口
if (begin >= end)
{
return;
}
int mid = (begin + end) >> 1;
/\*
* [begin, mid][mid + 1, end]
* —>继续进行子区间归并,相当于后序遍历【左,右,根】
*/
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int i = begin; //i要从每次递归进来的begin开始放,可能是在右半区间
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//最后将归并完后后的数据拷贝回原数组
memcpy(a + begin, tmp + begin, sizeof(int) \* (end - begin + 1));
}
/*归并排序*/
//时间复杂度:O(NlogN)
//空间复杂度:O(N)
void MergeSort(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror(“fail malloc”);
exit(-1);
}
\_MergeSort(a, 0, n - 1, tmp);
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
/*归并排序——非递归*/
void MergeSortNonR1(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror(“fail malloc”);
exit(-1);
}
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 \* rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 \* rangeN - 1;
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
/\*
* 处理越界的情况
*/
if (end1 >= n) {
break;
}
else if (begin2 >= n) {
break;
}
else if (end2 >= n) {
end2 = n - 1; //end2越界要修正一下,不可以break,因为最后需要求整个数组的长度
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//归并完一小组,拷贝一小组
memcpy(a + i, tmp + i, sizeof(int) \* (end2 - i + 1));
// + i表示每次归并的组在发生变化 //因为end是最后落的位置,i是初始化位置,不会改变
}
rangeN \*= 2;
}
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
void MergeSortNonR2(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror(“fail malloc”);
exit(-1);
}
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 \* rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 \* rangeN - 1;
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
/\*
* 处理越界的情况
*/
if (end1 >= n) {
end1 = n - 1;
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else if (begin2 >= n) {
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else 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++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
//整组中的所有小组都归并完了,一起拷贝回去
memcpy(a, tmp, sizeof(int) \* n);
rangeN \*= 2;
}
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
/*计数排序*/
void CountSort(int* a, int n)
{
//1.找出数组中的最大值和最小值,然后开辟统计数组
int min = a[0];
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
) * (end - begin + 1));
}
/*归并排序*/
//时间复杂度:O(NlogN)
//空间复杂度:O(N)
void MergeSort(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror(“fail malloc”);
exit(-1);
}
\_MergeSort(a, 0, n - 1, tmp);
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
/*归并排序——非递归*/
void MergeSortNonR1(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror(“fail malloc”);
exit(-1);
}
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 \* rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 \* rangeN - 1;
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
/\*
* 处理越界的情况
*/
if (end1 >= n) {
break;
}
else if (begin2 >= n) {
break;
}
else if (end2 >= n) {
end2 = n - 1; //end2越界要修正一下,不可以break,因为最后需要求整个数组的长度
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//归并完一小组,拷贝一小组
memcpy(a + i, tmp + i, sizeof(int) \* (end2 - i + 1));
// + i表示每次归并的组在发生变化 //因为end是最后落的位置,i是初始化位置,不会改变
}
rangeN \*= 2;
}
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
void MergeSortNonR2(int* a, int n)
{
//开辟空间,存放归并后的数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror(“fail malloc”);
exit(-1);
}
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 \* rangeN)
{
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 \* rangeN - 1;
int j = i; //表示tmp每次都从上一次归并完放置后的地方开始
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
/\*
* 处理越界的情况
*/
if (end1 >= n) {
end1 = n - 1;
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else if (begin2 >= n) {
begin2 = n; //begin2要比end2来的大,才不构成区间
end2 = n - 1;
}
else 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++];
}
}
//若是还有区间存在数据,表示没有归并完全,直接放入tmp即可
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
//整组中的所有小组都归并完了,一起拷贝回去
memcpy(a, tmp, sizeof(int) \* n);
rangeN \*= 2;
}
free(tmp); //防止内存泄漏
tmp = NULL; //防止野指针
}
/*计数排序*/
void CountSort(int* a, int n)
{
//1.找出数组中的最大值和最小值,然后开辟统计数组
int min = a[0];
[外链图片转存中…(img-ZHSbWFOp-1714330333132)]
[外链图片转存中…(img-aUOcOeM7-1714330333133)]
[外链图片转存中…(img-HKSnJKBv-1714330333133)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新