思想
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
(1) 分治法的基本思想
分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。
(2)快速排序的基本思想
设当前待排序的无序区为R[low..high],利用分治法可将快速排序的基本思想描述为:
①分解:
在R[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)和R[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。
注意:
划分的关键是要求出基准记录所在的位置pivotpos。划分的结果可以简单地表示为(注意pivot=R[pivotpos]):
R[low..pivotpos-1].keys≤R[pivotpos].key≤R[pivotpos+1..high].keys
其中low≤pivotpos≤high。
②求解:
通过递归调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。
③组合:
因为当"求解"步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,"组合"步骤无须做什么,可看作是空操作。
2、快速排序算法QuickSort
(1)递归算法
注:下面的算法是经过了很多的考虑之后达到了一个非常完美的解决方案,比很多书上写的要完整,主要是考虑了数组越界的情况
template<class T>
void QuickSort(T a[],int left,int right)
{
if (left >= right)
return;
int l = left+1,
r = right;
int pivot = a[left];
while (l < r)
{
while (l < r&&a[l] <= pivot)//为防止数组越界,必须加上限制条件l < r
++l;
while (r > left&&a[r] >= pivot)//同上防止数组越界,须加上限制条件r>left
--r;
if (l < r)
{
T tmp = a[l];
a[l] = a[r];
a[r] = tmp;
}
}
if (a[r] < pivot)//此条件很重要,当l和r相等时,我们需要判断是否需要交换
{
a[left] = a[r];
a[r] = pivot;
}
QuickSort(a,left,r-1);
QuickSort(a,r+1,right);
}
(2)非递归的解决方案
template<class T>
struct sPoint
{
T* p;
int index;
}
template<class T>
void QuickSort2(T a[],int n)
{
sPoint<T> sp;
stack<sPoint<T> > ss;
while (1)
{
if (2 == n)
{
if (a[0] > a[1])
Swap(a[0],a[1]);
if (ss.empty())
break;
sp = ss.top();
ss.pop();
a = sp.p;
n = sp.index;
continue;
}
T* l = a+1;
T* r = a+n-1;
while (l < r)
{
while (l < r&&*r <= *a)
++l;
while (r > a&&*r >= *a)
--r;
if (l < r)
Swap(*l,*r);
}
if (*r < *a)
Swap(*r,*a);
sp.p = r+1;
sp.index = n-(r-a+1);
ss.push(sp);
n = r-a;
}
}
3、算法分析
快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。
(1)最坏时间复杂度
最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。
因此,快速排序必须做n-1次划分,第i次划分开始时区间长度为n-i+1,所需的比较次数为n-i(1≤i≤n-1),故总的比较次数达到最大值:
Cmax = n(n-1)/2=O(n2)
如果按上面给出的划分算法,每次取当前无序区的第1个记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多。
(2) 最好时间复杂度
在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:
0(nlgn)
注意:
用递归树来分析最好情况下的比较次数更简单。因为每次划分后左、右子区间长度大致相等,故递归树的高度为O(lgn),而递归树每一层上各结点所对应的划分过程中所需要的关键字比较次数总和不超过n,故整个排序过程所需要的关键字比较总次数C(n)=O(nlgn)。
因为快速排序的记录移动次数不大于比较的次数,所以快速排序的最坏时间复杂度应为0(n2),最好时间复杂度为O(nlgn)。
(3)基准关键字的选取
在当前无序区中选取划分的基准关键字是决定算法性能的关键。
①"三者取中"的规则
"三者取中"规则,即在当前区间里,将该区间首、尾和中间位置上的关键字比较,取三者之中值所对应的记录作为基准,在划分开始前将该基准记录和该区伺的第1个记录进行交换,此后的划分过程与上面所给的Partition算法完全相同。
②取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准
选取基准最好的方法是用一个随机函数产生一个取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准,这相当于强迫R[low..high]中的记录是随机分布的。用此方法所得到的快速排序一般称为随机的快速排序算法。
注意:
随机化的快速排序与一般的快速排序算法差别很小。但随机化后,算法的性能大大地提高了,尤其是对初始有序的文件,一般不可能导致最坏情况的发生。算法的随机化不仅仅适用于快速排序,也适用于其它需要数据随机分布的算法。
(4)平均时间复杂度
尽管快速排序的最坏时间为O(n2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。它的平均时间复杂度为O(nlgn)。
(5)空间复杂度
快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为O(lgn),故递归后需栈空间为O(lgn)。最坏情况下,递归树的高度为O(n),所需的栈空间为O(n)。
(6)稳定性
快速排序是非稳定的。