首页 > 代码库 > 快速排序
快速排序
--------------------siwuxie095
快速排序法
它的工作原理如下:
它采用了一种分治的策略,利用分治法可将快速排序分为三步:
第一步:在数据集之中,选择一个元素作为 "基准"
第二步:所有小于 "基准" 的元素,都移到 "基准" 的左边;所有大于 "基准"
的元素,都移到 "基准" 的右边。这个操作称为分区操作,分区操作结束后,
基准元素所处的位置就是最终排序后它的位置
第三步:对 "基准" 左边和右边的两个子集,不断重复第一步和第二步,直到
所有子集只剩下一个元素为止
「分区操作是快速排序的主要内容」
参考链接:
参考链接1,参考链接2,参考链接3
程序 1:快速排序法
SortTestHelper.h:
#ifndef SORTTESTHELPER_H #define SORTTESTHELPER_H
#include <iostream> #include <string> #include <ctime> #include <cassert> using namespace std;
//辅助排序测试 namespace SortTestHelper {
//生成测试数据(测试用例),返回一个随机生成的数组: //生成有n个元素的随机数组,每个元素的随机范围为[rangeL,rangeR] int *generateRandomArray(int n, int rangeL, int rangeR) { //默认rangeL要小于等于rangeR assert(rangeL <= rangeR);
int *arr = new int[n];
//对于数组中的每一个元素,将之随机成为rangeL和rangeR之间的随机数 //先设置随机种子:这里将当前的时间作为种子来进行随机数的设置 srand(time(NULL));
for (int i = 0; i < n; i++) { //rand()函数+百分号+数的范围,即 取中间的一个随机整数,再加上rangeL即可 arr[i] = rand() % (rangeR - rangeL + 1) + rangeL; } return arr; }
//生成一个近乎有序的数组 int *generateNearlyOrderedArray(int n, int swapTimes) { //先生成完全有序的数组 int *arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = i; }
//以当前时间为随机种子 srand(time(NULL));
//再随机挑选几对元素进行交换,就是一个近乎有序的数组了 for (int i = 0; i < swapTimes; i++) { int posx = rand() % n; int posy = rand() % n; swap(arr[posx], arr[posy]); }
return arr; }
template<typename T> void printArray(T arr[], int n) { for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; }
//经过排序算法排序后,再次确认是否已经完全排序 template<typename T> bool isSorted(T arr[], int n) { for (int i = 0; i < n - 1; i++) { if (arr[i]>arr[i + 1]) { return false; } } return true; }
//衡量一个算法的性能如何,最简单的方式就是看这个算法在特定数据集上的执行时间 //(1)传入排序算法的名字,方便打印输出 //(2)传入排序算法本身,即函数指针 //(3)传入测试用例:数组和元素个数 template<typename T> void testSort(string sortName, void(*sort)(T[], int), T arr[], int n) { //在排序前后分别调用clock()函数 //时间差就是该排序算法执行的时钟周期的个数 clock_t startTime = clock(); sort(arr, n); clock_t endTime = clock();
assert(isSorted(arr, n));
//endTime 减去 startTime 转为double类型,除以 CLOCKS_PER_SEC,其中: // //CLOCKS_PER_SEC 是标准库中定义的一个宏,表示每一秒钟所运行的时钟周期 //的个数,而(endTime-startTime)返回的是运行了几个时钟周期 // //这样,最终的结果就是在这段时间中程序执行了多少秒 cout << sortName << ":" << double(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl; }
//复制数组 int *copyIntArray(int a[], int n) { int *arr = new int[n]; //copy()函数在std中: //第一个参数是原数组的头指针, //第二个参数是原数组的尾指针, //第三个参数是目的数组的头指针 // //注意:copy()函数运行时会报错,需要在: //项目->属性->配置属性->C/C++->预处理器->预处理器定义 //在其中添加:_SCL_SECURE_NO_WARNINGS copy(a, a + n, arr); return arr; } }
#endif |
QuickSort.h:
#ifndef QUICKSORT_H #define QUICKSORT_H
// 对arr[l...r]部分进行partition操作(分区操作) // 返回j,使得arr[l...j-1] < arr[j] ; arr[j+1...r] > arr[j] template <typename T> int __partition(T arr[], int l, int r) {
//基准元素取第一个元素,即arr[l] T v = arr[l];
// arr[l+1...j] < v ; arr[j+1...i) > v // //其中: //l+1 到 j 是前闭后闭,而 j+1 到 i 是前闭后开, //因为 i 位置是当前要考察的元素 // //j 初始化为 l,即 j<l+1,换言之,在初始情况下, //[l+1...j]这个区间是不存在的,是空的 int j = l;
//而 i 是循环遍历的索引,初值为 l+1 for (int i = l + 1; i <= r; i++) { //如果当前考察的元素arr[i]大于v,只需要i++, //继续向后考察即可,什么都不需要做 //(实际上等于v的部分也放到了大于v的部分) // //关键是当arr[i]小于v的时候,如下: if (arr[i] < v) { j++; swap(arr[j], arr[i]);
//或,如下:交换j+1位置的元素和当前考察的元素,交换完毕,j++ //swap(arr[j+1], arr[i]); //j++; //或,如下: //swap(arr[++j], arr[i]); } }
//把arr[l] 即 基准元素v,和arr[j]交换位置, //使得arr[l...j-1]<v ; arr[j+1...r]>v // //也即 把基准元素v正式放到中间,前面的元素 //都小于v,后面的元素都大于v //(实际上等于v的部分也放到了大于v的部分) swap(arr[l], arr[j]);
return j; }
// 对arr[l...r]部分进行快速排序 template <typename T> void __quickSort(T arr[], int l, int r) {
//如果递归到底,则返回 if (l >= r) { return; }
//分区操作:对l到r进行partition操作,并返回索引值p int p = __partition(arr, l, r);
__quickSort(arr, l, p - 1); __quickSort(arr, p + 1, r); }
//快速排序:从小到大进行排序 template <typename T> void quickSort(T arr[], int n) {
//整个快速排序将使用递归的方式对整个数组进行排序, //为此,要调用一个私有的递归函数 __quickSort() __quickSort(arr, 0, n - 1); }
//快速排序被称为二十世纪对世界影响最大的算法之一,这个算法之所以这么著名, //也正如它的名字所显示的那样,它能以非常快的速度完成排序这个任务,但即使 //如此,快速排序算法也是经过了很长时间的改进、优化,才被公认为是一个可以 //被信任的非常优秀的排序算法 // // // //快速排序的基本思想: // //归并排序是不管数组的内容是什么,直接将整个数组一分为二,之后再靠归并操作, //逐渐的排序 // //快速排序则是每次从当前考虑的数组中,选择一个元素,以这个元素为基准,之后 //想办法把基准位置的这个元素挪到它在排好序之后该所处的位置 // //如:数组中含有 5 个元素:1、4、2、3、5,如果选择元素4为基准,则排好序后, //元素4所在的位置就在第3个位置(索引) // //基准元素处在该处的位置,使得整个数组就有了这样一个性质:基准元素之前的所 //有元素都是小于基准元素的,基准元素之后的所有元素都是大于基准元素的 // //之后要做的就是对小于基准元素的这部分子数组和大于基准元素的这部分子数组分 //别继续使用快速排序的思路进行排序,逐渐递归下去,完成整个排序过程 // // //因此对于快速排序来说,最重要的就是如何把一个选定的元素挪到正确的位置上, //而这个过程也是快速排序的核心,通常把这个过程称为 Partition,也就是把整 //个数组分成两个部分这个过程 // //那么在Partition的过程中,都使用整个数组的第一个元素v作为我们分界的标志点, //也就是基准元素,对于这个数组来说,它的第一个位置,我们把它叫做 l,也就是 //left,之后逐渐遍历右边所有没有被访问的元素 // //在遍历的过程中,将逐渐整理,让整个数组,一部分是小于v的,另一部分是大于v的, //在这个过程中,就会记录小于v和大于v的分界点的位置,用一个索引j来记录这个位置, //当前访问的元素e的位置是i,这样一来,[l+1,j] 即 l+1 到 j 前闭后闭,这个区间 //的元素,就都是小于v的,而arr[j+1,i-1],即 j+1 到 i-1 也是前闭后闭,这个区间 //的所有元素就都是大于v的 // //下面讨论 i 这个位置,看如何决定当前考察的元素到底要怎样变化才能使整个数组保 //持上述的性质,分两种情况讨论: //(1)如果当前考察的元素e,比v还要大,直接把它放在大于v这一部分的后面即可 //(2)如果当前考察的元素e,比v还要小,要想办法把当前考察的元素e放到小于v的部 //分,只需要把j位置的元素和当前考察的i位置的元素,两个元素交换一下位置即可 // // // //************************************************************************** //此时的快速排序的弊端: // //对于近乎有序的数组来说,它的速度比起同样是O(n*lgn)级别的排序算法-归并排序 //就会慢了许多 // //因为在二分的过程中,归并排序可以保证每次都是平均的将整个数组一分为二, //而快速排序却没有这个保证 // //即 快速排序分出来的两个子数组可能是一大一小,所以快速排序调用递归的过程, //所生成的递归树的平衡度就比归并排序的要差,并且不能完全保证这颗树的高度就 //是log(N),它很有可能比log(N)还要高 // //快速排序最差的情况就是当整个数组近乎有序时,甚至如果整个数组完全有序,快 //速排序就会退化成了一个O(n^2)级别的算法 // //这也是为什么数组在近乎有序的情况下,快速排序那么慢的原因 // // //那么怎么改变这种情况?其实非常简单,如下: //现在是固定的选用左侧的第一个元素,作为标定元素(基准元素),然而所希望的 //却是尽可能选择整个数组中间(即大小适中)的那个元素,作为标定元素 // //我们不能非常快速的准确的定位那个中间元素,怎么办? //其实只要随机选择一个元素就好了,当随机选择一个元素作为标定元素时,可以用 //数学的方法证明出来,此时快速排序的时间复杂度的期望值是 n*lgn //**************************************************************************
#endif |
main.cpp:
#include "SortTestHelper.h" #include "QuickSort.h"
int main() {
int n = 1000000;
int *arr = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Quick Sort", quickSort, arr, n);
delete []arr;
system("pause"); return 0; } |
运行一览:
程序 2:随机化快速排序法
SortTestHelper.h:
#ifndef SORTTESTHELPER_H #define SORTTESTHELPER_H
#include <iostream> #include <string> #include <ctime> #include <cassert> using namespace std;
//辅助排序测试 namespace SortTestHelper {
//生成测试数据(测试用例),返回一个随机生成的数组: //生成有n个元素的随机数组,每个元素的随机范围为[rangeL,rangeR] int *generateRandomArray(int n, int rangeL, int rangeR) { //默认rangeL要小于等于rangeR assert(rangeL <= rangeR);
int *arr = new int[n];
//对于数组中的每一个元素,将之随机成为rangeL和rangeR之间的随机数 //先设置随机种子:这里将当前的时间作为种子来进行随机数的设置 srand(time(NULL));
for (int i = 0; i < n; i++) { //rand()函数+百分号+数的范围,即 取中间的一个随机整数,再加上rangeL即可 arr[i] = rand() % (rangeR - rangeL + 1) + rangeL; } return arr; }
//生成一个近乎有序的数组 int *generateNearlyOrderedArray(int n, int swapTimes) { //先生成完全有序的数组 int *arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = i; }
//以当前时间为随机种子 srand(time(NULL));
//再随机挑选几对元素进行交换,就是一个近乎有序的数组了 for (int i = 0; i < swapTimes; i++) { int posx = rand() % n; int posy = rand() % n; swap(arr[posx], arr[posy]); }
return arr; }
template<typename T> void printArray(T arr[], int n) { for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; }
//经过排序算法排序后,再次确认是否已经完全排序 template<typename T> bool isSorted(T arr[], int n) { for (int i = 0; i < n - 1; i++) { if (arr[i]>arr[i + 1]) { return false; } } return true; }
//衡量一个算法的性能如何,最简单的方式就是看这个算法在特定数据集上的执行时间 //(1)传入排序算法的名字,方便打印输出 //(2)传入排序算法本身,即函数指针 //(3)传入测试用例:数组和元素个数 template<typename T> void testSort(string sortName, void(*sort)(T[], int), T arr[], int n) { //在排序前后分别调用clock()函数 //时间差就是该排序算法执行的时钟周期的个数 clock_t startTime = clock(); sort(arr, n); clock_t endTime = clock();
assert(isSorted(arr, n));
//endTime 减去 startTime 转为double类型,除以 CLOCKS_PER_SEC,其中: // //CLOCKS_PER_SEC 是标准库中定义的一个宏,表示每一秒钟所运行的时钟周期 //的个数,而(endTime-startTime)返回的是运行了几个时钟周期 // //这样,最终的结果就是在这段时间中程序执行了多少秒 cout << sortName << ":" << double(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl; }
//复制数组 int *copyIntArray(int a[], int n) { int *arr = new int[n]; //copy()函数在std中: //第一个参数是原数组的头指针, //第二个参数是原数组的尾指针, //第三个参数是目的数组的头指针 // //注意:copy()函数运行时会报错,需要在: //项目->属性->配置属性->C/C++->预处理器->预处理器定义 //在其中添加:_SCL_SECURE_NO_WARNINGS copy(a, a + n, arr); return arr; } }
#endif |
InsertionSort.h:
#ifndef INSERTIONSORT_H #define INSERTIONSORT_H
//插入排序:从小到大进行排序 template<typename T> void insertionSort(T arr[], int n) { //对于插入排序来说,第一个元素(索引为0)根本不用考虑 // //因为在插入排序的初始,第一个元素放在那里,它本身就已经 //有序了,不需要再把它插入到前面的任何位置,所以从第二个 //元素(索引为1)开始考察 for (int i = 1; i < n; i++) { //寻找arr[i]合适的插入位置: //对已经排好序的部分,插入时从后向前比较, //最多只会考察到j=1时,因为是从后向前比较 // //先将要插入的元素arr[i]保存到e中,在和已经排好序的部分进行比较时: //(1)如果小于当前被比较的元素arr[j-1],则将当前被比较的元素向后移一位 //(2)如果大于当前被比较的元素arr[j-1],则当前位置j就是要插入的位置 T e = arr[i]; int j;//保存元素e应该插入的位置 for (j = i; j > 0 && arr[j - 1] > e; j--) { arr[j] = arr[j - 1]; } //将之前保存在e中的元素插入到位置j arr[j] = e;
} }
// 对arr[l...r]范围的数组进行插入排序 template<typename T> void insertionSort(T arr[], int l, int r) {
for (int i = l + 1; i <= r; i++) {
T e = arr[i]; int j; for (j = i; j > l && arr[j - 1] > e; j--) { arr[j] = arr[j - 1]; } arr[j] = e;
}
}
//插入排序是 O(n^2) 级别算法复杂度的排序算法,它的思想就类似于 //在玩扑克牌时插入牌的思想,即 将拿到的一张牌插入到合适的位置, //当最后一张牌也插入后,手中的整副扑克牌也就排序完成了
#endif |
QuickSort.h:
#ifndef QUICKSORT_H #define QUICKSORT_H
#include "InsertionSort.h" #include <ctime>
// 对arr[l...r]部分进行partition操作(分区操作) // 返回j,使得arr[l...j-1] < arr[j] ; arr[j+1...r] > arr[j] template <typename T> int __partition(T arr[], int l, int r) {
//随机选择一个元素和第一个元素交换位置 swap(arr[l], arr[rand() % (r - l + 1) + l]);
//基准元素取第一个元素,实际上是一个随机元素 T v = arr[l];
// arr[l+1...j] < v ; arr[j+1...i) > v // //其中: //l+1 到 j 是前闭后闭,而 j+1 到 i 是前闭后开, //因为 i 位置是当前要考察的元素 // //j 初始化为 l,即 j<l+1,换言之,在初始情况下, //[l+1...j]这个区间是不存在的,是空的 int j = l;
//而 i 是循环遍历的索引,初值为 l+1 for (int i = l + 1; i <= r; i++) { //如果当前考察的元素arr[i]大于v,只需要i++, //继续向后考察即可,什么都不需要做 //(实际上等于v的部分也放到了大于v的部分) // //关键是当arr[i]小于v的时候,如下: if (arr[i] < v) { j++; swap(arr[j], arr[i]);
//或,如下:交换j+1位置的元素和当前考察的元素,交换完毕,j++ //swap(arr[j+1], arr[i]); //j++; //或,如下: //swap(arr[++j], arr[i]); } }
//把arr[l] 即 基准元素v,和arr[j]交换位置, //使得arr[l...j-1]<v ; arr[j+1...r]>v // //也即 把基准元素v正式放到中间,前面的元素 //都小于v,后面的元素都大于v //(实际上等于v的部分也放到了大于v的部分) swap(arr[l], arr[j]);
return j; }
// 对arr[l...r]部分进行快速排序 template <typename T> void __quickSort(T arr[], int l, int r) { // 对于小规模数组,使用插入排序 //高级的排序算法到底层的时候,都可以采用插入排序的方法来进行优化 if (r - l <= 15) { insertionSort(arr, l, r); return; }
//分区操作:对l到r进行partition操作,并返回索引值p int p = __partition(arr, l, r);
__quickSort(arr, l, p - 1); __quickSort(arr, p + 1, r); }
//随机快速排序:从小到大进行排序 template <typename T> void quickSort(T arr[], int n) {
//使用srand()函数设置随机种子 srand(time(NULL)); //整个快速排序将使用递归的方式对整个数组进行排序, //为此,要调用一个私有的递归函数 __quickSort() __quickSort(arr, 0, n - 1); }
//这样,就使用了一个随机化的方案,让快速排序在近乎有序的数组的情况下, //也能非常出色的完成任务,这里,实际上相当于编写了一个随机算法 // //所谓随机算法,就是:我不能保证我的这个算法一定非常快,或一定是正确的, //但是我可以保证我的这个算法在 99.99% 的情况下,也就是说是一个非常高的 //概率的情况下,都能非常快的并且非常准确的得到结果 // //可以想象,此时的快速排序,它最坏的时间复杂度依然是O(n^2)的,但是退化 //到O(n^2)级别的这个时间复杂度的概率是极其低的,近乎为0 // //当然,对于使用这样一个随机化的方法,此时快速排序的时间复杂度的期望值 //就变成了 n*lgn // // //************************************************************************** //此时的快速排序依然存在弊端: // //如果随机数组的随机范围是0到10之间,此时,n依然是1百万,不难想象,这样一个 //数组就是一个拥有非常多的重复元素的数组 // //针对这种情况,快速排序的效率非常的低,它再次退化到O(n^2)这个级别,当整个 //数组包含大量重复元素时,那么partition操作(分区操作)就会非常有可能把整个 //数组分成极度不平衡的两部分 // //这是因为对于每一个元素来说,相同的重复元素太多,如果选的基准元素v稍微有一 //点不平衡的话,那么两部分的差距就会非常大,即使v选在了一个平衡的位置,但由 //于等于v的这个部分非常多,一样会导致整个数组被分成了两个及其不平衡的部分, //此时,快速排序就会退化到O(n^2)这样一个级别的排序算法 // //怎么改变这种状况呢?可以选择 双路快速排序法:选定第一个元素为基准元素v, //从头部(第二个元素)和尾部(最后一个元素)同时遍历,小于v的放在前面, //大于v的放在后面,而等于v的自由散落,有的落在了前面,有的落在了后面 // //即 将整个数组分成两路:小于v 和 大于v //**************************************************************************
#endif |
main.cpp:
#include "SortTestHelper.h" #include "QuickSort.h"
int main() {
int n = 1000000;
// 测试1 一般性测试 int *arr1 = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Quick Sort", quickSort, arr1, n);
delete []arr1;
// 测试2 测试近乎有序的数组 int swapTimes = 100;
int *arr2 = SortTestHelper::generateNearlyOrderedArray(n, swapTimes);
SortTestHelper::testSort("Quick Sort", quickSort, arr2, n);
delete []arr2;
system("pause"); return 0; } |
运行一览:
程序 3:双路快速排序法(在程序 2 的基础上,修改 QuickSort.h 和 main.cpp 即可)
QuickSort.h:
#ifndef QUICKSORT_H #define QUICKSORT_H
#include "InsertionSort.h" #include <ctime>
// 对arr[l...r]部分进行partition操作(分区操作) // 返回j,使得arr[l...j-1] < arr[j] ; arr[j+1...r] > arr[j] template <typename T> int __partition(T arr[], int l, int r) {
//随机选择一个元素和第一个元素交换位置 swap(arr[l], arr[rand() % (r - l + 1) + l]);
//基准元素v取第一个元素,实际上是一个随机元素 T v = arr[l];
// arr[l+1...i) <= v; arr(j...r] >= v // //其中: //l+1 到 i 是前闭后开,这个区间里的所有元素都小于等于 v, //且 i 位置是当前要考察的元素 // //j 到 r 是前开后闭,这个区间里的所有元素都大于等于 v, //且 j 位置也是当前要考察的元素 // // //i 初始化为 l+1,即 i=l+1,换言之,在初始情况下, //[l+1...i)这个区间是不存在的,因为一边开一边闭 // //j 初始化为 r,即 j=r,换言之,在初始情况下, //(j...r]这个区间是不存在的,因为一边开一边闭 int i = l + 1, j = r;
while (true) { //i是从前往后遍历 while (i <= r && arr[i] < v) { i++; }
//j是从后往前遍历 while (j >= l + 1 && arr[j] > v) { j--; }
//如果i>j,则遍历结束 if (i > j) { break; }
//否则进行交换 swap(arr[i], arr[j]); i++; j--; }
//最后把arr[l],即 基准元素v放到整理完后的数组的合适位置 swap(arr[l], arr[j]);
return j; }
// 对arr[l...r]部分进行快速排序 template <typename T> void __quickSort(T arr[], int l, int r) {
// 对于小规模数组,使用插入排序 //高级的排序算法到底层的时候,都可以采用插入排序的方法来进行优化 if (r - l <= 15) { insertionSort(arr, l, r); return; }
//分区操作:对l到r进行partition操作,并返回索引值p int p = __partition(arr, l, r);
__quickSort(arr, l, p - 1); __quickSort(arr, p + 1, r); }
//双路快速排序:从小到大进行排序 template <typename T> void quickSort(T arr[], int n) {
//使用srand()函数设置随机种子 srand(time(NULL)); //整个快速排序将使用递归的方式对整个数组进行排序, //为此,要调用一个私有的递归函数 __quickSort() __quickSort(arr, 0, n - 1); }
//************************************************************** //双路快速排序可继续改进和优化,让快速排序的性能在有很多重复元素 //的情况下,效率更高 // //改进和优化的方法正是 三路快速排序:选定第一个元素为基准元素v, //从头部(第二个元素)开始遍历,小于v的放在前面,大于v的放在后 //面,而等于v的放在中间 // //即 将整个数组分成三路:小于v、大于v、等于v //**************************************************************
#endif |
main.cpp:
#include "SortTestHelper.h" #include "QuickSort.h"
int main() {
int n = 1000000;
// 测试1 一般性测试 int* arr1 = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Quick Sort", quickSort, arr1, n);
delete []arr1;
// 测试2 测试近乎有序的数组 int swapTimes = 100;
int *arr2 = SortTestHelper::generateNearlyOrderedArray(n, swapTimes);
SortTestHelper::testSort("Quick Sort", quickSort, arr2, n);
delete []arr2;
// 测试3 测试存在包含大量相同元素的数组 int *arr3 = SortTestHelper::generateRandomArray(n, 0, 10);
SortTestHelper::testSort("Quick Sort", quickSort, arr3, n);
delete []arr3;
system("pause"); return 0; } |
运行一览:
程序 4:三路快速排序法(在程序 2 的基础上,修改 QuickSort.h 和 main.cpp 即可)
QuickSort.h:
#ifndef QUICKSORT_H #define QUICKSORT_H
#include "InsertionSort.h" #include <ctime>
//三路快速排序处理 arr[l...r] //将arr[l...r]分为 <v ; ==v ; >v 三部分 //之后递归对 <v ; >v 两部分继续进行三路快速排序 template <typename T> void __quickSort3Ways(T arr[], int l, int r) {
// 对于小规模数组,使用插入排序 //高级的排序算法到底层的时候,都可以采用插入排序的方法来进行优化 if (r - l <= 15) { insertionSort(arr, l, r); return; }
//由于整个三路排序的过程,不是像其它快速排序那样简单的返回一个 //索引 p,然后对 p-1 的部分和 p+1 的部分进行递归 // //而是对于中间等于v的部分也是一个区间因此: //partition操作(分区操作)的部分就不设计成一个函数了 // //这是因为在C++这样的语言中,这样的函数返回值不太好表达,就直接 //在这个 __quickSort3Ways() 里写partition操作的部分即可,如下: //随机选择一个元素和第一个元素交换位置 swap(arr[l], arr[rand() % (r - l + 1) + l]);
//基准元素v取第一个元素,实际上是一个随机元素 T v = arr[l];
//arr[l+1...lt] < v ; arr[gt...r] > v ; arr[lt+1...i) == v // //其中: //l+1 到 lt 是前闭后闭,lt 即 less than //gt 到 l 是前闭后闭,gt 即 greater than //lt+1 到 i 是前闭后开,因为 i 位置是当前考察的元素 // // // lt 初始化为 l,即 lt=l,换言之,在初始情况下, //[l+1...lt]这个区间是不存在的 // //gt 初始化为 r+1,即 gt=r+1,换言之,在初始情况下, //[gt...r]这个区间是不存在的 // //i 初始化为 l+1,即 i=l+1=lt+1,换言之,在初始情况下, //[lt+1...i)这个区间是不存在的,因为一边开一边闭 int lt = l; int gt = r + 1; int i = l + 1;
//只要 i 还没有和 gt 碰上,就继续循环 while (i < gt) { //如果当前考察的元素小于v,就交换到lt+1的位置,并维护 lt if (arr[i] < v) { swap(arr[i], arr[lt + 1]); i++;//考察下一个元素 lt++;//维护索引 lt,让它++ } //如果当前考察的元素大于v,就交换到gt-1的位置,并维护 gt // //注意:此时,i不需要动,因为此时i位置上依然是没有被处理 //的元素 else if (arr[i] > v) { swap(arr[i], arr[gt - 1]); gt--;//维护索引 gt,让它-- } //如果当前考察的元素等于v,位置不动,直接i++,考察下一个 //元素即可 else { i++; } }
//将arr[l],即基准元素v,相应的放到等于v的这部分的第一个位置 swap(arr[l], arr[lt]);
//因为基准元素v交换到lt位置后,没有对lt做--操作,所以传入lt-1 __quickSort3Ways(arr, l, lt - 1); __quickSort3Ways(arr, gt, r); }
//三路快速排序:从小到大进行排序 template <typename T> void quickSort3Ways(T arr[], int n) {
//使用srand()函数设置随机种子 srand(time(NULL)); //整个快速排序将使用递归的方式对整个数组进行排序, //为此,要调用一个私有的递归函数 __quickSort3Ways() __quickSort3Ways(arr, 0, n - 1); }
//使用快速排序的思想,给带有大量重复元素的数组进行排序的一个 //更加经典的是实现方式,即 三路快速排序法 // //三路快速排序背后的思想非常简单,之前进行快速排序的操作时, //都是将整个数组分成两部分:小于v和大于v // //而三路的快速排序,则把整个数组分成三部分,小于v、等于v 和 //大于v,不难想象,这样分割之后,在递归的过程中,对于等于v的 //部分,就根本不用管了,只需要递归的继续对小于v和大于v的部分 //进行同样的快速排序就好了
#endif |
main.cpp:
#include "SortTestHelper.h" #include "QuickSort.h"
int main() {
int n = 1000000;
// 测试1 一般性测试 int* arr1 = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr1, n);
delete []arr1;
// 测试2 测试近乎有序的数组 int swapTimes = 100;
int *arr2 = SortTestHelper::generateNearlyOrderedArray(n, swapTimes);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr2, n);
delete []arr2;
// 测试3 测试存在包含大量相同元素的数组 int *arr3 = SortTestHelper::generateRandomArray(n, 0, 10);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr3, n);
delete []arr3;
system("pause"); return 0; }
//一般系统级别的快速排序都会选择三路快速排序方案,就是因为它在处理 //有重复元素的数组时,优势非常大,而在处理没有重复元素的数组时,它 //的速度也可以得到保证 // // //******************************************************************* //归并排序和快速排序的衍生问题: // //这两种排序算法不仅更加高效快速的解决了排序这个问题,这两种算法本身 //背后也隐藏着非常深刻的算法设计的思想,即 它们都使用了分治算法的基本 //思想 // //所谓分治算法,顾名思义 ,分而治之,就是将原问题分割成同等结构的子问 //题,之后将子问题逐一解决后,原问题也就得到了解决 // //这里,归并排序和快速排序都是将原问题分割成两个子问题,但同时,归并 //排序和快速排序,也代表了分治算法的两类基本思想: // //对于归并排序来说,在分这个问题上,没有做太多的考虑,就是一刀切的把 //整个数组分成两部分,然后递归的进行归并排序,但关键在于,这样分完之 //后,如何把它们归并起来,也就是merge操作(归并操作)的过程 // //对于快速排序,则是费了很大的功夫在了如何分这个问题上,这里采用一个 //标定点(基准),然后使用partition操作(分区操作)这个子过程,将这 //个标定点移到合适的位置,之后,才将整个数组分割成了两部分,这样分完 //之后,在合的时候,只需一步一步的递归下去即可,不用再考虑其它 // // // //下面讨论两个直接从归并排序和快速排序所衍生出来的具体的问题: // //(1)求一个数组中,逆序对的数量 // //所谓逆序对就是从数组中任意抽出两个元素,如果前面的元素大于后面 //的元素,这就是一个逆序对。一个数组中,逆序对的数量,最典型的应 //用就是衡量这个数组的有序程度 // //暴力解法:用双重循环,考察每一个数对,如果是逆序对,计数器加1, //算法复杂度:O(n^2) // //归并排序:采用归并排序的思路求逆序对的个数,算法复杂度:O(n*lgn) // // //(2)取一个数组中第n大的元素 // //把这个问题退化一下,取一个数组中的最大值和最小值,这个只要从头 //到尾遍历,扫描一遍就好了。遍历,算法复杂度:O(n) // //如果取第n大的数组,也能直接想到的一个办法就是,给整个数组排遍序, //排完序后可以直接索引到那个第n大的元素。排序,算法复杂度:O(n*lgn) // //快速排序:采用快速排序的思路求数组中第n大元素,算法复杂度:O(n) //******************************************************************* |
运行一览:
【made by siwuxie095】
快速排序