快速排序 2019年8月23日17:45:21
之前我们已经分析过 冒泡排序,我建议:当你在看这篇博客的时候,请先回顾 冒泡排序:详见DSA之十大排序算法第一种:Bubble sort
文章目录
冒泡排序的时间复杂度在最差的情况下:达到了平方级。这当然不符合我们的要求,今天主要详解 最优秀的内部排序中的 Quick Sort。虽然我经常用,说实话 我是怎么用的呢? sort方法!!! C++中的sort 泛型算法就是基于快排实现(这样说 不是很严谨! 因为STL中的sort并非只是普通的快速排序,除了对普通的快速排序进行优化,它还结合了插入排序和堆排序。根据不同的数量级别以及不同情况,能自动选用合适的排序方法。当数据量较大时采用快速排序,分段递归。一旦分段后的数据量小于某个阀值,为避免递归调用带来过大的额外负荷,便会改用插入排序。而如果递归层次过深,有出现最坏情况的倾向,还会改用堆排序。快速排序最关键的地方在于枢轴的选择,最坏的情况发生在分割时产生了一个空的区间,这样就完全没有达到分割的效果。STL采用的做法称为median-of-three,即取整个序列的首、尾、中央三个地方的元素,以其中值作为枢轴。 这个以后再详谈 STL中的sort实现)
说起快速排序:人家这个名字起的就很简单粗暴,因为一听到这个名字,我们就知道它存在的意义:快,而且效率高!快速排序是我们处理大量数据 最快的排序算法之一了。虽然 在最差情况下的时间复杂度也达到了 O(n²),但是在大多数情况下作为一个时间复杂度为O(n logn)的算法竟然都比很多平均时间复杂度为 O(n logn) 的排序算法表现要更好。原因就在于:快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
好吧 路子走偏了,咱赶紧回来。快速排序使用Divide and conquer的策略来把一个数列分为两个子数列,因此快速排序也是一种分而治之思想在排序算法上的典型应用。但从本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法,因此也可以称为是冒泡的一种优化算是。
具体做法就是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以通过递归进行,以此达到整个数据变成有序序列,当然在这里我也提供一下非递归的方式实现版本。
排序步骤就是:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,把元素值比基准值小的放在基准前面,元素值比基准值大的放在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就数处于这个数列的中间位置。这个过程被称为分区(partition)操作,也有人把它称为:一次划分算法。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。(当然这里也提供一下 非递归版本。)
一次划分的三种实现方式:
对于分区操作(一次划分)而言,通常有3种实现方式:挖坑填补法、左右交换法、双指针前移法。
分析具体实现:挖坑填补法 |
第一种 挖坑填补法:
基本思路就是:
- 设置两个变量l和h,分别接收传入的一次划分one_Partition的low和high。在最开始的时候 low=0 high=size-1。
- 然后我们这里把 第一个元素src【0】作为 基准值pivotkey的开始值。注:当以数组第一个数值为pivotkey时,搜索必须先从后向前进行,当以最后一个数组元素为基准数时,搜索必须先从前向后进行。
- 首先 h从后向前 扫描,一直到 遇见第一个小于pivotkey的值 src【h】,将此时的src【l】和src【h】的值进行交换。这个过程即:h - -
- 接着l从前向后 扫描,一直到 遇见第一个大于pivotkey的值 src【l】,将此时的src【l】和src【h】的值进行交换。这个过程即:l + +
- 继续循环上面两个步骤,一直到 l=h的时候,循环即可结束。这个时候(把pivotkey 填入src【l】或者src【h】当中)。
- 然后把此时的 l 和 h 返回回去,下面接着去递归这个下标左面的部分和右面的部分。
一次划分one_Partition的具体操作如下:
此时的 pivotkey 49的左边数据都严格比它小,右边的数据不小于它。于是就可以使用递归的方式,依次处理 左边的{27,38,13} 和 右边的{76,97,65,49};如下所示:
算法测试打印:挖坑填补法 |
这里使用递归实现挖坑填补法的快排:
#include <iostream>
#include <vector>
#include <iomanip>
#include <algorithm>
using namespace std;
int one_Partition(vector<int>& src, int low, int high)
{
static int n = 1;//记录次数的
int l = low, h = high;
int pivotkey = src[l];//把这段数据的第一个数据 作为基准值
while (l < h)
{
while (l < h && src[h]>= pivotkey)
{
h--;
}
//把pivotkey填到src【h】里面,把src【l】这个坑挖好了
swap(src[l], src[h]);
while (l < h && src[l] <= pivotkey)
{
l++;
}
//把pivotkey填到src【l】里面,把src【h】这个坑挖好了
swap(src[l], src[h]);
}
//src[l] = pivotkey;//这个是把pivotkey 放到最后一个位置
//但是我感觉上面的这句代码 也可以不用写。因为我做的是 交换数据,本来就在那里了
cout << "第" << n++ << "次调整结束后,结果:";
for (int val : src)
{
cout << setw(2) << val << " ";
}
cout << endl;
return l;//把这个下标 返回,作为下一次递归的base中间线
}
void Quick_sort(vector<int>& src,int low,int high)
{
int base = one_Partition(src, low, high);
if (low + 1 < base)
Quick_sort(src, low, base - 1);//递归左边
if (base + 1 < high)
Quick_sort(src, base + 1, high);//递归右边
}
int main()
{
int Array[] = { 1,50,38,5,47,15,36,26,27,2,46,4,19,44,48 };
int Array2[] = { 49,38,65,97,76,13,27,49 };
#if 0
vector<int>myvec(begin(Array), end(Array));
Quick_sort(myvec, 0, myvec.size() - 1);
cout << "排序Quick_sort结束后,最终结果:";
for (int val : myvec)
{
cout << setw(2) << val << " ";
}
cout << endl;
cout << "**********************************" << endl;
#endif
vector<int>myvec2(begin(Array2), end(Array2));
Quick_sort(myvec2, 0, myvec2.size() - 1);
cout << "排序Quick_sort结束后,最终结果:";
for (int val : myvec2)
{
cout << setw(2) << val << " ";
}
cout << endl;
return 0;
}
以上 红色方框里面就是pivotkey值的调整。
对于分区操作(一次划分)而言,通常有3种实现方式:挖坑填补法、左右交换法、双指针前移法。
分析具体实现:左右交换法 |
第二种 左右交换法:
基本思路就是:
- 设置两个变量l和h,分别接收传入的一次划分one_Partition的low和high。在最开始的时候 low=0 high=size-1。
- 然后我们这里把 第一个元素src【0】作为 基准值pivotkey的开始值。(这个时候 也需要把这个基准值的下标保存下来val_key 值为 l )注:当以数组第一个数值为pivotkey时,搜索必须先从后向前进行,当以最后一个数组元素为基准数时,搜索必须先从前向后进行。
- 首先 h从后向前 扫描,一直到 遇见第一个小于pivotkey的值 src【h】,然后停止。这个过程即:h - -
- 接着l从前向后 扫描,一直到 遇见第一个大于pivotkey的值 src【l】,然后停止。这个过程即:l + +
- 接下来:把pivotkey值 两边的数据进行 swap。
- 继续循环上面三个步骤,一直到 l=h的时候,循环即可结束。这个时候(最后把pivotkey值 和中间集合下标 l或者h 数据 进行交换)。(这个地方就用到了上面刚刚保存的 基准值的下标val_key )
- 然后把此时的 l 和 h 返回回去,下面接着去递归这个下标左面的部分和右面的部分。
算法测试打印:左右交换法 |
#include <iostream>
#include <vector>
#include <iomanip>
#include <algorithm>
using namespace std;
int one_Partition(vector<int>& src, int low, int high)
{
int l = low, h = high;
int pivotkey = src[l];//把这段数据的第一个数据 作为基准值
int val_key = l;
while (l < h)
{
//这种情况下 必须先得从最右边开始
while (l < h && src[h] >= pivotkey)
{
h--;
}
//在这里 就找到了 自最右边开始的 第一个小于pivotkey值的 数据
//然后接下来 就在给定数列 最左边开始找带一个大于 pivotkey值的 数据
while (l < h && src[l] <= pivotkey)
{
l++;
}
swap(src[l], src[h]);//把pivotkey值 两边的数据进行 swap
}
//最后把pivotkey值 和中间集合下标数据 进行交换
swap(src[l], src[val_key]);
return l;//把这个下标 返回,作为下一次递归的base中间线
}
void Quick_sort(vector<int>& src, int low, int high)
{
int base = one_Partition(src, low, high);//左右交换法 实现
if (low + 1 < base)
Quick_sort(src, low, base - 1);//递归左边
if (base + 1 < high)
Quick_sort(src, base + 1, high);//递归右边
}
int main()
{
int Array[] = { 1,50,38,5,47,15,36,26,27,2,46,4,19,44,48 };
int Array2[] = { 49,38,65,97,76,13,27,49 };
vector<int>myvec(begin(Array), end(Array));
Quick_sort(myvec, 0, myvec.size() - 1);
cout << "排序Quick_sort结束后,最终结果:";
for (int val : myvec)
{
cout << setw(2) << val << " ";
}
cout << endl;
cout << "**********************************" << endl;
vector<int>myvec2(begin(Array2), end(Array2));
Quick_sort(myvec2, 0, myvec2.size() - 1);
cout << "排序Quick_sort结束后,最终结果:";
for (int val : myvec2)
{
cout << setw(2) << val << " ";
}
cout << endl;
return 0;
}
第三种 双指针前移法:
基本思路就是:
- 设置两个前后指针 slow和fast,分别在最开始的时候 fast=high - 1,而slow=fast - 1.slow被称为是 慢指针;fast被称为是快指针。整个过程由fast指针进行移动,搜寻。注:快指针去搜索比基准值pivotkey 小的元素,直到fast=high处 结束。当前外层循环 控制:while (fast < high)
- 然后我们这里把 最后一个元素src【high】作为 基准值pivotkey的开始值。
- 首先 fast指针从前向后 扫描,一直到 遇见第一个小于pivotkey的值 src【fast】,然后停止。这个过程即:while (src[fast] < pivotkey && (++slow) != fast) 找到第一个小于pivotkey的值的 src【fast】。
- 找到之后,若是slow的下一个位置 不是fast的情况下:swap(src[slow], src[fast]);
- 接下来:fast继续向后搜索。
- 继续循环上面三个步骤,一直到 fast=high的时候,循环即可结束。这个时候(此时的fast到达了high,这一轮的一次划分到此 循环结束)。接下来 把基准值和 slow位置的下一个元素值进行 交换。此时那个最初设置的基准值pivotkey就到达了其位置。(此时其左边数据 严格比它小,右边数据严格 大于等于它)。
- 然后此时,把这个slow的下一个位置的下标值 返回,作为下一次递归的base中间线,下面接着去递归这个下标(基准值pivotkey)左面的部分和右面的部分。
算法测试打印:双指针前移法 |
#include <iostream>
#include <vector>
#include <iomanip>
#include <string>
#include <algorithm>
using namespace std;
static int count_k = 1;//作为一次划分的次数
int one_Partition(vector<int>& src, int low, int high)// 前后指针 前移法
{
int fast = low;
int slow = fast - 1;
int pivotkey = src[high];//这次把最右边的数据 作为基准数
while (fast < high)
{
while (src[fast] < pivotkey && (++slow) != fast)
{
swap(src[slow], src[fast]);
}
++fast;
}
//此时的fast到达了high
//接下来 把基准值和 slow位置的下一个值进行 交换
swap(src[slow + 1], src[fast]);
//打印一下每一次的 一次划分结束时的结果:
cout << "第" << count_k++ << "次的一次划分 结束结果为:";
for (int val : src)
cout << val << " ";
cout << endl;
//把这个slow的下一个位置的下标 返回,作为下一次递归的base中间线
return slow + 1;
}
void Quick_sort(vector<int>& src, int low, int high)
{
int base = one_Partition(src, low, high);//双指针前移法 实现
if (low + 1 < base)
Quick_sort(src, low, base - 1);//递归左边
if (base + 1 < high)
Quick_sort(src, base + 1, high);//递归右边
}
int main()
{
int Array[] = { 1,50,38,5,47,15,36,26,27,2,46,4,19,44,48 };
int Array2[] = { 49,38,65,97,76,13,27,49 };
#if 0
vector<int>myvec(begin(Array), end(Array));
Quick_sort(myvec, 0, myvec.size() - 1);
cout << "排序Quick_sort结束后,最终结果:";
for (int val : myvec)
{
cout << setw(2) << val << " ";
}
cout << endl;
cout << "**********************************" << endl;
#endif
vector<int>myvec2(begin(Array2), end(Array2));
Quick_sort(myvec2, 0, myvec2.size() - 1);
cout << "排序Quick_sort结束后,最终结果:";
for (int val : myvec2)
{
cout << setw(2) << val << " ";
}
cout << endl;
return 0;
}
到此时为止:一次划分的三种实现方式 都描述清楚了。主要的函数接口:void Quick_sort(vector& src, int low, int high)是一致的,仅仅是在底层 一次划分上进行了调整。而且主要的函数 使用递归的方式实现的快速排序算法。接下来 我们要讨论的就是 关于基准数pivotkey 的选取方式:
关于基准数pivotkey 的选取方式:
从网上找到的图片:
第一种 固定基准数的选取
如上的三种 一次划分算法实现上都是采取这种方式 选取基准数:
通常情况下的基本的快速排序选取第一个或最后一个元素作为基准。但是,这是一直很不好的处理方法。因为其基本思想就是:选取第一个或最后一个元素作为基准值,而这样做 不是比较好的处理。算法代码 上面就写过了,不再赘述了。
//这个是 上面挖坑填补法的实现 一个典型的固定位置 选取基准值的实现
int one_Partition(vector<int>& src, int low, int high)
{
int l = low, h = high;
int pivotkey = src[l];//把这段数据的第一个数据 作为基准值
while (l < h)
{
while (l < h && src[h]>= pivotkey)
{
h--;
}
//把pivotkey填到src【h】里面,把src【l】这个坑挖好了
swap(src[l], src[h]);
while (l < h && src[l] <= pivotkey)
{
l++;
}
//把pivotkey填到src【l】里面,把src【h】这个坑挖好了
swap(src[l], src[h]);
}
//src[l] = pivotkey;//这个是把pivotkey 放到最后一个位置
//但是我感觉上面的这句代码 也可以不用写。因为我做的是 交换数据,本来就在那里了
return l;//把这个下标 返回,作为下一次递归的base中间线
}
第二种 随机基准数的选取
其基本思想:选取待排序列【low,high】中共high-low+1 个数据中的任意一个数作为基准值,选出的任意值与low下标的元素交换,可继续使用上面已经实现过的 一次划分算法代码。这样 反其道而行之,同样到达了pivotkey值的随机性。
//这个是 上面挖坑填补法的实现 随机选取基准值的实现
int one_Partition(vector<int>& src, int low, int high)
{
swap(src[low],src[low + rand() % (high-low+1)]);//做到基准值随机的效果
int l = low, h = high;
int pivotkey = src[l];//把这段数据的第一个数据 作为基准值
while (l < h)
{
while (l < h && src[h]>= pivotkey)
{
h--;
}
//把pivotkey填到src【h】里面,把src【l】这个坑挖好了
swap(src[l], src[h]);
while (l < h && src[l] <= pivotkey)
{
l++;
}
//把pivotkey填到src【l】里面,把src【h】这个坑挖好了
swap(src[l], src[h]);
}
//src[l] = pivotkey;//这个是把pivotkey 放到最后一个位置
//但是我感觉上面的这句代码 也可以不用写。因为我做的是 交换数据,本来就在那里了
return l;//把这个下标 返回,作为下一次递归的base中间线
}
第三种 三数取中 基准数选取
因为虽然快速排序整体的效率可观,但是当最坏情况发生时它的效率就会降低,为了降低最坏情况发生的概率,我们要尽量在最坏情况下的选取合适的基准数。当我们每次划分的时候选择的基准数接近于整组数据的最大值或者最小值时,快速排序就会发生最坏的情况,但是每次选择的基准数都接近于最大数或者最小数的概率随着排序元素的增多就会越来越小,我们完全可以忽略这种情况。但是在数组有序的情况下,它也会发生最坏的情况,为了避免这种情况,我们在选择基准数的时候可以采用三数取中法来选择基准数。
- 首先:看一下 固定基准值的情况下的 交换情况:
static int k = 1;
int one_Partition(vector<int>& src, int low, int high)// 左右交换法
{
int l = low, h = high;
int pivotkey = src[low];//这次把最左边的数据 作为基准数
int val_key = l;
int count_k = 0;
while (l < h)
{
while (l<h && src[h] >= pivotkey)
{
h--;
}
//在这里 就找到了 自最右边开始的 第一个小于pivotkey值的 数据
//然后接下来 就在给定数列 最左边开始找带一个大于 pivotkey值的 数据
while (l < h && src[l] <= pivotkey)
{
l++;
}
count_k++;
swap(src[l], src[h]);
}
//最后把pivotkey值 和中间集合下标数据 进行交换
swap(src[l], src[val_key]);
count_k++;
//打印一下每一次的 一次划分结束时的结果:
cout << "第" << k++ << "次的一次划分,经过了" << count_k << "次的交换。 结束结果为:";
for (int val : src)
cout << val << " ";
cout << endl;
return l;//把这个下标 返回,作为下一次递归的base中间线
}
2. 接下来 看一下随机基准值的交换情况:
static int k = 1;
int one_Partition(vector<int>& src, int low, int high)// 左右交换法
{
srand(unsigned (time(nullptr)));
swap(src[low], src[low + rand() % (high - low + 1)]);//做到基准值随机的效果
int l = low, h = high;
int pivotkey = src[low];//这次把最左边的数据 作为基准数
int val_key = l;
int count_k = 0;
while (l < h)
{
while (l<h && src[h] >= pivotkey)
{
h--;
}
//在这里 就找到了 自最右边开始的 第一个小于pivotkey值的 数据
//然后接下来 就在给定数列 最左边开始找带一个大于 pivotkey值的 数据
while (l < h && src[l] <= pivotkey)
{
l++;
}
count_k++;
swap(src[l], src[h]);
}
//最后把pivotkey值 和中间集合下标数据 进行交换
swap(src[l], src[val_key]);
count_k++;
//打印一下每一次的 一次划分结束时的结果:
cout << "第" << k++ << "次的一次划分,经过了" << count_k << "次的交换。 结束结果为:";
for (int val : src)
cout << setw(2) << val << " ";
cout << endl;
return l;//把这个下标 返回,作为下一次递归的base中间线
}
所谓的三数取中法: 其基本思想就是选择这组数据的第一个元素、中间的元素、最后一个元素,这三个元素里面值大小 居中的那个元素作为基准数。即:调整成为src[mid] <= src[low] <= src[high],使得arr[low]处于三数中的中间值,这样的话,我们就可以去继续使用上面的一次划分函数。
static int k = 1;
// src[mid] <= src[low] <= src[high]
void resetlow_mid_high(vector<int>& src, int low, int high)//重置 三数取中
{
int mid = (low + high) >> 1;
if (src[mid] > src[low])
{
swap(src[mid], src[low]);
}
if (src[low] > src[high])
{
swap(src[low], src[high]);
}
if (src[mid] > src[high])
{
swap(src[mid], src[high]);
}
}
int one_Partition(vector<int>& src, int low, int high)// 左右交换法
{
resetlow_mid_high(src, low, high);//三数取中
int l = low, h = high;
int pivotkey = src[low];//这次把最左边的数据 作为基准数
int val_key = l;
int count_k = 0;
while (l < h)
{
while (l<h && src[h] >= pivotkey)
{
h--;
}
//在这里 就找到了 自最右边开始的 第一个小于pivotkey值的 数据
//然后接下来 就在给定数列 最左边开始找带一个大于 pivotkey值的 数据
while (l < h && src[l] <= pivotkey)
{
l++;
}
count_k++;
swap(src[l], src[h]);
}
//最后把pivotkey值 和中间集合下标数据 进行交换
swap(src[l], src[val_key]);
count_k++;
//打印一下每一次的 一次划分结束时的结果:
cout << "第" << k++ << "次的一次划分,经过了"
<< count_k << "次的交换。 结束结果为:";
for (int val : src)
cout << setw(2) << val << " ";
cout << endl;
return l;//把这个下标 返回,作为下一次递归的base中间线
}
快排算法的优化
第一种 当待子序列的长度 被划分到较小数值时,采用插入排序
对于待排序的序列长度很小或是基本趋于有序时,快排的效率还是没有插入排序的好。也即:当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插入排序。所有我们在这里自定义这个 子序列长度:序列长度N=5。当待排序的序列长度被分割到5时,采用插入排序 而非继续分割快速排序。
void Straight_Insertion_Sort2(vector<int>& src)//使用的是两两交换前进
{
int size = src.size();
for (int i = 0; i < size - 1; ++i)
{
int j = i + 1;//取出无序序列的第一个元素
int k = i;
while (k >= 0 && src[j] < src[k])//这个k的比较 必须放前面
{
swap(src[k], src[j]);
k--;
j--;
}
}
cout << "第" << k++ << "次的, 结束结果为:";
for (int val : src)
cout << setw(2) << val << " ";
cout << endl;
}
int one_Partition(vector<int>& src, int low, int high)// 左右交换法
{
int l = low, h = high;
int pivotkey = src[low];//这次把最左边的数据 作为基准数
int val_key = l;
int count_k = 0;
while (l < h)
{
while (l<h && src[h] >= pivotkey)
{
h--;
}
//在这里 就找到了 自最右边开始的 第一个小于pivotkey值的 数据
//然后接下来 就在给定数列 最左边开始找带一个大于 pivotkey值的 数据
while (l < h && src[l] <= pivotkey)
{
l++;
}
count_k++;
swap(src[l], src[h]);
}
//最后把pivotkey值 和中间集合下标数据 进行交换
swap(src[l], src[val_key]);
count_k++;
//打印一下每一次的 一次划分结束时的结果:
cout << "第" << k++ << "次的一次划分,经过了"
<< count_k << "次的交换。 结束结果为:";
for (int val : src)
cout << setw(2) << val << " ";
cout << endl;
return l;//把这个下标 返回,作为下一次递归的base中间线
}
void Quick_sort(vector<int>& src, int low, int high)
{
int base = one_Partition(src, low, high);//双指针前移法 实现
if (high - low + 1 < 5) // 当整个序列的大小high-low+1<5时,采用直接插入排序
{
Straight_Insertion_Sort2(src);
return;
}
else
{
if (low + 1 < base)
Quick_sort(src, low, base - 1);//递归左边
if (base + 1 < high)
Quick_sort(src, base + 1, high);//递归右边
}
}
第二种 聚集相同基准法
对于数列中 存在着大量的重复的数据时,可以考虑使用: 在一次划分后,把与基准值pivotkey相等的元素gather在一起,可以减少大量的迭代次数,从而大大地提升排序效率。
排序步骤就是:
- 在一次划分的过程中,把与基准值pivotkey相等元素放入数列的两端
- 划分结束后,把与基准值pivotkey相等的元素移到枢轴周围。
- 对重复数列的左右两部分 分别进行排序。
例如:待排序序列 1 4 6 7 6 6 7 6 8 6(这里选取基准值的方式:三数取中法)
- 三数取中选取枢轴:下标为4的数6
- 三数调整后,待分割序列:6 4 6 7 1 6 7 6 8 6。其基准值pivotkey:6
- 在一次划分的过程中,把与pivotkey相等元素放入数列的两端
如下为:6 4 1 6(枢轴) 7 8 7 6 6 6。此时,与6相等的元素全放入在两端了 - 本次的一次划分结束后,把与基准值pivotkey相等的元素移到枢轴的周围。结果如下为:1 4 6 6(枢轴) 6 6 6 7 8 7。此时,与6相等的元素全移到枢轴周围了。
- 接下来:在1 4 和 7 8 7两个子序列继续进行快速排序(即 重复上面四个步骤)。
#include <iostream>
#include <vector>
#include <iomanip>
#include <string>
#include <algorithm>
#include <time.h>
using namespace std;
static int k = 1;
// src[mid] <= src[low] <= src[high]
void resetlow_mid_high(vector<int>& src, int low, int high)//重置 三数取中
{
int mid = (low + high) >> 1;
if (src[mid] > src[low])
{
swap(src[mid], src[low]);
}
if (src[low] > src[high])
{
swap(src[low], src[high]);
}
if (src[mid] > src[high])
{
swap(src[mid], src[high]);
}
}
void Straight_Insertion_Sort2(vector<int>& src)//使用的是两两交换前进
{
int size = src.size();
for (int i = 0; i < size - 1; ++i)
{
int j = i + 1;//取出无序序列的第一个元素
int k = i;
while (k >= 0 && src[j] < src[k])//这个k的比较 必须放前面
{
swap(src[k], src[j]);
k--;
j--;
}
}
cout << "第" << k++ << "次的, 结束结果为(直接插入排序): ";
for (int val : src)
cout << setw(2) << val << " ";
cout << endl;
}
//下面是 一次划分 处理和基准值pivotkey重复数据的
int one_Partition(vector<int>& src, int low, int high)// 左右交换法
{
int l = low, h = high;
resetlow_mid_high(src, low, high);//三数取中 得到基准数
int pivotkey = src[low];
int val_key = l;
int count_k = 0;
while (l < h)
{
while (l < h && src[h] >= pivotkey)
{
h--;
}
//在这里 就找到了 自最右边开始的 第一个小于pivotkey值的 数据
//然后接下来 就在给定数列 最左边开始找带一个大于 pivotkey值的 数据
while (l < h && src[l] <= pivotkey)
{
l++;
}
count_k++;
swap(src[l], src[h]);
}
//最后把pivotkey值 和中间集合下标数据 进行交换
swap(src[l], src[val_key]);
count_k++;
//打印一下每一次的 一次划分结束时的结果:
cout << "第" << k++ << "次的一次划分,经过了" << count_k
<< "次的交换。 结束结果为:";
for (int val : src)
cout << setw(2) << val << " ";
cout << endl;
return l;//把这个下标 返回,作为下一次递归的base中间线
}
void gather_same_pivotkey(vector<int>& src, int low, int base, int high,
int* same_pivotkey_left, int* same_pivotkey_right)
{
int this_left = base - 1;//base 是上一次 一次划分返还的中间线位置下标
if (low < high)
{
for (int i = base - 1; i >= low; --i)
{
if (src[i] == src[base] && i != this_left)
{
swap(src[i], src[this_left]);
this_left--;
}
}
if (this_left >= low && src[this_left] == src[base])
* same_pivotkey_left = this_left;
else *same_pivotkey_left = this_left + 1;
//
//接下来 处理base的 右边数据
int this_right = base + 1;
for (int i = base + 1; i <= high; ++i)
{
if (src[i] == src[base] && i != this_right)
{
swap(src[i], src[this_right]);
this_right++;
}
}
if (this_right <= high && src[this_right] == src[base])
* same_pivotkey_right = this_right;
else *same_pivotkey_right = this_right - 1;
}
}
void Quick_sort(vector<int>& src, int low, int high)
{
#if 0
if (high - low + 1 < 10) // 当整个序列的大小high-low+1<10时,采用直接插入排序
{
Straight_Insertion_Sort2(src);
return;
}
#endif
int base = one_Partition(src, low, high);
//和基准数相同的 数值区间的左右下标
int same_pivotkey_left = 0, same_pivotkey_right = 0;
//将一次划分之后的 和pivotkey相同的数据 都集中到base的附近
//即:【same_pivotkey_left,same_pivotkey_right】
gather_same_pivotkey(src, low, base, high, &same_pivotkey_left, &same_pivotkey_right);
//接下来 处理的时候:只需要从same_pivotkey_left的左边开始
if (low + 1 < same_pivotkey_left)
Quick_sort(src, low, same_pivotkey_left - 1);//递归左边
//和只需要从same_pivotkey_right的右边开始
if (same_pivotkey_right + 1 < high)
Quick_sort(src, same_pivotkey_right + 1, high);//递归右边
}
int main()
{
int Array[] = { 1, 4, 6 ,7 ,6, 6, 7, 6, 8, 6 };
vector<int>myvec(begin(Array), end(Array));
cout << "排序Quick_sort结束前,初始状态: ";
for (int val : myvec)
{
cout << setw(2) << val << " ";
}
cout << endl;
Quick_sort(myvec, 0, myvec.size() - 1);
cout << "排序Quick_sort结束后,最终结果: ";
for (int val : myvec)
{
cout << setw(2) << val << " ";
}
cout << endl;
return 0;
}
注 :我这里是闭区间 【same_pivotkey_left,same_pivotkey_right】
注:2019年8月30日20:28:18 此次快速排序算法的研究到此为止。总共花了 大概3天时间,看了很多比较厉害的博客。这几天的成长 比在图论课堂上学到的东西 要多得多。是事必躬亲,才能有所成长。