快速排序:
一、快速排序的简介
快速排序是一种总体上来讲时间复杂度较低的排序,其主要利用了分冶的思想;在排序一大段数据时,每次通过选取key值,
然后利用不同的方法将该段数据分为两段(小于等于key的一段在一边,大于key的一段在一边,key的数据在这两段的中间);
然后通过递归的方法分别对上述的左右两段数据采用同样的思想分段;快速排序每一趟下来,位于两段中间的key值就会被置
于最终排序时该数据的正确位置,就是说快速排序每经过一趟排序,排好一个元素;快速排序递归的返回条件是,当某段数据
只剩下一个数据或者没有数据时,那么递归返回;
快速排序排的是一个左右闭合区间;
快排每一趟只将一个数排好,放在正确的位置;
key能选最左边也能选最右边
当一组数为有序时使用快排对这组数排序,此时为快排的最坏情况;时间复杂度最高,为O(N^2);
二。要点
(1)快速排序的实现方法
①递归的方法
左右指针法
挖坑法
前后指针法
②非递归的方法
利用容器适配器实现快速排序的非递归方法
(2)快速排序递归方法的优化
三数取中法
为了避免快速排序的最坏情况的出现,就是快速排序是取key值时取到最大值或者最小值;
利用三数取中法,可以保证key不会取到最大值和最小值;这样就不会出现快排的最坏情况,时间复杂度为O(N^2)的情况;
小区间优化法
快排的递归实现,在递归时会进行函数的压栈开销很大,如果对于一堆很多的数据,完全采用递归的快排进行排序,
那么递归的越深,出现的小区间越多,函数的压栈开销越大,从而让快排的时间复杂度更高,而且,对于小区间的较少数据的排序,
利用快排实际和利用插入排序的时间效率差不多,可能插入排序更高效,所以可以对快排的算法递归到一定的程度的小区间时,
不在利用快排的原理进行排序,而是利用插入排序进行排序,这样就会没有大的函数压栈的时间空间开销,让快排整体来看效率更高;
(3)STL C++标准模板库中sort()算法的底层实现就是利用使用了三数取中和小区间优化后的快速排序实现的;
三、快速排序的详细讲解
(1)左右指针法
算法步骤
1>选取基准值(key)—我选数据集合的最右边的数
2>定义要排序的区间的两端的位置,分别用left和right表示;
3>left从左边第一个数先开始向右寻找大于key的数,直到找到后停止
4>right从右边第一个数开始向左寻找小于key的数,直到找到后停止
5>将left位置处的数据和right中的数据交换;
6>left继续向右找,right继续向左找,直到left和right相遇跳出循环
7>将相遇位置的数据和Key值位置的数据进项交换,一趟快速排序完成,现在key值所呆的位置就是全部排序玩key所呆的位置;
此时key值所在位置的左边全是小于key得数据;key值所在位置的右边全是大于key的数据
8>递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
代码实现:
左右指针法:
int partsort1(int* a,int begin,int end)
{
int& key = a[end];
while(begin < end)
{
while(begin < end && a[begin] <= key)
++begin;
while(begin < end && a[begin] >= key)
--end;
if(begin < end)
swap(a[begin],a[end]);
}
swap(a[begin],key);
return begin;
}
void quicksort(int* a,int left,int right)
{
assert(a);
if(right - left <= 13)
{
InsertSort(a+left,right-left+1);
}
else
{
if(left < right)
return;
int ret = partsort1(a,left,right);
quicksort(a,left,ret-1);
quicksort(a,ret+1,right);
}
}
(2)挖坑法
1.算法实现步骤
1>用两个位置left和right用来标识区间范围,初始坑设置到key值的地方。由于我将key值定义为区间最右边的值,所以要左指针开始走。hollow记录坑的位置;
2>两个位置left和right;首先是left从左边向右边开始找比key坑位置数据大的数据;找到后,将left所在位置的数据填入key坑中;此时left变为坑;
3>然后right从右向左开始寻找比k值小的值,找到以后,直接将此值填入left坑中,这时right有变为坑;
4>重复3,4部操作,直到left和right相遇,一趟快排完成;
此时比key值小的数据全部在key的左边,比key大的值全在key的右边。
5>利用递归把小于基准值key元素的子数列和大于基准值元素的子数列排序。
代码实现:
int partsort2(int* a,int begin,int end)
{
int key = a[end];
while(begin < end)
{
while(begin < end && a[begin] <= key)
begin++;
if(begin < end)
a[end] = a[begin];
while(begin < end && a[end] >= key)
end--;
if(begin < end)
a[begin] = a[end];
}
a[begin] = key;
return begin;
}
6.分析赋值挖坑法和交换挖坑法:
其实赋值挖坑法只是每次将找到的大于或者小于的数赋值给了当前
的坑,这就势必导致在数组中,第一个被赋值的数,也就是key的值,
被前一个数覆盖,在数组中找到不key;也就是说每次赋值完以后,
其实就是当前被赋值的坑保存了下一个坑的值,那么最后一个坑的值会
被他的前一个坑保存,所以,在left和right相遇的坑即最后一个坑
的值会在数组中出现两个,所以要把这个数赋值为key;
那么交换赋值法,是每次把找到的数和坑值交换,这就保证数组
中不会多数也不会少数,每次更新的坑都保存key,最后不用那
key赋值给最后的坑;
(3)前后指针法
1.算法步骤:
1>在left到right的全闭合区间里,取cur为left;prev为cur的前一个位置;取right位置的值为key值;
2>cur从左向右寻找比key小的值,找到以后停下来,然后将prev++,此后,将prev处的值和cur处的值交换;
3>重复2步骤;直到cur走到最后一个位置;此时将prev++,然后将prev位置处的值和cur位置处的值交换;至此一趟快排完成;
4>利用递归,将key只左右的大数据区间和小数据区间进行排序;
int partsort3(int* a,int begin,int end)
{ //默认可以就是a[end]。
int prev = begin-1,cur = begin;
while(cur < end)
{
if(a[cur] < a[end] && ++prev != cur)
swap(a[cur],a[prev]);
++cur;
}
swap(a[end],a[++prev]);
return prev;
}
优化:
(1)三数取中法
每次取待排序区间的左端,右端,中间,这三个数中的中间大小数
作为key值;
当我们取得基准值key为要排序区间的最大值或者最小值的时候,
这时就是快排的最坏情况,时间复杂度最大位O(N^2);这种情况最明
显的例子就是要排序的区间本身就是一个有序区间时,此时该区间
的左右两端的值就位最大值和最小值;
那么即便不是有序区间,我们在取key值时,也有可能取到最大或者
最小值;
那么如何避免取到的key值为最大值或者最小值呢?方法就是三数区
中法,
三数取中法保证了所取得key值不是最大也不是最小,避免了快排最
坏情况的出现,当数据有序时,利用三数取中法,可以将原来的这
种情况为快排的最坏情况变为快排的最好情况,因为,这时,利用
三数取中,每次取到的key值都是该待排序有序区间的中间的数,
没趟都不用交换,且递归的次数会少一点;这样时间复杂度将达到
最低;
代码实现:
int MidGet(int* a,int begin,int end)
{
int mid = begin + ((end-begin)>>1);
if(a[begin] < a[mid])
{
if(a[mid] < a[end])
return mid;
else if(a[begin] > a[end])
return begin;
else
return end;
}else{
if(a[mid] > a[end])
return mid;
else if(a[begin] < a[end])
return begin;
else
return end;
}
}
(2)小区间优化法
我们知道,递归代码虽然看起来比较简单,但是递归时的函数进行
压栈的开销是比较大的,效率很低,所以,我们可以对排序进行
优化:如果区间比较小时,我们可以采用插入排序。下边给出代码
实现
QuickSort(int* arr,int left ,int right)
{
//由于递归太深会导致栈溢出,效率低,
//所以,区间比较小时(小于13)采用插入排序。
if (right-left>13)
{
//三种快排的有效函数体
}
else//要排的区间小于13,则使用选择排序进行排序;
InsertSort(a+begin,end-begin + 1);
}
//非递归实现
#include<stack>
void quicksortNonR(int* a,int left,int right)
{
stack<int> s;
s.push(a[right]); //压栈的顺序,先右后左
s.push(a[left]);
while(!s.empty())
{
int begin = s.top();
s.pop();
int end = s.top();
s.pop();
int div = partsort2(a,begin,end);
if(begin < div-1)
{
s.push(div-1);
s.push(begin);
}
if(div+1 < end)
{
s.push(end);
s.push(div+1);
}
}
}
六:递归快排的时间复杂度
(1)一般情况(含最优情况):
快速排序的时间复杂度取决于它的递归的深度;一般情况下为O(N*logN)
(2)最坏情况:
当基准值key取到待排序区间的最小值或者最大值时(其中最典型也是一定出现最坏情况的例子就是待排序区间的数据为有序数据),就会出现最坏情况;
假设有N个数据,这样总共需要递归N-1次;
递归总共需要比较的次数为:
(N-1)+(N-2)+(N-3)+…+1=N(N-1)/2
每层比较除去自身和前一层已经排序好的元素
所以最坏情况的时间复杂度为O(N^2);
算法的时间复杂度一般说的是最坏情况的时间复杂度。
然而,有例外:
有时时间复杂度并不看最坏情况,而看最好情况,比如哈希表的查找(哈希冲突出现的概率很小),快速排序(有相应的优化机制)。