排序算法之快速排序及其优化 快速排序介绍 快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),一种排序算法,最早由东尼·霍尔提出。使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
快速排序通常明显比其他Ο(n log n)算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。在平均状况下,排序n个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n\^2)次比较,但这种状况并不常见。
快速排序 看图说话 实例分析排序过程 待排数组:
选取数组的第一个元素为基准:5
将小于5的元素放在其左边,大于5的元素放在其右边:
然后不断的对左右两边进行上面的分割,最终为:
注意:我用线将当前的数组的顺序连接了起来,这已经是排序完成了,大家看了下面的分割过程,自己将这个过程用这样的图在纸上或者keynote等画出来,之后,下面的程序是非常好理解的!
分析分割过程 我们来看分割: 定义 v 为基准值;定义 l 为数组的第一个元素的索引;j 为小于基准值的最后一个元素的索引;i 为当前和基准值比较的值的索引,e 为 其值。
数组arr中 [l+1,j] 区间的值都是 小于 v 的,[j+1,i-1] 都是大于 v 的,具体如下图:
1.当 e 小于 v 的时候: 把 j+1 位置的值 和 e 交换:
然后 j ++,i ++,就行了;
2.当 e 大于 v 的时候:
这个就简单了 直接 i ++ 就 O 了;
3.当分割完了之后:
此时,只要将 l 和 j 的位置的值交换一下:
我们已经将数组分割成为:左边小于 v ,右边 大于 v.
关于边界处理,请看后面的程序。
下面是分割的代码:
1 2 3 4 5 6 7 8 9 10 11 12 int __partition(int arr[],int l,int r) { int v = arr[l] int j = l for (int i = l+1 if (arr[i] < v) { swap(&arr[j +1 ], &arr[i]) j ++ } } swap(&arr[l], &arr[j]) return j }
相信,有了前面的图,理解这个程序是很容易的。
快速排序,使用递归的方法是很容易理解的,我们这里也采用递归的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void __quickSort(int arr[],int l,int r) { if (l >= r) { return ; } int p = __partition(arr,l,r); __quickSort(arr, l, p-1 ); __quickSort(arr, p+1 , r); } void quickSort (int arr[],int n) { __quickSort(arr,0 ,n-1 ); }
上面不断的递归分割,直到 l > r 结束递归;你能一步步的看到这里,就可以在纸上,手撸快速排序的代码了。
测试一下 和前面的一样 还是四种测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 * * * * * * * * * * * * * * 有序* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.004465s sortname:归并排序 sorttime: 0.023814s sortname:快速排序 sorttime: 14.126987s * * * * * * * * * * * * * * 近乎有序* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.015746s sortname:归并排序 sorttime: 0.031117s sortname:快速排序 sorttime: 0.896174s * * * * * * * * * * * * * * 随机* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.034790s sortname:归并排序 sorttime: 0.037268s sortname:快速排序 sorttime: 0.018142s * * * * * * * * * * * * * * 倒叙* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.010014s sortname:归并排序 sorttime: 0.024656s sortname:快速排序 sorttime: 23.304824s
快速排序不是号称最快的排序方法么,可是测试结果说明了一切 😂😂😂
其实我们写的这个并没有经过优化,并且快速排序并非能适应所有的排序场景。
随机化快速排序 首先想一下归并排序是不是和快速排序的方法很相似啊,不同的是归并排序是等分的而快速排序不是的,是根据选择的基值来分的,我们上面选择的是数组的第一个
最差的情况是测试用例中 倒叙的:
我们想的是:每次得取数组中中间的那个数值作为基值,但这样做需要重新遍历一遍数组,耗费了大量的时间。我们可以去个折中的方法,就是随机取数组中的值作为基值,且看程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 int __partition_00(int arr[],int l,int r) { swap(&arr[l], &arr[rand()%(r-l+1 ) + l]); int v = arr[l]; int j = l; for (int i = l+1 ; i <= r; i++) { if (arr[i] < v) { swap(&arr[j +1 ], &arr[i]); j ++; } } swap(&arr[l], &arr[j]); return j; }
程序中的 swap(&arr[l], &arr[rand()%(r-l+1) + l]); 就是使用 rand() 函数 随机取了值 和 为l索引上的值交换,作为基值,虽然还是有可能 降为 n^2,但概率远远的降低了。
我们再来看测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 * * * * * * * * * * * * * * 有序* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.004857s sortname:归并排序 sorttime: 0.026325s sortname:快速排序 sorttime: 14.352925s sortname:快速排序随机 sorttime: 0.013611s * * * * * * * * * * * * * * 近乎有序* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.012678s sortname:归并排序 sorttime: 0.025845s sortname:快速排序 sorttime: 0.632769s sortname:快速排序随机 sorttime: 0.013047s * * * * * * * * * * * * * * 随机* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.031845s sortname:归并排序 sorttime: 0.036922s sortname:快速排序 sorttime: 0.016712s sortname:快速排序随机 sorttime: 0.020723s * * * * * * * * * * * * * * 倒序* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.010177s sortname:归并排序 sorttime: 0.023441s sortname:快速排序 sorttime: 23.705080s sortname:快速排序随机 sorttime: 0.014164s
我们再看时间,是不是在性能上有不小的提升
双路快速排序 在上面测试的几种用例当中 有大量的重复值 这一项让快速排序慢的不是一点点。那为什么这么慢呢?
我们来看一组测试数据:
1 5 , 6 , 5 , 5 , 5 , 5 , 8 , 3 , 5 , 5
第一次进行分割之后变成:
1 [3 ] [5 , 5 , 5 , 5 , 5 , 8 , 6 , 5 , 5 ]
相信看到这里大家就明白了,快速排序变成了 O(n^2 ) 了:
在这里介绍一种解决方法:双路快速排序
双路快速排序的分析 我们用两路分别从左右两边进行分割;
l :指向选取的 v 在数组的首位;
i :从左向右指向下一个要扫描的元素;
j :从右向左指向下一个要扫描的元素;
然后进行如下操作:
从左向右扫描直到 >= v 停止;
从右向左扫描直到 <= v 停止;
交换 i 和 j 指向的元素;
i++,j++;
直到 j <= i 停止扫描;
大家可能会有这样的一个疑问:当 左右的两个 e 元素 相等时也会进行交换的
是的,这样可以确保在一定程度上分割平衡。所以分割实际上是这样的:
arr[l+1,i-1] <= v 和 arr[j,r] >= v
双路快速排序的程序代码 现在分割函数写成了这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int __partition_twoWays(int arr[],int l,int r) { swap(&arr[l], &arr[rand()%(r-l+1 ) + l]); int v = arr[l]; int i = l + 1 ; int j = r; while (1 ) { while (i <= r && arr[i] < v) { i ++; } while (j >= l+1 && arr[j] > v) { j --; } if (j <= i) { break ; } swap(&arr[i], &arr[j]); i++; j--; } swap(&arr[l], &arr[j]); return j; }
测试 我们再来看测试(测试用例 50w个):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 * * * * * * * * * * * * * * 有序* * * * * * * * * * * * * sortname:插入排序优化 sorttime: 0.002745s sortname:希尔排序 sorttime: 0.026173s sortname:快速排序随机 sorttime: 0.078291s sortname:快速排序双路 sorttime: 0.047122s * * * * * * * * * * * * * * 近乎有序* * * * * * * * * * * * * sortname:插入排序优化 sorttime: 0.010042s sortname:希尔排序 sorttime: 0.030206s sortname:快速排序随机 sorttime: 0.063545s sortname:快速排序双路 sorttime: 0.046858s * * * * * * * * * * * * * * 有大量的重复值* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.053225s sortname:快速排序随机 sorttime: 27.211613s sortname:快速排序双路 sorttime: 0.058205s * * * * * * * * * * * * * * 随机* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.179634s sortname:快速排序随机 sorttime: 0.102915s sortname:快速排序双路 sorttime: 0.102822s * * * * * * * * * * * * * * 倒叙* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.056003s sortname:快速排序随机 sorttime: 0.065304s sortname:快速排序双路 sorttime: 0.048225s
我们可以看到 双路快速排序 的时间相比之前快了不少!
三路快速排序 关于 有大量的重复值 的快速排序还有一种经典的解法 -> 三路快速排序
三路快速排序的分析 我们重新定义了如下几个变量:
l :指向选取的 v 在数组的首位;
lt :从左向右 小于v 的最后一个元素(初始情况: lt = l);
i :从左向右指向下一个要扫描的元素(初始情况: i = l + 1);
gt :从右向左 大于v 的第一个元素(初始情况: gt = r + 1);
初始的情况
当 e == v 时
这个很简单 直接 i ++ 就行了,如下图
然后:
当 e < v 时
将 i 指向的元素 和 lt+1 指向的元素 交换位置,然后 i++ , lt++
然后:
当 e > v 时
将 i 指向的元素 和 gt-1 指向的元素 交换位置,然后 gt –
然后:
扫描完成时
三路快速排序的程序代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void __quickSort_threeWays(int arr[],int l,int r) { if (l >= r) { return ; } swap(&arr[l], &arr[rand ()%( r-l+1 ) + l]); int v = arr[l]; // arr[l+1 ...lt ] < v 、 arr[lt +1 ...i-1 ] == v 和 arr[gt ...r] > v int lt = l; int i = l + 1 ; int gt = r + 1 ; while (i < gt ) { if (arr[i] < v) { swap(&arr[i], &arr[lt + 1 ]); i++; lt ++; } else if (arr[i] > v) { swap(&arr[i], &arr[gt - 1 ]); gt --; } else { i ++; } } swap(&arr[l], &arr[lt ]); __quickSort_threeWays(arr, l, lt -1 ); __quickSort_threeWays(arr, gt , r); } void quickSort_threeWays(int arr[],int n) { srand ((unsigned)time (NULL)); __quickSort_threeWays(arr,0 ,n-1 ); }
测试 我们再来看测试(测试用例 50w个):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 * * * * * * * * * * * * * * 有序* * * * * * * * * * * * * sortname:插入排序优化 sorttime: 0.003101s sortname:希尔排序 sorttime: 0.023716s sortname:快速排序随机 sorttime: 0.070738s sortname:快速排序双路 sorttime: 0.057771s sortname:快速排序三路 sorttime: 0.110716s * * * * * * * * * * * * * * 近乎有序* * * * * * * * * * * * * sortname:插入排序优化 sorttime: 0.014255s sortname:希尔排序 sorttime: 0.033605s sortname:快速排序随机 sorttime: 0.067371s sortname:快速排序双路 sorttime: 0.051204s sortname:快速排序三路 sorttime: 0.106871s * * * * * * * * * * * * * * 有大量的重复值* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.054126s sortname:快速排序随机 sorttime: 27.016716s sortname:快速排序双路 sorttime: 0.054265s sortname:快速排序三路 sorttime: 0.014668s * * * * * * * * * * * * * * 随机* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.175923s sortname:快速排序随机 sorttime: 0.099126s sortname:快速排序双路 sorttime: 0.098717s sortname:快速排序三路 sorttime: 0.111747s * * * * * * * * * * * * * * 倒叙* * * * * * * * * * * * * sortname:希尔排序 sorttime: 0.059569s sortname:快速排序随机 sorttime: 0.065489s sortname:快速排序双路 sorttime: 0.047557s sortname:快速排序三路 sorttime: 0.090296s
我们可以看到对 有大量的重复值 排序又快了一点点。当然在其它情况下,三路快速排序的时间都很稳定,所以三路快速排序应当是首选。
结束 至此,快速排序便介绍完了,后面即将介绍 堆排序 。
最后附上Demo