快速排序的整体思想是分治的思想,也就是说首先对一个大区间进行单趟排序,然后将大区间又进行分割,然后再进行单趟排序,直到这个区间剩下单个元素就不再分割了,小区间有序了返到上一层大区间就有序了,也就是递归思想。
下面是一个快排的动图:
一.根据以上分析,单趟排序是将复杂问题简单化的关键,下面就来分析一下单趟排序的几种方法:
1.左右指针法
步骤可分为:
(1)选择一个关键字,可通过三数取中法来对其进行优化,避免每次取得最大或最小数,从而降低了效率;
(2)给定下标left,right相当于左右两个指针,维护这段区间;左边找一个比关键字大的数,右边找比关键字小的数,然后进行交换,直到left与right指向同一位置,再将关键字换到left或right指向的这个位置,即满足关键字的左区间元素小于关键字,其右区间元素大于关键字;
(3)区间分割再重复上述两个步骤。
//左右指针法
int PartSort1(int* a,int begin,int end)
{
int left=begin;
int right=end;
//用三数取中法来对其进行优化,避免每次取得最大或最小数
int mid=GetMidOfThree(a,begin,end);
std::swap(a[mid],a[end]);
int key=a[end];
while(left < right)
{
while(left<right && a[left]<=key)
{
++left;
}
while(left<right && a[right]>=key)
{
--right;
}
if(left < right)
{
std::swap(a[left],a[right]);
}
}
//left==right
std::swap(a[left],a[end]);
return left;
}
它的思想见下图分析:
//挖坑法
int PartSort2(int* a,int begin,int end)
{
int left=begin;
int right=end;
int mid=GetMidOfThree(a,begin,end);
std::swap(a[mid],a[end]);
int tmp=a[end];
while(left < right)
{
while(left < right && a[left]<=tmp)
{
++left;
}
a[right]=a[left]; //左边找比tmp大的,右边找比它小的
while(left <right && a[right]>=tmp)
{
--right;
}
a[left]=a[right];
}
//此时left=right
a[left]=tmp;
return left;
}
3.前后指针法
分析入下图,这里需要明确一点,prev指针不能定义为-1,因为begin代表的是一段区间的开始,prev起初指向的是cur的前一位置,cur的作用是找一个比关键字小的,找到后prev++,然后将cur和prev指向的元素进行交换,当cur指向end时,将prev指向的下一个值与end指向的这个值进行较换,如此便可完成单趟排序,具体分析过程如下:
//前后指针法
int PartSort3(int* a,int begin,int end)
{
int prev=begin-1;
int cur=begin;
int mid=GetMidOfThree(a,begin,end);
std::swap(a[mid],a[end]);
int key=a[end];
while(cur<end)
{
//只有a[cur]<key时,prev才往后走
if(a[cur]<key)
{
++prev;
if(prev!=cur)
std::swap(a[cur],a[prev]);
}
++cur;
}
++prev;
std::swap(a[prev],a[end]);
return prev;
}
二.快排的两种算法
1.递归快排
其思想就是先对整个区间进行单趟排序,然后不断分割,使得各个子区间都有序,实现如下:
void QuickSort(int* a,int begin,int end)
{
assert(a);
//int div=PartSort1(a,begin,end);
//int div=PartSort2(a,begin,end);
int div=PartSort3(a,begin,end);
if(div-1 > begin)
QuickSort(a,begin,div-1);
if(div+1 < end)
QuickSort(a,div+1,end);
}
2.非递归快排
递归其实就是不断压栈,然后满足递归结束条件后又层层往上返的过程,那么我们同样也可以用栈的方式来模拟递归的过程,如下实现:
//非递归快速排序
void QuickSortNonR(int* a,int begin,int end)
{
stack<int> s;
s.push(end);
s.push(begin);
while(!s.empty())
{
int left=s.top();
s.pop();
int right=s.top();
s.pop();
//int div=PartSort1(a,left,right);
//int div=PartSort2(a,left,right);
int div=PartSort3(a,left,right);
if(left < div-1)
{
s.push(div-1);
s.push(left);
}
if(div+1 < right)
{
s.push(right);
s.push(div+1);
}
}
}
三.快排的两种优化
1.前面有提到三数取中法,这种方式用来对快排进行优化还是挺常见的,它的目的就是避免出现我们选出的关键字是最大值或最小值,可以反过来思考如果我们所选关键字都是最大的或最小的,那么一趟快速排序就变成了一趟冒泡排序,从而效率就降低了。每次取得一个中间值,可使快排的时间复杂度尽量接近O(N*lgN).
//三数取中,可避免每次取到的数是最大的或最小的,如果取到关键字为中间值则可提高效率
//使比它小的尽快排到前面去,使比它大的尽快的排到后面去
int GetMidOfThree(int* a,int begin,int end)
{
//注意符号优先级问题
int mid=begin+((end-begin)>>1);
if(a[begin]<a[mid])
{
if(a[mid] < a[end]) //3 4 5
return mid;
else if(a[mid] > a[end]) //3 5 4
return end;
else
return begin;
}
else //a[begin]>a[mid]
{
if(a[mid]<a[end]) //4 3 5
return begin;
else if(a[mid]>a[end]) //4 3 2
return mid;
else
return end;
}
}
2.减少递归栈使用的优化,快速排序的实现需要消耗递归栈的空间,而大多数情况下都会通过使用系统递归栈来完成递归求解。对系统栈的频繁存取会影响到排序的效率。
快速排序对于小规模的数据集性能不是很好,没有插入性能高。
快速排序算法使用了分治技术,最终来说大的数据集都要分为小的数据集来进行处理。
当数据集较小时,不必继续递归调用快速排序算法,使用插入排序代替快速排序。STL中sort就是用的快排+插入排序的,使得最坏情况下的时间复杂度也是O(N*lgN).这一改进被证明比持续使用快速排序算法要有效的多。当划分的子序列很小的时候,一般认为小于13个元素的时候,插入排序优于快排.因为快速排序对数组进行划分就像一棵二叉树一样,当序列个数小于13的时候,再使用快排的话就相当于增加了二叉树最后几层的节点数目,也就增加了递归的次数,所以我们在当子序列小于13个元素的时候采取直接插入来对这些子序列进行排序。
void QuickSort(int* a,int begin,int end)
{
assert(a);
if(end-begin >13)
{
//int div=PartSort1(a,begin,end);
//int div=PartSort2(a,begin,end);
int div=PartSort3(a,begin,end);
if(div-1 > begin)
QuickSort(a,begin,div-1);
if(div+1 < end)
QuickSort(a,div+1,end);
}
else
{
InsertSort(a+begin,end-begin+1); //元素个数需注意
}
}
四.快排的性能分析: