快速排序
//部分参考维基百科 https://zh.wikipedia.org/wiki/
目录
基本介绍
在平均状况下,排序个项目要次比较。在最坏状况下则需要次比较,但这种状况并不常见。事实上,快速排序通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成;快速排序是不稳定的
使用的思想是分治法,分成两组,递归的解决
最坏情况下:
出现在输入完全逆序的情况下
每次划分产生的两组分别包含n-1个和0个元素,整个的运行时间递归式可以表示为:
T(n) = T(n-1)+θ(n),也就是O(n^2)级别
最好情况划分:
最平衡的划分中,Partition得到的两个子问题的规模都不大于n/2,这种情况下快速排序的性能非常好
T(n)=2*T(n/2)+θ(n),也即是θ(nlogn)
快速排序的平均运行时间更接近于其最好情况,基本上只要划分是常数比例的,算法的运行时间总是O(nlogn)
整体的思路
1、从待排序数列中选择一个基准值(key)
2、重新排列数组,使得比key小的元素在key的左边,比key大的元素在key右边,这个操作称为分割(partition)
3、递归的把小于key的部分数列和大于key的部分数列进行重排
递归到最底部时,数组的长度为0或1,该算法一定会结束,因为至少会把一个元素摆到后面的位置去
C++中的 sort函数就是实现的快排算法
代码实现
下面给出一种原址排序的C++实现方法:
p是第一个下标位置,r是最后一个数字的下标位置
在Partition中,以最后一个元素为key值进行划分,在剩下的元素中找出比k小的元素,交换到左边 --- 以i记录比key小的最后一个元素的下标,j去找等待被交换回来的小于k的元素的下标
关键是记住i的初始化,先更新i再交换,以及最后返回的key在第i+1位置上
int Partition(int p, int r)
{
int k = a[r];
int i = p - 1;
for (int j = p; j < r; j++)
{
if (a[j] <= k)
{
i++;
swap(a[i], a[j]);
}
}
swap(a[i + 1], a[r]);
return i + 1;
}
void quickSort(int p, int r)
{
if (p < r)
{
int q = Partition(p, r);
quickSort(p, q - 1);
quickSort(q + 1, r);
}
}
另一种方法的区别是Partition部分,采用“挖坑填坑”的思想,用i,j分别从两边找,以最后一个为k的值的话,i从前往后找比k大的,j从后往前找比k小的,直到i==j为止
int Partition2(int p, int r)
{
int k = a[r];
int i = p, j = r;
while (i<j)
{
while (i<j && a[i]<k)
{
i++;
}
if (i < j) { a[j] = a[i]; j--; }
while (i<j && a[j]>k)
{
j--;
}
if (i < j) { a[i] = a[j]; i++; }
}
a[i] = k;
return i;
}
第K大数字:
直接复用上面的Partition代码就可以
有几点要注意:返回的索引q在当前数组里是第key大的,不是直接比较q与k; 另外在k>key时,不在求的是第k大,而是第k-key大;
还有递归出口是p==r的情况
int selectKBiggest(int p, int r, int k)
{
if (p == r)return a[p];
int q = Partition(p, r);
int key = q - p+1 ;
if (k == key)return a[q];
else if (k < key)
return selectKBiggest(p, q - 1, k);
else return selectKBiggest(q + 1, r, k-key);
}
非递归版本:
int selectKUnRec(int p, int r, int k)
{
while (p <= r)
{
int q = Partition(p, r);
int key = q - p + 1;
if (k == key)return a[q];
else if (k < key)r = q-1;
else { p = q + 1; k = k - key; }
}
}
只需要一个while就可以了,该上下下标和k值就好了
最坏情况优化:
分治方法,分成的两个数组差不多大时,效率是最高的,所以基准的选择很重要;前面都是选择了固定的最后一个元素作为划分的基准;
在此基础上稍作改进可以是随机的选择基准值,能一定程度提高,但是最坏的情况下还是O(n^2);
进一步的改进是选用三数取中的方法:
最佳的取值应当是待排序数组分成相等的两组,选择中间那个数;但是要精确的找到中间那个数是很费时的,所以可以从三个数中选择中间那个值,实际上选择首、尾和中间值,找到这三个数中中间位置的数作为key即可,这样可以消除预输入不好的情况;当然也可以由三数取中换成五数取中等
但是三数取中优化不了重复数组的情况
在这上面的进一步优化的考虑可以是,在划分到一定程度后,用插入排序来替代继续划分,因为当数组部分有序时,快排没有插入排序效率高;
if (high - low + 1 < 10)
{
InsertSort(arr,low,high);
return;
}//else时,正常执行快排
或者在一次划分结束后,将与key相等的元素聚集到一起,划分时不对其进行划分: 可以在划分时,将与key相等的数移动到两端,然后划分结束后再移回中轴附近
还有一种优化思路是,在QuickSort中有两次递归,可以改成一次尾递归:
void QSort(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;
}
}
---------------------
作者:insistgogo
来源:CSDN
原文:https://blog.csdn.net/insistGoGo/article/details/7785038
版权声明:本文为博主原创文章,转载请附上博文链接!
上面的博主实验结果:这里效率最好的快排组合 是:三数取中+插排+聚集相等元素,它和STL中的Sort函数效率差不多
与堆排序、归并排序的比较
平均时间复杂度 | 最好 | 最坏 | 辅助空间 | 稳定性 | |
---|---|---|---|---|---|
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | 看实现 | 不稳定 |