🤡博客主页:醉竺
🥰本文专栏:《数据结构与算法》
😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!
✨✨💜💛想要学习更多数据结构与算法点击专栏链接查看💛💜✨✨
目录
前面我们学习了插入类排序中的直接插入排序和希尔排序,这次,我们学习另一类排序—— 交换排序。 《插入排序算法:直接插入排序和希尔排序》。
所谓交换类排序,就是根据序列中两个关键字的比较结果,来决定是否要交换这两个关键字对应的记录在序列中的位置。交换类排序主要包括冒泡排序和快速排序。接下来我们先讲冒泡排序再讲快速排序。
1. 冒泡排序
冒泡排序思想
按照从小到大排序:
在有n个元素的序列中,先将第一个记录的关键字和第二个记录的关键字进行比较,也就是两两比较相邻关键字,如果第一个关键字大于第二个关键字,则交换这两个数据。
接着,比较第二个关键字和第三个关键字,如果第二个关键字大于第三个关键字,则交换这两个数据。依次类推,直到 第n-1 个数据和 第n 个数据比较完为止。
- n 个数据只需要进行 n - 1 趟排序即可。
- 每趟排序结束后,就会有一个“最大值”在最终的位置,因此下一趟排序,数据之间就可以少比较一次 。
冒泡排序过程
此排序之所以称为冒泡排序,是因为大的数据往最下面(后面)沉,小的数据自然就会向上冒,这个过程就好像气泡在水中上浮的过程,所以叫做冒泡排序。
下面是冒泡排序动态图:
下面举一个具体的例子,来体验一下冒泡排序的思想。
对于数组 {16,1,45,23,99,2,18,67,42,10},第一趟冒泡排序后,结果为 {1,16,23,45,2,18,67,42,10,99}。如图 1 所示:
接着要开始第二趟冒泡排序了。但因为最后一个记录已经是最大值,因此第二趟冒泡排序只需要两两比较前n-1个元素,这样就会把第二大的记录放到倒数第二个记录位置上。显然,接着第一趟冒泡排序的结果,第二趟冒泡排序的结果为{1,16,23,2,18,45,42,10,67,99}。如图2所示:
接着开始第三趟、第四趟……冒泡排序,当 在一趟排序中发现没有任何一对数据进行过交换的操作时,说明此时所有元素都已经按照从小到大的顺序排好了,此时冒泡排序结束(如果趟数还没有走完也可以退出了循环了)。
冒泡排序代码
冒泡排序的代码编写有很多种方法,比如有的方法会从序列中的最后两条记录开始比较,把关键字最小的记录不断向最前面移动。这里我选择的编码方式还是按照上述描述的冒泡排序基本思想来实现。
外层循环条件:数组有length个数据,只需要进行 length - 1 趟排序即可。
内层循环条件:每趟排序结束后,就会有一个“最大值”在最终的位置,因此下一趟排序,数据之间就可以少比较一次 。每趟排序两两比较次数:n - 1 - i (i 是趟数)
冒泡排序结束条件:在一趟排序中没有两两元素进行交换的操作。
//冒泡排序(从小到大)
template<typename T>
void BubbleSort(T myarray[], int length)
{
if (length <= 1) //不超过1个元素的数组,没必要排序
return;
//外层循环只控制排序的趟数
for (int i = 0; i < length - 1; ++i)
{
bool cgflag=false;//表本趟冒泡排序是否发生过记录交换,false:无;true:有
//内层循环控制元素的大小比较和交换位置
for (int j = 0; j < length - i - 1; ++j) //每趟比较的次数都会减少
{
if (myarray[j] > myarray[j + 1]) //前面的数据如果比后面的数据大
{
//交换元素位置
T temp = myarray[j + 1];
myarray[j + 1] = myarray[j];
myarray[j] = temp;
cgflag = true; //标记本趟冒泡排序发生过记录交换(可能1次或者多次)
}
}
if (cgflag == false) //本趟冒泡排序没有发生过记录交换,表示整个冒泡排序结束
break;
}
}
冒泡排序性能分析
- 从代码中可以看到,冒泡排序实现代码比较简单。空间复杂度为O(1)。
- 在时间复杂度方面,对于具有n个元素的数组,在最好的情况下,即数组中元素已经是排好序的情况下,则只需要一趟排序并且这趟排序只需要进行n-1次比较次数且不需要做任何数据交换,所以最好情况时间复杂度为O(n)。
- 在最坏情况下,即数组中元素正好是逆序排列的情况下,此时需要进行n-1趟排序,比较次数和记录交换次数都是1+2+3+……+(n-1)=
次,即最坏情况时间复杂度为O(
)。
- 平均情况时间复杂度的分析要结合一些概率论知识,这里就不详细说明,结论也是O(
)。
- 此外,从实现代码中不难看到,只有 myarray[j] > myarray[j + 1] 时才会交换元素位置。即遇到了关键字相同的两条记录,这两条记录的相对顺序也不会发生改变,所以此排序算法是 稳定 的。
总结:冒泡排序会进行多趟排序,每趟排序都会把当前参与排序的数字中的最大数字下沉到最后,当然,不能影响已经在最后面的排好序的数字。
2. 快速排序
快速排序思想
上面我们一起学习了交换类排序中的冒泡排序,这次我们继续学习交换类排序中的快速排序。
这两种排序算法的主要区别在于排序的效率和实现代码。如果说冒泡排序是通过相邻元素的比较和交换达成排序,那么快速排序就是一种分而治之的思想,是对冒泡排序的改进。
快速排序的英文名称是Quick Sort,他通过分而治之的思想,把待排序的表分隔成若干个子表,每个子表都以一个称为枢轴的元素为基准进行排序。
一般来说,在元素数量一定的内部排序算法中,快速排序算法平均性能是最优秀的,因此,C++标准库中也提供了qsort函数来实现快速排序功能(其实qsort的实现版本中,还可能会用到其他排序)。
快速排序是Hoare于1962年提出的一种交换排序方法,其基本思想为:
- 任取待排序元素序列中的某元素作为枢轴(或基准,通常取首元素),通过一趟排序将待排序集合划分为独立的左右两子序列。
- 左子序列中所有元素均小于该基准值,右子序列中所有元素均大于基准值。
- 然后对左右子序列分别重复该过程,直到所有元素都排列在相应位置上为止。
通过 一趟排序 将所有关键字小于枢轴的元素都放置在枢轴前面,大于枢轴的元素都放置在枢轴后面。这样,这趟排序就将待排元素 分割 成了两个独立的部分。而且这个时候,枢轴元素所在的位置其实也就是该元素 最终 应该在的位置了。
用集合语言表示就是:
快速排序——递归实现
根据快速排序的递归思想,先对序列进行分区排序,然后依次对各区域分而治之进行排序 ,而分区又有以下3种实现方法!
将区间按照基准值划分为左右两半部分进行分区的常见方式有:
-
Hoare 分区法
- 选择一个基准元素(通常是数组的第一个元素),将其值保存在key中。使用两个指针left和right分别指向数组的左右边界。
- 从右边开始,将right指针向左移动,直到找到第一个小于基准元素的值;从左边开始,将left指针向右移动,直到找到第一个大于基准元素的值;然后交换 left 和 right 指针指向的值。
- 将left指针继续右移,right指针继续左移。直到left 指针大于等于right指针。
- 将基准元素的值放入left指针所指的位置,此时left指针(以及right指针)指向的位置就是分区的分界点。
先写好“交换”代码,后面不再重复
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
Hoare法分区代码
// 1.Hoare分区方法(HoarePartition):
int HoarePartition(int* myarray, int left, int right)
{
int keyi = left;
while (left < right)
{
// 右指针从右往左找比key小的值
while (left < right && myarray[right] >= myarray[keyi])
{
--right;
}
// 左指针从左往右找比key大的值
while (left < right && myarray[left] <= myarray[keyi])
{
++left;
}
Swap(&myarray[left], &myarray[right]);
}
Swap(&myarray[keyi], &myarray[left]);
return left;
}
基于Hoare法分区后进行快速排序的代码
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int myarray[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = HoarePartition(myarray, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(myarray, left, div);
// 递归排[div+1, right)
QuickSort(myarray, div+1, right);
}
-
Lomuto 分区法
- 选择一个基准元素(通常是数组的第一个元素),将其值保存在key中。使用两个指针 left 和 right 分别指向数组的左右边界。
- 从右边开始,找到第一个小于基准元素的值,将右指针right向左移动。将找到的小于基准元素的值赋给基准元素所在的空位(hole),并将hole更新为右指针right。
- 从左边开始,找到第一个大于基准元素的值,将左指针left向右移动。将找到的大于基准元素的值赋给基准元素所在的空位(hole),并将hole更新为左指针left。
- 继续执行上述步骤,直到左指针left大于等于右指针right。最后将基准元素的值放入最后一个空位hole中。返回hole作为分区的分界点。
Lomuto分区方法代码
// 2.Lomuto分区方法(LomutoPartition)
int LomutoPartition(int* myarray, int left, int right)
{
int key = myarray[left];
int hole = left;
while (left < right)
{
while (left < right && myarray[right] >= key)
{
--right;
}
myarray[hole] = myarray[right];
hole = right;
while (left < right && myarray[left] <= key)
{
++left;
}
myarray[hole] = myarray[left];
hole = left;
}
myarray[hole] = key;
return hole;
}
Lomuto分区方法代码优化
其实完全不需要坑位, 上述过程进行如下修改:
- 选择一个基准元素(通常是数组的第一个元素),将其值保存在key中。使用两个指针left 和 right 分别指向数组的左右边界。
- 从右边开始,找到第一个小于基准元素的值,将右指针 right 向左移动。将找到的小于基准元素的值赋给左指针 left 指向的位置。
- 从左边开始,找到第一个大于基准元素的值,将左指针 left 向右移动。将找到的大于基准元素的值赋给右指针 right 指向的位置。
- 继续执行上述步骤,直到左指针 left 大于等于右指针 right。最后将基准元素的值 key 放入两指针共同指向的位置。返回 left 或者right 都可。
// 2.Lomuto分区方法优化(LomutoPartition):
int LomutoPartition(int* myarray, int left, int right)
{
int key = myarray[left];
while (left < right)
{
while (left < right && myarray[right] >= key)
{
--right;
}
myarray[left] = myarray[right];
while (left < right && myarray[left] <= key)
{
++left;
}
myarray[right] = myarray[left];
}
myarray[left] = key;
return left;
}
基于Lomuto法分区后进行快速排序的代码
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int myarray[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = LomutoPartition(myarray, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(myarray, left, div);
// 递归排[div+1, right)
QuickSort(myarray, div+1, right);
}
-
DoublePointer 分区法
- 选择一个基准元素(通常是数组的第一个元素),将其值保存在key中。初始时prev 指针指向序列开头, cur 指针指向prev 指针的后一个位置。
- 若cur 指向的数据大于 key,则cur ++;若cur 指向的数据小于 key,则prev 先后移一位++,然后与 cur 指针指向的数据交换,最后cur 再++.
- 重复上述过程,直到 cur指针为空,最后将prev 指向的内容与一开始保存的key 值进行交换。
- 结束,此时key 左边的数据都比 key 小,key 右边的数据都比key 大,分区完毕。
上述过程类似于:
1.最开始时 prev与cur相邻
2.当 cur 遇到比 key 大的值后,cur 向后移动,这时它们之间的值都是比 key大的值
3.相当于把 prev 与 cur 之间比 key 大的值翻滚式的往右边推,同时把小的换到左边、
// 3.前后指针法(DoublePointerPartition):
int DoublePointerPartition(int* myarray, int left, int right)
{
int prev = left;
int cur = left + 1;
int key = myarray[left];
while (cur <= right)
{
if (myarray[cur] < key && ++prev != cur)
{
Swap(&myarray[prev], &myarray[cur]);
}
++cur;
}
Swap(&myarray[prev], &key);
return prev;
}
基于DoublePointer法分区后进行快速排序的代码
void QuickSort(int* myarray, int begin, int end)
{
// 在快速排序算法中,当需要排序的子数组的长度为0或1时,已经是有序的,不需要再进行排序。
// 因此,当begin大于等于end时,即子数组的长度为0或1,就可以终止递归,直接返回。
if (begin >= end)
return;
int keyi = DoublePointerPartion(myarray, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(myarray, begin, keyi - 1);
QuickSort(myarray, keyi + 1, end);
}
快速排序算法性能分析
根据快速排序 QuickSort 的代码我们可知,其实现逻辑与二叉树前序遍历规则非常像,所以快速排序的性能与二叉树的性质有密切关系。
从代码和显示的结果可以看到,Partition是核心的分割函数, 这里以数组{16,1,45,23,99,2,18,67,42,10} 为例进行排序,来看看是怎么样的排序过程呢?
用hoare法分区,以每一部分的首元素作为基准元素来演示:
第一次调用Partition分割,元素16将整个数组分割成了两块,第一块包括元素10、1、2,第二块包括元素99、23、18、67、42、45。
第二次调用Partition分割(第几次调用该函数如图中圆形编号所示),元素10将数组(这里的数组当然是上面已经分割开的子数组)分割出了第三块,第三块包含元素2、1。
第三次调用Partition分割,元素2将数组分割出了第四块,第四块包含元素1。
第四次调用Partition分割,元素99将数组分割出了第五块,第五块包含元素45、23、18、67、42。
第五次调用Partition分割,元素45将数组分割出了第六块和第七块,第六块包含元素42、23、18,第七块包含元素67。
第六次调动Partition分割,元素42将数组分割出了第八块,第八块包含元素18、23。
第七次调用Partition分割,元素18将数组分割出了第九块,第九块包含元素23。
至此,调用了七次Partition进行分割后,整个快速排序执行完毕。
第一次Partition调用会将数组中的n个元素,也就是从low到high之间的数据全部扫描一次,时间复杂度为O(n)。第二次、第三次……,调用Partition函数所需要扫描的数据会越来越少,都会<n,所以每次调用Partition的时间复杂度都不会超过O(n)。
其实,通过统计Partition被调用的次数来求解快速排序算法的时间复杂度,与通过统计QuickSort递归函数的调用深度来求解快速排序算法的时间复杂度是一回事。
所以,整个快速排序算法的时间复杂度为O(n*递归调用深度),这意味着快速排序算法的时间复杂度可以看成是和递归层数紧密相关。当然,快速排序算法的空间复杂度也和递归层数相关,因为每次递归都会用到栈空间来暂存很多信息。所以,快速排序算法的空间复杂度为O(递归调用深度)。
因为每次递归调用QuickSort都会把当前需要处理的区间再次划分成左右两个子区间。所以图2换一种绘制方式其实可以变成一棵二叉树,如图3所示:
图3中,快速排序把数组中的n个元素组织成了一棵二叉树,二叉树的层数也就代表着递归调用的深度。所以 快速排序算法的递归调用深度问题就可以转换为对二叉树高度范围的判断。
(下面快速排序的效率分析是优化前,默认让每一个区域首元素当作基准元素的情况)
本专栏二叉树文章中曾经讲过,对于有n个节点的二叉树,它的最小高度是⌊⌋ +1,最大高度是n(斜树)。所以,对于快速排序算法,最少的递归深度(递归层数)应该是⌊
⌋ +1,而最大的递归深度应该是n,才可以完成整个排序过程。
所以,根据前面所说——整个快速排序算法的时间复杂度为O(n*递归调用深度)。不难看到,快速排序算法最好情况时间复杂度为O(),最坏情况时间复杂度为O(
),平均情况时间复杂度为O(
)。而因为快速排序算法的空间复杂度为O(递归调用深度),所以快速排序算法最好情况空间复杂度为O(
),最坏情况空间复杂度为O(
),平均情况空间复杂度为O(
)。
设想一下,如果每一趟快速排序选中的枢轴都能够将数组元素均匀的划分为两个部分,那么调用QuickSort递归的深度就会最小,算法效率就会达到最高。 但如果数组元素原本就是有序(顺序逆序都可以)的,比如数组元素是int arr[] = { 1,2,3,4,5,6,7,8,9,10 };,那么此时枢轴就完全无法将数组元素做均匀划分,此时递归调用的深度将达到9层,此时的算法效率会达到最低。
换句话说,如果给定的数组原本就是有序的(顺序或者逆序),此时快速排序算法的性能最差。这也称为快速排序算法的退化,退化成了冒泡排序算法。
总结:
- 快速排序最好的情况下,时间复杂度为O(
)。这是因为快速排序采用分治的思想,将数组分成较小和较大的两部分,并对两部分分别进行排序,最终得到有序的数组。
- 平均情况时间复杂度为 O(
)
- 快速排序最坏情况下,时间复杂度为O(
)。这种情况发生在待排序的数组已经是有序或基本有序的情况下。在这种情况下,快速排序的划分过程可能会将数组分成极度不平衡的两部分,导致递归深度增加,从而导致性能下降。
- 快速排序的效果受到输入数据的分布情况的影响。当输入数据近似随机分布时,快速排序的效果通常最好。而当输入数据具有一定的有序性或逆序性时,快速排序的效果可能最差。
快速排序分区枢轴优化
在上述 Hoare分区法, Lamuto分区法和DoublePointer分区法的代码中,我们选取的枢轴元素都是数组中的第一个元素。
由快速排序性能分析的结论可知,快速排序中基准元素的选取非常重要,影响着快速排序的效率,所以需要对其进行优化!
为了尽可能提高快速排序的效果,可以采用以下几种方法选择基准元素:
- 随机选择:随机选择数组中的一个元素作为划分元素。这种方法可以减少最坏情况发生的概率,并且在平均情况下能够获得较好的效果。
- 中位数选择:选择待排序数组的中位数作为划分元素。这种方法可以尽可能地将数组均分为两部分,避免极度不平衡的划分。
- 三数取中:选择待排序数组的头部、中间和尾部的三个元素,取它们的中位数作为划分元素。这种方法是一种2折中的选择,既考虑了性能,又避免了极端情况的发生。
我们选择三数取中这种方法:选取 a[left], a[mid] 和 a[right] 3个数字中的中位数。这个方案能够在很大程度上改善快速排序算法在最坏情况下的性能。
注意!mid只是索引的中位数,不代表a[mid]三个数中的中位数,所以需要判断大小,下面是选取中位数的实现代码:
// 快速排序分区枢轴优化 (三数取中)
int GetMidIndex(int* myarray, int left, int right)
{
int mid = left + (right - left) / 2;
// 数组的头部是中位数
if ((myarray[mid] < myarray[left] && myarray[left] < myarray[right])
|| (myarray[right] < myarray[left] && myarray[left] < myarray[mid]))
{
return left;
}
// 数组的中部是中位数
else if ((myarray[left] < myarray[mid] && myarray[mid] < myarray[right])
|| (myarray[right] < myarray[mid] && myarray[mid] < myarray[left]))
{
return mid;
}
// 数组的尾部是中位数
else
{
return right;
}
}
在上面 Hoare分区法, Lamuto分区法和DoublePointer分区分代码中的开头添加下面两行代码即可:
int mididx = GetMidIndex(myarray, left, right);
Swap(&myarray[left], &myarray[mididx]);
快速排序之——非递归实现
这里可以参考本专栏的有关栈实现的文章《深入理解栈》 ,当然下面这个代码是一种方法,高级语言C++,Java等都有官方库已经实现的栈,直接用即可。
- 创建一个栈,用于保存待处理的子数组的起始和结束位置。
- 将整个数组的起始和结束位置压入栈。
- 进入循环,直到栈为空。
- 在每次循环中,从栈中弹出一个子数组的起始和结束位置。
- 使用划分函数将该子数组分为两部分,并得到分割元素的索引。
- 如果分割元素右边的子数组长度大于1,则将右半部分的起始和结束位置压入栈。
- 如果分割元素左边的子数组长度大于1,则将左半部分的起始和结束位置压入栈。
- 循环继续,直到栈为空。
- 所有子数组都被处理完毕,排序完成。
使用栈来实现快速排序的非递归版本的主要思想是模拟递归过程。通过将待处理的子数组的起始和结束位置保存在栈中,可以避免使用递归时的系统栈溢出问题。每次从栈中弹出一个子数组并进行划分,然后根据划分的结果将子数组的起始和结束位置压入栈。该过程会持续进行,直到所有的子数组都被处理完毕,排序完成。
// 快速排序的非递归方式
void QuickSortNonR(int* myarray, int begin, int end)
{
Stack st; // 创建一个栈用于保存待处理的子数组的起始和结束位置
StackInit(&st); // 初始化栈
StackPush(&st, end); // 将结束位置压入栈
StackPush(&st, begin); // 将起始位置压入栈
while (!StackEmpty(&st)) // 当栈不为空时进行循环
{
int left = StackTop(&st); // 弹出栈顶元素作为子数组的起始位置
StackPop(&st);
int right = StackTop(&st); // 弹出栈顶元素作为子数组的结束位置
StackPop(&st);
int keyi = Partition1(myarray, left, right); // 使用划分函数将子数组划分为两部分,并返回分割元素的索引
if (keyi + 1 < right) // 如果分割元素右边的子数组长度大于1,则将右半部分的起始和结束位置压入栈
{
StackPush(&st, right);
StackPush(&st, keyi + 1);
}
if (left < keyi - 1) // 如果分割元素左边的子数组长度大于1,则将左半部分的起始和结束位置压入栈
{
StackPush(&st, keyi - 1);
StackPush(&st, left);
}
}
StackDestroy(&st); // 销毁栈
}
快速排序之——三路归并实现
快速排序的三路归并版本是一种对具有大量重复元素的数组进行排序的改进版本。它通过将数组分为小于、等于和大于分割元素的三部分来实现排序。下面是该算法的实现逻辑:
- 选择一个分割元素,可以是数组中的任意一个元素。通常为了避免最坏情况的发生,我们会选择随机的分割元素。
- 定义三个指针,分别表示小于分割元素的部分、等于分割元素的部分和大于分割元素的部分的起始位置。初始时,这三个指针都指向数组的起始位置。
- 从数组的第二个元素开始遍历,直到遍历到数组的最后一个元素。
- 对于每个遍历到的元素,将其与分割元素进行比较:
- 如果该元素小于分割元素,将其与小于部分的指针指向的元素交换位置,并将小于部分的指针和等于部分的指针都向右移动。
- 如果该元素大于分割元素,将其与大于部分的指针指向的元素交换位置,并将大于部分的指针向左移动。
- 如果该元素等于分割元素,将等于部分的指针向右移动。
- 遍历结束后,整个数组被分为小于分割元素、等于分割元素和大于分割元素的三个部分。
- 递归地对小于和大于部分进行排序,即对小于部分进行快速排序和对大于部分进行快速排序。
- 递归结束后,数组就被排序好了。
通过使用三路归并进行快速排序,可以有效地提高对具有大量重复元素的数组进行排序的效率。它减少了重复比较的次数,将具有相同值的元素放在一起,提高了算法的性能。
通过使用三路归并进行快速排序,可以有效地提高对具有大量重复元素的数组进行排序的效率。它减少了重复比较的次数,将具有相同值的元素放在一起,提高了算法的性能。
void QuickSort3Ways(int* myarray, int begin, int end)
{
if (begin >= end) // 如果起始位置大于等于结束位置,则表示只有一个元素或者没有元素,已经有序,直接返回
{
return;
}
int left = begin; // 左指针初始位置为起始位置
int right = end; // 右指针初始位置为结束位置
int cur = left + 1; // 当前指针初始位置为左指针的下一个位置
int mididx = GetMidIndex(myarray, left, right); // 获取中间元素的索引
Swap(&myarray[left], &myarray[mididx]); // 将中间元素与起始元素交换位置,以确保选择的分割元素是随机的
int key = myarray[left]; // 分割元素为起始元素
while (cur <= right) // 当当前指针小于等于右指针时进行循环
{
if (myarray[cur] < key) // 如果当前元素小于分割元素
{
Swap(&myarray[left], &myarray[cur]); // 将当前元素与左指针指向的元素交换位置
++left; // 左指针向右移动
++cur; // 当前指针向右移动
}
else if (myarray[cur] > key) // 如果当前元素大于分割元素
{
Swap(&myarray[right], &myarray[cur]); // 将当前元素与右指针指向的元素交换位置
--right; // 右指针向左移动
}
else // 如果当前元素等于分割元素
{
++cur; // 当前指针向右移动
}
}
// 将数组分为小于分割元素、等于分割元素和大于分割元素的三部分
// 小 l r 大
// [begin, left-1] [left, right] [right+1, end]
QuickSort3Ways(myarray, begin, left - 1); // 对小于分割元素的部分进行递归排序
QuickSort3Ways(myarray, right + 1, end); // 对大于分割元素的部分进行递归排序
}
myarray[c] < key :交换c和l位置的值,++l,++c
myarray[c] > key :交换c和r位置的值,--r
myarray[c] == key :++c
三路划分的本质:
1.小的甩到左边,大的甩到右边
2.跟key相等的值推到中间