声明:本篇文章主要是为了讲解快速排序的一种优化方式,对于快速排序的基本原理和初级代码讲解的并不详细。大家有这方面的需求可参考其他文章。
快速排序的基本原理
快速排序又称为“填坑法”,采用“分治”的思想,是经典排序算法之一。
该方法的基本思想是:
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
根据以上原理,我们可以得到如下代码:
#include <stdio.h>
#include <stdlib.h>
int partition(int *x,int low,int high){
//枢轴的选取与暂存
x[0]=x[low];
int pivotkey;
pivotkey=x[low];
//填坑操作
while(low<high){
while(low<high && x[high]>=pivotkey){
--high;
}
x[low]=x[high];
while(low<high && x[low]<=pivotkey){
++low;
}
x[high]=x[low];
}
//枢轴的位置
x[low]=x[0];
return low; //返回此位置
}
void Qsort(int *x,int s,int t){
int pivotloc;
if(s<t){
pivotloc=partition(x,s,t);
//递归:对枢轴两侧的数据进行排序
Qsort(x,s,pivotloc-1);
Qsort(x,pivotloc+1,t);
}
}
int main()
{
int n;
scanf("%d",&n);
int x[n+1];
for(int i=1;i<n+1;i++){
scanf("%d",&x[i]);
}
Qsort(x,1,n);
printf("%d",x[1]);
for(int i=2;i<n+1;i++){
printf(" %d",x[i]);
}
return 0;
}
平均时间复杂度:
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)
最佳时间复杂度:
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)
最差时间复杂度:
O
(
N
2
)
O(N^2)
O(N2)
根据快速排序的原理不难发现,快速排序在数据为顺序有序和逆序有序的情况下,性能最差,将退化为冒泡排序,时间复杂度为
O
(
N
2
)
O(N^2)
O(N2)。
采用PTA平台上的一道题目,来进行一下性能测试:
上面的代码运行结果如下:
三值取中法,是对快速排序一种最常见、最简便的优化方式。
快速排序的优化–三值取中法
所谓“三值取中法”,就是取数据最左端、最右端和中间的三个数的中间大的那个数。但是,假设我们找到了这个值,我们如何用上面的代码去实现呢?
我们注意到,上面的代码取的是数据最左端的元素,如果要保持数据不动,那我们在找到中间值的时候还必须知道它的下标,从而帮助我们来判断是在数据的那个位置,而且这样做就会破坏数据两侧的两个指针的移动规律,因此这种做法不妥。
所以,我们为什么不让数据变动一下呢?将中值元素和数据最左端原本的数据交换一下不就可以了吗?代码仅需做一个功能的添加即可,非常简单清晰:
#include <stdio.h>
#include <stdlib.h>
int partition(int *x,int low,int high){
int index=getMiddle(x,low,high); //取中函数,返回中值元素的数组下标
if(index!=low){
int temp;
temp=x[low];
x[low]=x[index];
x[index]=temp;
}
//仅需加入这几行代码,其余部分都不变
int pivotkey;
x[0]=x[low];
pivotkey=x[low];
while(low<high){
while(low<high && x[high]>=pivotkey){
--high;
}
x[low]=x[high];
while(low<high && x[low]<=pivotkey){
++low;
}
x[high]=x[low];
}
x[low]=x[0];
return low;
}
void Qsort(int *x,int s,int t){
int pivotloc;
if(s<t){
pivotloc=partition(x,s,t);
Qsort(x,s,pivotloc-1);
Qsort(x,pivotloc+1,t);
}
}
int main()
{
int n;
scanf("%d",&n);
int x[n+1];
for(int i=1;i<n+1;i++){
scanf("%d",&x[i]);
}
Qsort(x,1,n);
printf("%d",x[1]);
for(int i=2;i<n+1;i++){
printf(" %d",x[i]);
}
return 0;
}
现在的问题是:取中函数如何设计呢?这个问题基本上等同于三个数比较大小,返回中间那个数字,但是如果我们采用if-else这种分类讨论的思路来写代码,或许十分容易出错,因此我们可以借助求最大值和最小值的函数来设计取中函数,具体代码如下:
int max(int *x,int low,int high){
return x[low]>x[high]?low:high;
}
int min(int *x,int low,int high){
return x[low]<x[high]?low:high;
}
// 取中函数
int getMiddle(int *x,int low,int high){
int mid=(low+high)/2;
int m=max(x,low,high);
return min(x,m,mid);
}
这样写出来的代码既简洁明了,又思路清晰,不易出错。基本思路也很简单:两个数字中较大的那个数和第三个数中较小的那个数,一定是中间大的那个数。
采用三值取中法的代码,运行结果如下:
由此可见:采用这种方法极大的提高了快速排序的速度,避免了快速排序在序列基本有序的情况下退化为冒泡排序。
拓展–由大到小排序
上面的代码都是对数据进行由小到大排序,如果要实现由大到小排序,应该怎么办呢?改动非常小,明白了原理即可,我就不过多解释了,直接上代码:
#include <stdio.h>
#include <stdlib.h>
int max(int *x,int low,int high){
return x[low]>x[high]?low:high;
}
int min(int *x,int low,int high){
return x[low]<x[high]?low:high;
}
int getMiddle(int *x,int low,int high){
int mid=(low+high)/2;
int m=max(x,low,high);
return min(x,m,mid);
}
int partition(int *x,int low,int high){
int index=getMiddle(x,low,high);
if(index!=low){
int temp;
temp=x[low];
x[low]=x[index];
x[index]=temp;
}
int pivotkey;
x[0]=x[low];
pivotkey=x[low];
while(low<high){
while(low<high && x[high]<=pivotkey){ // <= 改为 >=
--high;
}
x[low]=x[high];
while(low<high && x[low]>=pivotkey){ // >= 改为 <=
++low;
}
x[high]=x[low];
}
x[low]=x[0];
return low;
}
void Qsort(int *x,int s,int t){
int pivotloc;
if(s<t){
pivotloc=partition(x,s,t);
Qsort(x,s,pivotloc-1);
Qsort(x,pivotloc+1,t);
}
}
int main()
{
int n;
scanf("%d",&n);
int x[n+1];
for(int i=1;i<n+1;i++){
scanf("%d",&x[i]);
}
Qsort(x,1,n);
printf("%d",x[1]);
for(int i=2;i<n+1;i++){
printf(" %d",x[i]);
}
return 0;
}
以上都是我在复习数据结构时的一些心得和体会,以及一些拓展的思路。第一次写这种博客,如有错误,欢迎大家批评指正!