三种快速排序以及快速排序的优化:
一:快速排序的基本思想
快排使用分治的思想: | 通过一趟排序将待排序序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。 |
二:快速排序的三个步骤
1.选择基准 | 在待排序列中,按照某种方式挑出一个元素,作为“基准”(pivot) |
2.分割操作 | 以该基准在序列中的位置,把序列分成两个子序列,此时基准左边的比基准小,基准右边的都比基准大 |
3.递归操作 | 递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。 |
三:选择基准的方式
对于分治算法,当每次划分时,算法若能都划分成两个子序列时,那么分治算法效率会达到最大。也就是说,基准的选择将会决定算法的效率。选择基准的方式决定了两个分割后子序列的长度,进而对整个算法的效率产生决定性的影响。
最理想的方法:选择基准恰好能把待排序序列分成两个等长的子序列。
下面我们将介绍3种选择基准的方法:
方法(1):固定位置(基础)
①思想:取序列的第一个或最后一个元素作为基准。基本快速排序
int SelectPivot(int arr[] , int low , int high)
{
return arr[low];//选择序列的第一个元素作为基准。
//return arr[high];
}
//注意:基本快排选取第一个或者最后一个元素作为基准。
②测试数据:
③测试数据分析:
如果输入的序列是随机的,处理时间可以接受。
但如果数组已经有序了,此时的分割方法是一个非常不好的分割,因为每次分割只能使待排序序列减一,此时为最坏的情况,导致快速排序沦为冒泡排序。时间复杂度为O(n^2)。
因此,使用第一个元素作为枢纽元素是非常糟糕的,为了避免这种情况,就引入了下面两个获取基准的方法。
方法(2):随机选取基准
①思想:随机取带排序元素中的元素作为基准。
/*随机选择枢轴的位置,区间在low和high之间*/
int SelectPivotRandom(int arr[],int low , int high)
{
srand((unsigned)time(NULL));
int pivotPos = rand()%(high - low) + low;
//把枢轴位置的元素和low位置的元素互换,此时可以和普通的快排一样调用换分函数
swap(arr[pivotPos],arr[low]);
return arr[low];
}
②测试数据:
③:测试数据分析
这是一种相对安全的策略。由于枢轴的位置是随机的,那么产生的分割也不会总是出现劣质分割。
但是在整个数组数字全相等时,认识最坏的情况,时间复杂度为O(n^2)。实际上:随机化快排得到的理论上最坏的情况可能性仅仅为
1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。
一位前辈做出了一个精辟的总结:“随机化快速排序可以满足一个人一辈子的人品需求。”
方法(3):三数取中(median-of-three)(优化有序的数据)
引入的原因:虽然随机选取枢轴时,减少出现不好分割的几率,但是最坏的情况下还是O(n^2),要缓解这种情况,就引入了三数取中的选取枢轴。
①:具体思想
对待排序序列中low,mid,high三个位置上数据进行排序,取他们中间的那个数据作为枢轴,并且用0下标元素储存枢轴。
即:三数取中,并且0下标元素储存枢轴。
/*函数作用:取待排序序列中low,mid,high三个位置上数据,选取他们中间的那个数据作为枢轴*/
int SelectPivotMedianOfThree( int arr[],int low,int high)
{
int mid = low + ((low + high)>>1);//计算数组中间元素的下标。
if(arr[mid] > arr[high])//目标:arr[mid] <= arr[high]
{
swap(arr[mid] , arr[high]);
}
if(arr[low] > arr[high])
{
swap(arr[low] , arr[high]);
}
if(arr[mid] > arr[low])
{
swap(arr[mid] , arr[low]);
}
//此时,arr[mid]<=arr[low]<=arr[high]
return arr[low];
//low位置上保存这三个位置中间的值,分割时可以直接使用low位置的元素作为枢轴,而不改用分割函数了
}
②:测试数据
③:测试数据分析:使用三数取中选择枢轴的优势还是很明显的,但是还是处理不了重复数组
优化:
优化1:对于很小和部分有序的数组。快排不如插入排序好。当待排序序列的长度分割到一定大小之后,继续分割的效率比插入排序要差。此时可以使用插排而不是快排。
截至范围:待排序序列长度N = 10.虽然在2 ~ 20之间任意截至范围都有可能产生类似大的结果,这种做法也避免了一些有害的退化情形。摘自《数据结构与算法分析》Mark Allen Weiness著。
if(high - low + 1 < 10)
{
insertSort(arr,low,high);
return ;
}
//else正常执行快排
②:测试数据
③:测试数据分析
针对随即数组,使用三数取中选择枢轴+插排,效率还是可以提高一点。
但是针对已排序数组,是没有作用的。因为待排序序列是已经有序的,那么每次划分只能使得待排序序列减一。此时插入排序是起不了任何作用的,所以这里看不到任何的时间减少。
同时该方法对于重复数组还是没有任何的办法。
优化2:再一次分割结束后,可以把与key相等的元素聚在一起。继续下次分割时,不再用对于key相等元素分割。
①:具体的处理过程
第一步: | 再划分过程中,把与key相等元素放入数组的两端。 |
第二步: | 划分结束后,把与key相等的元素移到枢轴周围。 |
举例:
待排序序列:1 4 6 7 6 6 7 6 8 6 |
三数取中选取枢轴:下标为4 的数 6 |
转化后待分割序列:6 4 6 7 1 6 7 6 8 6 枢轴key: 6 |
第一步:再划分过程中,把与key相同的元素放入数组的两端 结果为: 6 4 1 6(枢轴) 7 8 7 6 6 6 。此时与6相等的元素全部放入两端 |
第二步:划分结束后,把与key相等的元素移到枢轴周围。结果:1 4 6 6(枢轴) 6 6 6 7 8 7.此时与6相等的元素全移到枢轴周围了。 |
之后,在1 4 和 7 8 7两个子序列中进行快排。 |
void QSort(int arr[],int low,int high)
{
int first = low;
int lase = high;
int left = low;
int right = high;
int leftlen = 0;
int rightlen = 0;
if(high - low +1 < 10)
{
InsertSort(arr,low,high);
return;
}
//一次分割;
int key = SelectPivotMedianOfThree(arr,low,high);//使用三数取中选择枢轴
while(low < high)
{
while(high < low && arr[high] >= key)
{
if(arr[high] == key)//处理相等元素
{
swap(arr[right],arr[high]);
right--;
rightlen++;
}
high--;
}
arr[low] = arr[high];
while(high > low && arr[low] <= key)
{
if(arr[low] == key)
{
swap(arr[left],arr[low]);
left++;
leftlen++;
}
low++
}
arr[high] = arr[low];
}
arr[low] = ley;
//一次排序结束
//把与枢轴key相同的元素移到枢轴最终位置周围
int i = low - 1;
int j = first;
while(j < left && arr[i] != key)
{
swap(arr[i],arr[j]);
i--;
j++;
}
i = low +1;
j = last;
while(j > right && arr[i] != key)
{
swap(arr[i],arr[j]);
i++;
j--;
}
QSort(arr,first,low-1-leftlen);
QSort(arr,low + 1 + rightlen,last);
}
②:测试数据
③:测试数据分析:三数取中选择枢轴+插排+聚集相等元素的组合竟然效果好的 出奇。
原因:在数组中,如果有相等的元素,那么减少不少冗余的划分。这点再重复数组中的体现特别明显。(这里的插排的作用还是不明显)。
优化3:优化递归操作
①:思想:快排函数在函数末尾有两次递归操作,我们可以对其使用尾递归优化。
②优点:如果待排序的序列划分的极端不平衡,递归的深度将趋近于n,而栈的大小有限,每次递归调用都会耗费一定 的占空间,函数参数越多,每次递归耗用的空间也就越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(log n)
void Qsrot(int arr[] , int low , int high)
{
int pivotPos = -1;
if(high - low + 1 < 10)
{
InsertSort(arr , low , high);
return;
}
while(low < high)
{
pivotPos = Partition(arr,low,high);
QSort(arr , low , pivot - 1);
low = pivot + 1;
}
}
注意:第一次递归后,low就没用了;此时第二次递归可以使用循环代替。
②测试数据
③:测试分析:其实这种优化编译器会自己优化,相比于不用该优化方法,时间几乎没少。
优化4:使用并行或多线程处理子程序(不详细解释)
所有的数据测试:
概括:这里效率最好的快排组合 是:三数取中 + 快排 + 聚合相等元素。他和STL中的Sort函数的效率差不多。