快速排序由于其效率在同为O(n * log n)的几种排序方法中效率较高,故经常被使用,其思想为:分治法
该算法的基本思想是:
1.先从数列中取出一个数作为枢轴数。
2.分组过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数,达到整个序列的有序目的
(PS:怎么和归并排序的算法思路这么像。。。)
代码所需的基本结构:(实际解决问题时,自己根据需求写结构,这里仅仅是为了方便)
#define MAX 10
//排序所用的顺序表结构
typedef struct
{
int a[MAX+1]; //存储排序数组,a[0]用作临时变量,不存入数值
int length; //记录顺序表的长度
}Sqlist;
//交换
void swap(Sqlist *L,int i,int j)
{
int t=L->a[i];
L->a[i]=L->a[j];
L->a[j]=t;
}
算法代码:
int Partition(SqList *L,int low,int high)
{
int pivotkey;
pivotkey=L->a[low]; //用子表的第一个记录作为枢轴记录
while(low<high)
{
while(low<high && L->a[high]>=pivotkey) //当high处的记录大于枢轴记录时,high继续左移
high--;
swap(L,low,high); //否则就交换
while(low<high && L->a[low]<=pivotkey) //当low处的记录小于枢轴记录时,low继续右移
low++;
swap(L,low,high);
}
return low; //返回枢轴所在位置
}
void Qsort(SqList *L,int low,int high)
{
int pivot;
if(low<high)
{
pivot=Partition(L,low,high); //将L->[low...high]分成两部分
//枢轴值为pivot
Qsort(L,low,pivot-1); //对低子表进行递归排序
Qsort(L,pivot+1,high); //对高子表进行递归排序
}
}
注意在Partition代码中,最终返回时,low即为枢轴值,处于数列的中间位置
复杂度
时间复杂度: 平均情况下,算法的时间复杂度为 O(n * logn)
空间复杂度:主要是递归造成的栈空间的使用,最好的情况下,递归树的深度为 log2 n,其空间复杂度为 O(logn),
最坏情况下,需要n-1次的递归调用,其空间复杂度为 O(n),
平均情况下,为 O(logn)
稳定性
由于关键字的比较和交换是跳跃进行的,故快速排序不稳定
优化
1. 每次选取枢轴值的时候,我们都是选择第一个元素,但若是选择中间的元素作为枢轴值的话,算法的效率会提高很多
三数取中法
取三个关键字先进行排序,将中间数作为枢轴,一般是取左端、右端和中间三个数(当然也可以随机抽取)
于是需要将Parition函数进行修改:
int Partition1(SqList *L,int low,int high)
{
int pivotkey;
int m= low + (high - low)/2; //取数组中间的数为 m
if(L->a[low]>L->a[high]) //若左端元素大,交换
swap(L,low,high);
if(L->a[m]>L->[high]) //若中间元素比右端的大,交换
swap(L,m,high);
if(L->a[m]<L->[low]) //若中间元素小于左端的,交换
swap(L,m,low);
/*此时m已经成为中间值*/
pivotkey=L->a[m];
while(low<high)
{
while(low<high && L->a[high]>=pivotkey)
high--;
swap(L,low,high);
while(low<high && L->a[low]<=pivotkey)
low++;
swap(L,low,high);
}
return low; //返回枢轴所在位置
}
2. 在Partition函数中,交换是不需要的,我们将交换改成替换
int Partition2(SqList *L,int low,int high)
{
int pivotkey;
/*三数取中法*/
int m= low + (high - low);
if(L->a[low]>L->a[high])
swap(L,low,high);
if(L->a[m]>L->[high])
swap(L,m,high);
if(L->a[m]<L->[low])
swap(L,m,low);
/*此时m已经成为中间值*/
pivotkey=L->a[m];
L->a[0]=pivotkey; //备份枢轴值
while(low<high)
{
while(low<high && L->a[high]>=pivotkey)
high--;
L->a[low]=L->a[high]; //将交换改为替换
while(low<high && L->a[low]<=pivotkey)
low++;
L->a[high]=L->a[low];
}
L->a[low]=L->a[0]; //替换回枢轴值
return low; //返回枢轴所在位置
}
3. 数组很小的时候,用快速排序就不如直接插入排序(简单排序中,直接插入排序性能最好),原因在于快速排序中的递归操作不适用处理小数组,所以我们进行优化
将快速排序和直接插入排序结合:
#define MAX_LENGTH 7 //数组长度阈值
void Qsort1(SqList *L,int low,int high)
{
int pivot;
if((high-low)>MAX_LENGTH) //大于这个值时,使用快速排序
{
pivot=Partition(L,low,high);
Qsort(L,low,pivot-1);
Qsort(L,pivot+1,high);
}
else //使用直接插入排序
InsertSort(L);
}
(阈值的大小有资料说7,有资料说50比较好,根据需要调整)
4. 递归对性能有一定的影响,Qsort函数在尾部有两次递归操作,若待排序的序列划分非常不平衡,递归的深度会由平衡的log n
变为不平衡的 n 若减少递归,性能会大大提升
所以我们实行尾递归优化
尾递归:
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作
//递归优化
#define MAX_LENGTH 7
void Qsort1(SqList *L,int low,int high)
{
int pivot;
if((high-low)>MAX_LENGTH)
{
while(low<high)
{
pivot=Partition(L,low,high);
Qsort(L,low,pivot-1);
low=pivot+1; //尾递归
}
}
else
InsertSort(L);
}
将 if 改为 while 后,第一次递归后,low无用处了,所以将pivot赋值给low,
再次循环后,Paritition(L,low,high),等同于Qsort(L,pivot+1,high)
优化后,采用迭代的方法缩减堆栈的深度,提高了性能