之前的一篇文章对快速排序进行了初步优化,减少算法本身的数据比较和交换次数。另外,还可能会遇到这样的情况,即快速排序在数据已经基本有序的情况下会得到最差的时间复杂度O(n^2),要避免这种情况,从以下几个方面着手,继续对算法进行优化:
1.在对数组元素进行排序前,选择一种随机化算法堆数组的元素进行随机化处理。尽量选择能够在线性时间复杂度上对数组进行随机化的方法。这样就可以在虽大程度上避免排序算法的性能出现最差的情况。(感觉实用性不大,平白增加了时间开销,未测试效果)
2.选择枢轴元素的时候采用随机的方法。注意,
(1)取随机数的时候,要给随机数产生函数一个种子,一般取系统当时的时间即可。而且为了增加效率,srand((unsigned)time(NULL))只在主函数中执行一次就可以了。
(2)以C++语言为例,系统时间要包含time.h这个头文件,虽然sys/time.h这个头文件也可以使程序正常工作,但是从测试结果来看,后者使程序的执行时间大大增加,大概增加到20倍左右。
(3)本文的源代码中虽然这两个文件头文件都有,但是程序实际上应该是调用了time.h中的相关函数。本程序中的sys/time.h实际上是为getCurrentTime()函数服务的。
3.选择枢轴的时候采用首、尾、中三数取中的方法。
4.当需要排序的序列大小降低到一定程度的时候,使用其他简单排序的算法,以避免不断的递归创建函数栈等开销。在这里当需要排序的序列长度k=25时,使用插入法进行排序。对于k值的确定,请看第5小节的测试。
不同的排序策略对排序效率的影响(三数取中算法采用实现方式一,见后文):
数据类型 | 随机数据1 | 随机数据2 | 随机数据3 | 随机数据4 | 正序数据 | 倒序数据 | 相等数据 |
---|---|---|---|---|---|---|---|
固定枢轴 | 280 | 281 | 280 | 281 | —— | —— | 169 |
随机枢轴 | 307 | 305 | 301 | 307 | 168 | 180 | 193 |
三数取中 | 305 | 303 | 303 | 306 | 104 | 255461 | 178 |
固定枢轴+插入排序 | 289 | 291 | 293 | 291 | —— | —— | 145 |
随机枢轴+插入排序 | 265 | 267 | 267 | 267 | 108 | 128 | 149 |
三数取中+插入排序 | 269 | 268 | 268 | 273 | 81 | 255589 | 148 |
不同的排序策略对排序效率的影响(三数取中算法采用实现方式二,见后文):
数据类型 | 随机数据1 | 随机数据2 | 随机数据3 | 随机数据4 | 正序数据 | 倒序数据 | 相等数据 |
---|---|---|---|---|---|---|---|
固定枢轴 | 299 | 301 | 301 | 300 | —— | —— | 180 |
随机枢轴 | 315 | 321 | 325 | 323 | 185 | 184 | 213 |
三数取中 | 285 | 285 | 286 | 288 | 125 | 144 | 185 |
固定枢轴+插入排序 | 274 | 277 | 276 | 276 | —— | —— | 152 |
随机枢轴+插入排序 | 274 | 272 | 269 | 272 | 115 | 139 | 155 |
三数取中+插入排序 | 297 | 288 | 289 | 292 | 88 | 119 | 153 |
快速排序核心代码:
//快速排序1,最左边为枢轴
void quick_sort_1(int a[], int left, int right){
if(left < right){
int pivot = a[left];
int i = left, j = right;
while(i < j){
while(i < j && a[j] > pivot){
j--;
}
if(i < j){
a[i] = a[j];
i++;
}
while(i < j && a[i] < pivot){
i++;
}
if(i < j){
a[j] = a[i];
j--;
}
}
a[i] = pivot;
quick_sort_1(a, left, i - 1);
quick_sort_1(a, i + 1, right);
}
}
以上核心源代码中需要注意的细节:
(1)第7行中,用的是a[j] > pivot,而不是a[j] >= pivot。虽然可能会因此增加数据的移动量,但是对于所有的数据都想等这种极端状况下,仍然能够保证O(nlogn)的时间复杂度。经过对100万随机数据进行测试,发现性能下降不是很明显。第14行也类似。
(2)第12行和第19行。该元素处理完毕后,即可让游标移到下一个相邻的位置,否则while循环会额外进行一次判定。并且对于本程序第7行代码的比较大小(即使用>而非>=)的方式而言,游标的这种移动方式是必须的,否则的话游标会停滞不前,永远没有机会移动从而造成死循环。
(3)对于本程序(见后面)中的SelectPivotMid()函数,有两种实现方式:
实现方式一运用了看似较为复杂的比较方式。相对于数据交换来说,进行比较所花费的时间要少很多。对于函数SelectPivotMid()实现方式一来说,虽然看似比较次数较多,但是对于任何一个实际的过程,最多只会进行三次比较和一次交换,而实现方式二最多会进行三次比较和三次交换。在时间效率上应该是实现方式一略优。但在实际的执行过程中,实现方式一对于倒序数组排序时间不知为什么非常大,尚未找到原因。实现方式二一切正常。方式一已找到原因,参见:快速排序中枢轴元素从首、尾、中间三个元素取中间值的算法探究
SelectPivotMid()算法实现一:
void SelectPivotMid(int a[], int left, int right){
int pivotPos = left;
int mid = (right + left)/2;
if(a[mid] <= a[left]){
if(a[left] <= a[right]){
pivotPos = left;
}else{
if(a[mid] <= a[right]){
pivotPos = right;
}else{
pivotPos = mid;
}
}
}else{
if(a[mid] <= a[right]){
pivotPos = mid;
}else{
if(a[left] <= a[right]){
pivotPos = right;
}else{
pivotPos = left;
}
}
}
int tmp = a[pivotPos];
a[pivotPos] = a[left];
a[left] = tmp;
}
SelectPivotMid()算法实现二:
void SelectPivotMid_2(int arr[],int low,int high)
{
int mid = low + ((high - low) >> 1);//计算数组中间的元素的下标
//使用三数取中法选择枢轴
if (arr[mid] > arr[high])//目标: arr[mid] <= arr[high]
{
swap(arr[mid],arr[high]);
}
if (arr[low] > arr[high])//目标: arr[low] <= arr[high]
{
swap(arr[low],arr[high]);
}
if (arr[mid] > arr[low]) //目标: arr[low] >= arr[mid]
{
swap(arr[mid],arr[low]);
}
//此时,arr[mid] <= arr[low] <= arr[high]
//return arr[low];
//low的位置上保存这三个位置中间的值
//分割时可以直接使用low位置的元素作为枢轴,而不用改变分割函数了
}
5.K取值的优化实验:确定一个最佳值,保证快速排序和插入排序的结果最优,下面的实验:
优化前:固定枢轴元素
优化后:固定枢轴元素 + 插入排序
数据条件:1000万个随机产生的整数,进行排序,当快速排序规模大于k值时利用快速排序,否则利用插入排序
试验结果:k值在20~25附近时效果最好,优化幅度在5%左右。
K | 优化前(ms) | 优化后(ms) | 优化幅度(%) |
---|---|---|---|
50 | 3384 | 3415 | -0.9 |
40 | 3367 | 3399 | -1 |
30 | 3362 | 3224 | 4.1 |
30 | 3355 | 3226 | 3.8 |
25 | 3359 | 3195 | 4.9 |
25 | 3366 | 3195 | 5.1 |
20 | 3360 | 3189 | 5.1 |
20 | 3376 | 3195 | 5.4 |
15 | 3361 | 3202 | 4.7 |
15 | 3357 | 3194 | 4.9 |
10 | 3357 | 3244 | 3.4 |
10 | 3361 | 3253 | 3.2 |
8 | 3368 | 3297 | 2.1 |
5 | 3360 | 3354 | 0.2 |
以下是本算法所有程序的完整源代码:
#include <iostream>
#include <fstream>
#include <string>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>
#define MAX 1000000
using namespace std;
int flag = 25;
void readNum(int a[]){
string filename;
ifstream infile("data_100w_4.txt", ios::in);
string textline = "";
int i = 0;
while(getline(infile, textline, '\n')){
a[i] = atoi(string(textline).c_str());
i++;
}
infile.close();
}
void SelectPivotRandom(int a[], int left, int right){
int pivotPos = rand()%(right - left) + left;
int tmp = a[pivotPos];
a[pivotPos] = a[left];
a[left] = tmp;
}
void SelectPivotMid(int a[], int left, int right){
int pivotPos = left;
int mid = (right + left)/2;
if(a[mid] <= a[left]){
if(a[left] <= a[right]){
pivotPos = left;
}else{
if(a[mid] <= a[right]){
pivotPos = right;
}else{
pivotPos = mid;
}
}
}else{
if(a[mid] <= a[right]){
pivotPos = mid;
}else{
if(a[left] <= a[right]){
pivotPos = right;
}else{
pivotPos = left;
}
}
}
int tmp = a[pivotPos];
a[pivotPos] = a[left];
a[left] = tmp;
}
void SelectPivotMid_2(int arr[],int low,int high)
{
int mid = low + ((high - low) >> 1);//计算数组中间的元素的下标
//使用三数取中法选择枢轴
if (arr[mid] > arr[high])//目标: arr[mid] <= arr[high]
{
swap(arr[mid],arr[high]);
}
if (arr[low] > arr[high])//目标: arr[low] <= arr[high]
{
swap(arr[low],arr[high]);
}
if (arr[mid] > arr[low]) //目标: arr[low] >= arr[mid]
{
swap(arr[mid],arr[low]);
}
//此时,arr[mid] <= arr[low] <= arr[high]
//return arr[low];
//low的位置上保存这三个位置中间的值
//分割时可以直接使用low位置的元素作为枢轴,而不用改变分割函数了
}
void insert_sort(int a[],int left, int right){
for(int i = left + 1; i <= right; i++){
int tmp = a[i];
for(int j = i - 1; j >= left; j--){
if(tmp < a[j]){
a[j+1] = a[j];
}
else{
a[j+1] = tmp;
break;
}
}
}
}
//快速排序1,最左边为枢轴
void quick_sort_1(int a[], int left, int right){
if(left < right){
int pivot = a[left];
int i = left, j = right;
while(i < j){
while(i < j && a[j] > pivot){
j--;
}
if(i < j){
a[i] = a[j];
i++;
}
while(i < j && a[i] < pivot){
i++;
}
if(i < j){
a[j] = a[i];
j--;
}
}
a[i] = pivot;
quick_sort_1(a, left, i - 1);
quick_sort_1(a, i + 1, right);
}
}
//快速排序2,随机枢轴
void quick_sort_2(int a[], int left, int right){
if(left < right){
SelectPivotRandom(a, left, right);
int pivot = a[left];
int i = left, j = right;
while(i < j){
while(i < j && a[j] > pivot){
j--;
}
if(i < j){
a[i] = a[j];
i++;
}
while(i < j && a[i] < pivot){
i++;
}
if(i < j){
a[j] = a[i];
j--;
}
}
a[i] = pivot;
quick_sort_2(a, left, i - 1);
quick_sort_2(a, i + 1, right);
}
}
//快速排序3,首、尾、中三数取中为枢轴
void quick_sort_3(int a[], int left, int right){
if(left < right){
SelectPivotMid(a, left, right);
int pivot = a[left];
int i = left, j = right;
while(i < j){
while(i < j && a[j] > pivot){
j--;
}
if(i < j){
a[i] = a[j];
i++;
}
while(i < j && a[i] < pivot){
i++;
}
if(i < j){
a[j] = a[i];
j--;
}
}
a[i] = pivot;
quick_sort_3(a, left, i - 1);
quick_sort_3(a, i + 1, right);
}
}
//快速排序4,固定枢轴 + 插入排序
void quick_sort_4(int a[], int left, int right){
if(left < right){
if(right - left < flag){
insert_sort(a, left, right);
}
else{
int pivot = a[left];
int i = left, j = right;
while(i < j){
while(i < j && a[j] > pivot){
j--;
}
if(i < j){
a[i] = a[j];
i++;
}
while(i < j && a[i] < pivot){
i++;
}
if(i < j){
a[j] = a[i];
j--;
}
}
a[i] = pivot;
quick_sort_4(a, left, i - 1);
quick_sort_4(a, i + 1, right);
}
}
}
//快速排序5,随机枢轴 + 插入排序
void quick_sort_5(int a[], int left, int right){
if(left < right){
if(right - left < flag){
insert_sort(a, left, right);
}
else{
SelectPivotRandom(a, left, right);
int pivot = a[left];
int i = left, j = right;
while(i < j){
while(i < j && a[j] > pivot){
j--;
}
if(i < j){
a[i] = a[j];
i++;
}
while(i < j && a[i] < pivot){
i++;
}
if(i < j){
a[j] = a[i];
j--;
}
}
a[i] = pivot;
quick_sort_5(a, left, i - 1);
quick_sort_5(a, i + 1, right);
}
}
}
//快速排序6,枢轴三者取一 + 插入排序
void quick_sort_6(int a[], int left, int right){
if(left < right){
if(right - left < flag){
insert_sort(a, left, right);
}
else{
SelectPivotMid(a, left, right);
int pivot = a[left];
int i = left, j = right;
while(i < j){
while(i < j && a[j] > pivot){
j--;
}
if(i < j){
a[i] = a[j];
i++;
}
while(i < j && a[i] < pivot){
i++;
}
if(i < j){
a[j] = a[i];
j--;
}
}
a[i] = pivot;
quick_sort_6(a, left, i - 1);
quick_sort_6(a, i + 1, right);
}
}
}
int verify(int a[]){
for(int i = 0; i < MAX - 1; i++){
if(a[i] > a[i+1])
return -1;
}
return 0;
}
long getCurrentTime(){
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
int main()
{
int a[MAX] = {0};
int b[MAX] = {0};
int c[MAX] = {0};
int d[MAX] = {0};
int e[MAX] = {0};
int f[MAX] = {0};
readNum(a);
readNum(b);
readNum(c);
readNum(d);
readNum(e);
readNum(f);
srand((unsigned)time(NULL));
cout << "Number Counts:" << MAX << endl;
cout << "Sort Result:" << endl;
long time_1 = getCurrentTime();
cout << "quick_sort_1 had spend ";
quick_sort_1(a, 0, MAX-1);
long time_2 = getCurrentTime();
cout << time_2 - time_1 << " ms, sort result : ";
cout << (verify(a) == 0 ? "True" : "False") << endl;
long time_3 = getCurrentTime();
cout << "quick_sort_2 had spend ";
quick_sort_2(b, 0, MAX-1);
long time_4 = getCurrentTime();
cout << time_4 - time_3 << " ms, sort result : ";
cout << (verify(b) == 0 ? "True" : "False") << endl;
long time_5 = getCurrentTime();
cout << "quick_sort_3 had spend ";
quick_sort_3(c, 0, MAX-1);
long time_6 = getCurrentTime();
cout << time_6 - time_5 << " ms, sort result : ";
cout << (verify(c) == 0 ? "True" : "False") << endl;
long time_7 = getCurrentTime();
cout << "quick_sort_4 had spend ";
quick_sort_4(d, 0, MAX-1);
long time_8 = getCurrentTime();
cout << time_8 - time_7 << " ms, sort result : ";
cout << (verify(d) == 0 ? "True" : "False") << endl;
long time_9 = getCurrentTime();
cout << "quick_sort_5 had spend ";
quick_sort_5(e, 0, MAX-1);
long time_10 = getCurrentTime();
cout << time_10 - time_9 << " ms, sort result : ";
cout << (verify(e) == 0 ? "True" : "False") << endl;
long time_11 = getCurrentTime();
cout << "quick_sort_6 had spend ";
quick_sort_6(f, 0, MAX-1);
long time_12 = getCurrentTime();
cout << time_12 - time_11 << " ms, sort result : ";
cout << (verify(f) == 0 ? "True" : "False") << endl;
return 1;
}