注:我测试所用数据都是可以比较大小的类型且无重复元素,且排序按照升序排列。
前言:快排的思想其实和冒泡排序很像,冒泡排序是通过相邻两个元素间的不断交换最终完成排序,我觉得快速排序其实可以理解为从两个相邻的元素开始再发展到两段相邻的数组片段依次回溯进行比较,最终完成排序。
1、快速排序的算法描述
快排和归并排序一样运用了递归的典型思想——分治。我先选取一个枢纽元作为中间元素,不大于它的元素放左边,不小于它的元素放右边,因为我进行排序的是一个随机数数组,所以我选取了每次进行排序的第一个元素为枢纽元,当排序的元素不随机时可以使用三中值分法选取枢纽元。当元素分割完成后,将枢纽元放入左右数组段的中间位置,再在左右数组段中递归进行排序,当待排序元素只有一个时即完成排序。
2、快速排序的函数实现
快排函数:
首先判断开始位置与终点位置的大小关系,若开始位置大于终点位置则说明只有一个元素无需排序。然后,分别声明两个变量 i,j 记录起始位置与终点位置,选取第一个元素为枢纽元。进行终止条件为i,j相等的循环,在循环中若i小于j,每次先移动 j 到达遇到的第一个小于枢纽元的元素下标,再移动 i 到遇到的第一个大于枢纽元的元素下标,且移动顺序不能反,因为先移动 i 可能会造成 i 大于 j 的情况出现,到时候交换枢纽元位置就会发生错误。然后,在 i<j 的情况下交换两个单元元素,使得不大于枢纽元的元素进入左边,不小于枢纽元的元素进入右边(此处考虑到了有元素相等的情况,通过多余的交换避免了边界情况的特殊处理,增强了程序的健壮性),当循环结束时,即元素分割完成,将枢纽元移动到左右数组段的中间位置,然后递归地对左右数组段进行排序。
/*一些声明
typedef int ElementType;
const int N=1000;
*/
void QuickSort(ElementType a[],int begin,int end){
if(begin>end) return ; //只有一个元素无需再排序
int i=begin,j=end;
ElementType temp=a[begin],t; /*因为本次排序的是随机数数组,数据随机,
所以可以直接用第一个元素作枢纽元,否则可以用三中值分法确定枢纽元*/
while(i!=j){ //i等于j时说明两边元素已分好 ,结束循环
/*两个循环顺序不能反,第一个循环每次寻找左边中大于枢纽元的元素下标,
第二个循环每次寻找右边中小于枢纽元的元素的下标*/
while(a[j]>=temp&&j>i) j--;
while(a[i]<=temp&&j>i) i++;
if(i<j){ //显式地写出避免调用交换函数,提高排序速度
t=a[i];
a[i]=a[j];
a[j]=t;
}
} /*以下两步是使枢纽元移动到合适位置,因为循环完成后左边的元素
小于等于枢纽元,右边的元素大于等于枢纽元,枢纽元即可不用再进入排序*/
a[begin]=a[i];
a[i]=temp;
QuickSort(a,begin,i-1); //对枢纽元左侧元素排序
QuickSort(a,i+1,end); //对枢纽元右侧元素排序
}
3、快速排序的分析
3.1 时间复杂度分析
快速排序地运行时间依赖于划分是否平衡,当划分不平衡时快速排序地性能就会下降到接近插入排序,如果划分平衡快速排序地性能与归并排序一样。所以,快速排序地最坏时间复杂度为O(N²),平均时间复杂度为O(nlogn)。所以,使用数组地第一个下标作枢纽元通常是不太好的方法,因为如果数据不随机就会导致划分不太平衡,降低快排性能,比较好地方法是用三分中值法选取枢纽元。
3.2 运行实例分析
在这里我生成一个从1——N的随机数数组,N的选取为10000,1000000,1000000,太小了运行时间太短,注意要在main函数外面声明数组,main函数中声明的数组长度不能太大,因为main函数里面是局部变量放在堆栈段,局部变量太大会导致栈溢出。
生成随机数组的函数:
void Swap(ElementType* x,ElementType* y){
ElementType temp=*x;
*x=*y;
*y=temp;
}
void Randomize(ElementType a[],int length){
memset(a,0,sizeof(a));
srand((ElementType)time(NULL));
for(int i=0;i<length;i++) a[i]=i+1;
for(int i=length-1;i>0;i--) Swap(&a[i],&a[rand()%i]);
}
不同数组长度下的10次运行平均时间:
10000 | 100000 | 1000000 | 10000000 |
---|---|---|---|
0 | 0.01 | 0.142 | 3.9872 |
4、整体代码
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
typedef int ElementType;
const int N=1000000;
ElementType num[N];
void Swap(ElementType*,ElementType*);
void Randomize(ElementType [],int);
void QuickSort(ElementType [],int,int);
int main(void){
clock_t start=0,finish=0;
Randomize(num,N);
start=clock();
QuickSort(num,0,N-1);
finish=clock();
//for(int i=0;i<N;i++) printf("%d\n",num[i]);
printf("time is:\n%lf",(double)(finish-start)/CLOCKS_PER_SEC);
return 0;
}
void QuickSort(ElementType a[],int begin,int end){
if(begin>end) return ; //只有一个元素无需再排序
int i=begin,j=end;
ElementType temp=a[begin],t; /*因为本次排序的是随机数数组,数据随机,
所以可以直接用第一个元素作枢纽元,否则可以用三中值分法确定枢纽元*/
while(i!=j){ //i等于j时说明两边元素已分好 ,结束循环
/*两个循环顺序不能反,第一个循环每次寻找左边中大于枢纽元的元素下标,
第二个循环每次寻找右边中小于枢纽元的元素的下标*/
while(a[j]>=temp&&j>i) j--;
while(a[i]<=temp&&j>i) i++;
if(i<j){ //显式地写出避免调用交换函数,提高排序速度
t=a[i];
a[i]=a[j];
a[j]=t;
}
} /*以下两步是使枢纽元移动到合适位置,因为循环完成后左边的元素
小于等于枢纽元,右边的元素大于等于枢纽元,枢纽元即可不用再进入排序*/
a[begin]=a[i];
a[i]=temp;
QuickSort(a,begin,i-1); //对枢纽元左侧元素排序
QuickSort(a,i+1,end); //对枢纽元右侧元素排序
}
void Swap(ElementType* x,ElementType* y){
ElementType temp=*x;
*x=*y;
*y=temp;
}
void Randomize(ElementType a[],int length){
memset(a,0,sizeof(a));
srand((ElementType)time(NULL));
for(int i=0;i<length;i++) a[i]=i+1;
for(int i=length-1;i>0;i--) Swap(&a[i],&a[rand()%i]);
}
总结
快速排序是一种运行速度较快的排序算法,虽然其最坏情况接近插入排序,但是稍加努力就可以避免最坏情况的出现。从时间复杂度上看,快速排序的平均时间复杂度为O(nlogn),从空间复杂度上看,快排的平均空间复杂度为O(logn)。快排虽然不如归并排序稳定,但是归并排序的空间复杂度高,且快排稍加努力即可避免最坏情况。总的来说,快排的平均时间、空间复杂度都比较好,是一种使用十分广泛的排序算法。