一、快速排序的简介
快速排序是一种总体上来讲时间复杂度较低的排序,其主要利用了分冶的思想;在排序一大段数据时,每次通过选取key值,然后利用不同的方法将该段数据分为两段(小于等于key的一段在一边,大于key的一段在一边,key的数据在这两段的中间);然后通过递归的方法分别对上述的左右两段数据采用同样的思想分段;快速排序每一趟下来,位于两段中间的key值就会被置于最终排序时该数据的正确位置,就是说快速排序每经过一趟排序,排好一个元素;快速排序递归的返回条件是,当某段数据只剩下一个数据或者没有数据时,那么递归返回;
快速排序排的是一个左右闭合区间;
快排每一趟只将一个数排好,放在正确的位置;
key能选最左边也能选最右边
当一组数为有序时使用快排对这组数排序,此时为快排的最坏情况;时间复杂度最高,为O(N^2);
二、学习快速排序的要点
(1)快速排序的实现方法
①递归的方法
- 左右指针法
- 挖坑法
- 前后指针法
②非递归的方法
利用容器适配器实现快速排序的非递归方法
(2)快速排序递归方法的优化
三数取中法
为了避免快速排序的最坏情况的出现,就是快速排序是取key值时取到最大值或者最小值;
利用三数取中法,可以保证key不会取到最大值和最小值;这样就不会出现快排的最坏情况,时间复杂度为O(N^2)的情况;小区间优化法
快排的递归实现,在递归时会进行函数的压栈开销很大,如果对于一堆很多的数据,完全采用递归的快排进行排序,那么递归的越深,出现的小区间越多,函数的压栈开销越大,从而让快排的时间复杂度更高,而且,对于小区间的较少数据的排序,利用快排实际和利用插入排序的时间效率差不多,可能插入排序更高效,所以可以对快排的算法递归到一定的程度的小区间时,不在利用快排的原理进行排序,而是利用插入排序进行排序,这样就会没有大的函数压栈的时间空间开销,让快排整体来看效率更高;(3)STLc++标准模板库中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)把小于基准值元素的子数列和大于基准值元素的子数列排序。图说
程序实现
#include<iostream>
#include<assert.h>
using namespace std;
void QuickSort1(int* arr,int left,int right)
{
int key=right;
int begin=left;
int end=right;
while (left>=right)//递归返回条件
{
return ;
}
while(left<right)
{
while (left<right&&arr[left]<=arr[key])//从左边开始找到比基准值大的数停下来
{
++left;
}
while (right>left&&arr[right]>=arr[key])//从右边开始找到比基准值小的数停下来
{
--right;
}
if (left<right&&arr[left]!=arr[right])//如果左右指针都停下来了,并且不是同一个值,自身和自身交换,没意义,降低效率
{
swap(arr[left],arr[right]);
}
}
//一趟排序完成,此时left=right,交换相遇点位置的数据和key位置的数据
swap(arr[left],arr[key]);//不论最后一次是谁主动遇见在等的谁,每次最后一次用相遇点和key作交换,都是用大值与key交换;
//递归地把小于基准值元素的子数列和大于基准值元素的子数列排序
QuickSort1(arr,begin,left-1);
QuickSort1(arr,left+1,end);
}
void Printf(int* a,int sz)
{
assert(sz>0);
for (int i=0;i<sz;++i)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
int main()
{
int a[]={2,0,4,9,3,6,8,7,1,5};
int sz=sizeof(a)/sizeof(a[0]);
QuickSort1(a,0,sz-1);
Printf(a,sz);
return 0;
}
4.测试结果
(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元素的子数列和大于基准值元素的子数列排序。
2.图说
3.代码实现
#include<iostream>
#include<assert.h>
using namespace std;
void QuickSort2(int* arr,int left,int right)
{
assert(arr);
if (left>=right)
{
return ;
}
int key=arr[right];//健值
int hollow=right;//记录坑的位置
int begin=left;
int end=right;
while (left<right)
{
while (left<right&&arr[left]<=key)//寻找大于健值的数据
{
++left;
}
//出来以后left找到了大于健值的数据,将其填入当前坑中;之后left的当前位置变为坑
arr[hollow]=arr[left];//填坑
hollow=left;//更新坑的位置
while(right>left&&arr[right]>=key)//寻找小于健值的数据
{
--right;
}
arr[hollow]=arr[right];//填坑
hollow=right;//更新坑的位置
//出来以后right找到了小于健值的数据,将其填入当前left坑中;之后right的当前位置变为坑
}
//循环结束以后,left和right相遇;此时将key值填入当前的坑中,一趟快排完成
if (left==right)
{
arr[left]=key;
}
QuickSort2(arr,begin,left-1);
QuickSort2(arr,left+1,end);
}
void Printf(int* a,int sz)
{
assert(sz>0);
for (int i=0;i<sz;++i)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
int main()
{
int a[]={1,7,8,4,2,3,6,5};
int sz=sizeof(a)/sizeof(a[0]);
QuickSort2(a,0,sz-1);
Printf(a,sz);
return 0;
}
4.测试结果
5.挖坑法的代码优化
用swap代替赋值;少去最后一步给相遇点赋值key值,提高程序性能
//挖坑法优化--用swap代替赋值;少去最后一步给相遇点赋值key值,提高程序性能
void QuickSort22(int* arr,int left,int right)
{
assert(arr);
if (left>=right)
{
return ;
}
int key=arr[right];//健值
int hollow=right;//记录坑的位置
int begin=left;
int end=right;
while (left<right)
{
while (left<right&&arr[left]<=key)//寻找大于健值的数据
{
++left;
}
//出来以后left找到了大于健值的数据,将其填入当前坑中;之后left的当前位置变为坑
swap(arr[hollow],arr[left]);//填坑
hollow=left;//更新坑的位置
while(right>left&&arr[right]>=key)//寻找小于健值的数据
{
--right;
}
swap(arr[hollow],arr[right]);//填坑
hollow=right;//更新坑的位置
//出来以后right找到了小于健值的数据,将其填入当前left坑中;之后right的当前位置变为坑
}
QuickSort2(arr,begin,left-1);
QuickSort2(arr,left+1,end);
}
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只左右的大数据区间和小数据区间进行排序;
2.图说:
3.代码实现
#include<iostream>
#include<assert.h>
using namespace std;
//3.前后指针法
void QuickSort3(int* arr,int left,int right)
{
assert(arr);
if (left>=right)
{
return ;
}
int begin=left;
int end=right;
int key=arr[right];//选取基准值key
int cur=left;
int prev=left-1;
while (cur!=right)
{
if (arr[cur]<key&&++prev!=cur)
{
swap(arr[prev],arr[cur]);
}
++cur;
}
//循环出来以后,此时,cur到了最后的位置
swap(arr[++prev],arr[right]);
QuickSort3(arr,begin,prev-1);
QuickSort3(arr,prev+1,end);
}
void Printf(int* a,int sz)
{
assert(sz>0);
for (int i=0;i<sz;++i)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
int main()
{
int a[]={6,0,5,1,3,4};
int sz=sizeof(a)/sizeof(a[0]);
QuickSort3(a,0,sz-1);
Printf(a,sz);
return 0;
}
4.测试结果:
5.前后指针法快速排序的深刻理解
prev好人cur两个指针之间有两种状态:
状态一:prev始终紧跟着cur,这种状态的保持条件是cur向右走时没有遇到比key值答的数据;每次遇到一个小的数据就停下来让prev++;和自己处在同一位置;
状态二:prev好人cur分离,prev在一个小数据位置原地等待,cur勇敢的朝前取寻找自己的猎物—小数据,当在寻找的过程中碰见了比key大的数据,此时cur不会停止,继续向右寻找,而prev始终在当前第一个比key值大的数据的紧前面,直到cur找到比key小的值,此时将prev++;来到区间从左到右第一个大于key值得数据的位置,然后prev位置的值和cur位置的小于key的值交换;让小的值房左边,大的值放右边,交换完毕之后,prev所处位置的值小于key;当cur到达区间的尾端时,将prev++使其来到第一个大于key的值得位置和key值交换;这时一趟前后指针的快排完成;小数据全在prev的右面,大数据全在prev的左边,
四、快排的优化
(1)三数取中法
每次取待排序区间的左端,右端,中间,这三个数中的中间大小数作为key值;
当我们取得基准值key为要排序区间的最大值或者最小值的时候,这时就是快排的最坏情况,时间复杂度最大位O(N^2);这种情况最明显的例子就是要排序的区间本身就是一个有序区间时,此时该区间的左右两端的值就位最大值和最小值;
那么即便不是有序区间,我们在取key值时,也有可能取到最大或者最小值;
那么如何避免取到的key值为最大值或者最小值呢?方法就是三数区中法,
三数区中法保证了所取得key值不是最大也不是最小,避免了快排最坏情况的出现,当数据有序是,利用三数取中法,可以将原来的这种情况为快排的最坏情况变为快排的最好情况,因为,这时,利用三数取中,每次取到的key值都是该待排序有序区间的中间的数,没趟都不用交换,且递归的次数会少一点;这样时间复杂度将达到最低;
- 三数取中法的程序实现
int GetMidIndex(int* arr,int left,int right)
{
assert(arr);
int mid=(left+right)/2;
if (arr[left]>arr[mid])
{
if (arr[mid]>arr[right])//arr[left]>arr[mid]>arr[right]
{
return mid;
}
//arr[mid]<=arr[right]
else if (arr[left]>arr[right])//arr[left]>arr[right]=>arr[mid]
{
return right;
}
else//arr[right]>=arr[left]>=arr[mid]
{
return left;
}
}
else//arr[left]<=arr[mid]
{
if (arr[left]>arr[right])//arr[mid]>=arr[left]>arr[right]
{
return left;
}
//arr[left]<=arr[right]
else if(arr[mid]>arr[right])//arr[mid]>arr[right]>arr[left]
{
return right;
}
//arr[mid]<=arr[right]
else//arr[right]>=arr[mid]=>arr[left]
{
return mid;
}
}
}
(2)小区间优化法
我们知道,递归代码虽然看起来比较简单,但是递归时的函数进行压栈的开销是比较大的,效率很低,所以,我们可以对排序进行优化:如果区间比较小时,我们可以采用插入排序。下边给出代码实现:
- 代码:
QuickSort(int* arr,int left ,int right)
{
//由于递归太深会导致栈溢出,效率低,所以,区间比较小时(小于13)采用插入排序。
if (right-left>13)
{
//三种快排的有效函数体
}
else
InsertSort(a+begin,end-begin + 1);//要排的区间小于13,则使用选择排序进行排序;
}
(3)使用两个优化以后的前后指针法快排的程序
#include<iostream>
#include<assert.h>
using namespace std;
//三数取中
int GetMidIndex(int* arr,int left,int right)
{
assert(arr);
int mid=(left+right)/2;
if (arr[left]>arr[mid])
{
if (arr[mid]>arr[right])//arr[left]>arr[mid]>arr[right]
{
return mid;
}
//arr[mid]<=arr[right]
else if (arr[left]>arr[right])//arr[left]>arr[right]=>arr[mid]
{
return right;
}
else//arr[right]>=arr[left]>=arr[mid]
{
return left;
}
}
else//arr[left]<=arr[mid]
{
if (arr[left]>arr[right])//arr[mid]>=arr[left]>arr[right]
{
return left;
}
//arr[left]<=arr[right]
else if(arr[mid]>arr[right])//arr[mid]>arr[right]>arr[left]
{
return right;
}
//arr[mid]<=arr[right]
else//arr[right]>=arr[mid]=>arr[left]
{
return mid;
}
}
}
//左右指针法
void QuickSort1(int* arr,int left,int right)
{
assert(arr);
if (right-left<13)//小区间优化法
{
int key=GetMidIndex(arr,left,right);//三数取中法,优化key的最坏情况的取值
int begin=left;
int end=right;
while (left>=right)//递归返回条件
{
return ;
}
while(left<right)
{
while (left<right&&arr[left]<=arr[key])//从左边开始找到比基准值大的数停下来
{
++left;
}
while (right>left&&arr[right]>=arr[key])//从右边开始找到比基准值小的数停下来
{
--right;
}
if (left<right&&arr[left]!=arr[right])//如果左右指针都停下来了,并且不是同一个值,自身和自身交换,没意义,降低效率
{
swap(arr[left],arr[right]);
}
}
//一趟排序完成,此时left=right,交换相遇点位置的数据和key位置的数据
swap(arr[left],arr[key]);
//递归地把小于基准值元素的子数列和大于基准值元素的子数列排序
QuickSort1(arr,begin,left-1);
QuickSort1(arr,left+1,end);
}
else
InsertSort(a+begin,end-begin + 1);
}
五、快排的非递归实现
#include<iostream>
#include<stack>
#include<assert.h>
using namespace std;
int QuickSort(int* arr,int left,int right)
{
int key=right;
while(left<right)
{
while (left<right&& arr[left]<=arr[key])
{
++left;
}
while(right>left&&arr[right]>=arr[key])
{
--right;
}
if (left<right&&arr[left]!=arr[right])
{
swap(arr[left],arr[right]);
}
}
if (left==right&&arr[left]!=arr[key])
{
swap(arr[left],arr[key]);
}
return left;
}
//**************************核心代码*********************************
void NoRecursiveQuickSort(int* arr,int left,int right)
{
assert(arr);
stack<int> s;
s.push(left);//先压入区间的左边
s.push(right);//再压入区间的右边
while(!s.empty())
{
int end=s.top();
s.pop();
int begin=s.top();
s.pop();
int div=QuickSort(arr,begin,end);
if (begin<div-1)
{
s.push(begin);
s.push(div-1);
}
if (div+1<end)
{
s.push(div+1);
s.push(end);
}
}
}
//**************************核心代码*********************************
void Rrintf(int* arr,int left,int right)
{
assert(arr);
while (left<=right)
{
cout<<arr[left++]<<" ";
}
}
int main()
{
int a[]={5,2,7,3,4,1,6,9,8};
int sz=sizeof(a)/sizeof(a[0]);
NoRecursiveQuickSort(a,0,sz-1);
Rrintf(a,0,sz-1);
return 0;
}
六:递归快排的时间复杂度
(1)一般情况(含最优情况):
快速排序的时间复杂度取决于它的递归的深度;一般情况下为O(N*logN)
(2)最坏情况:
当基准值key取到待排序区间的最小值或者最大值时(其中最典型也是一定出现最坏情况的例子就是待排序区间的数据为有序数据),就会出现最坏情况;
假设有N个数据,这样总共需要递归N-1次;
递归总共需要比较的次数为:
(N-1)+(N-2)+(N-3)+…+1=N(N-1)/2
每层比较除去自身和前一层已经排序好的元素
所以最坏情况的时间复杂度为O(N^2);
算法的时间复杂度一般说的是最坏情况的时间复杂度。
然而,有例外:
有时时间复杂度并不看最坏情况,而看最好情况,比如哈希表的查找(哈希冲突出现的概率很小),快速排序(有相应的优化机制)。
END!!!