再看快速排序(QuickSort)

来源:互联网 发布:javascript delay函数 编辑:程序博客网 时间:2024/06/05 16:24

      快速排序是一个十分伟大的算法,作为再一次的学习,写一写快排以及和快排相关的问题。


1.基本的快速排序方法。

快速排序(QuickSort)的基本思想是:通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可以分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

快速排序的基本过程在此不做赘述,主要展示代码以及和快排相关的问题。当然是先快排有很多种方法很代码,基本程序的框架是一样的。


代码:

#include<iostream>using namespace std;template <typename T>void QuickSort(T *a,int low , int high){int pivot ;if(low < high){pivot = Partition(a,low,high);//算出枢轴将数组a以pivot一分为二 QuickSort(a,low,pivot-1);     //对低子表递归排序 QuickSort(a,pivot+1,high);    //对高子表递归排序 }}/*Partition函数要做的,就是先选取当中的一个关键字,比如选择第一个关键字50,然后将它放到某一个位置,使得它左边的 值都比它小,右边的值比它大。*/ template <typename T>int Partition(T *a,int low,int high){int pivot_key = a[low];//用子表的第一个记录作为枢轴记录     /*从表的两端交替向中间扫描*/ while( low < high ){        /*从后向前扫描*/while( low<high && a[high]>=pivot_key ){high--;}a[low] = a[high];        /*从前向后扫描*/while( low<high && a[low]<=pivot_key )        {    low++;}a[high] = a[low];}a[low] = pivot_key;return low; //返回枢轴所在的位置 }template <typename T>void print(T *a , int len){for(int i=0;i<len;i++){cout<<a[i]<<" ";}cout<<endl;}int main(){int a[] = {50,10,90,30,70,40,80,60,20};char b[] = {'e','a','i','c','g','d','h','f','b'};QuickSort( a,0,sizeof(a)/sizeof(a[0])-1 );cout<<"After QSort:"<<endl; print( a,sizeof(a)/sizeof(a[0]) );QuickSort( b,0,sizeof(b)/sizeof(b[0])-1 );    cout<<"After QSort:"<<endl;print( b,sizeof(b)/sizeof(b[0]) );    system("pause");return 0;}

结果:



2.快排的优化

2.1优化选取枢轴

 三数取中法:取三个关键字先进性排序,将中间数作为枢轴,一般是曲左端、右端和中间三个数。这样至少中间数一定不会是最小或者最大的数,从个概率上来讲,取三个数均为最小或者最大数的可能性微乎其微,因此中间数位于较为中间的值的可能性就大大提高了。因此可以在Partition函数的第一行代码(int pivot_key = a[low];)前加入如下代码:

int m = ( (high - low) >> 1 ) + low;//计算数组中间元素的下标if( a[low] > a[high]) {    swap(a,low,high);}if( a[m] > a[high] ){    swap(a,high,m);}if( a[m] > a[low] ){    swap(a,m,low);}/*此时a[low]已经为整个序列左中右三个关键字的中间值*/int pivot_key = a[low];.... </span>

2.2优化小数组的排序方案

        快排适合于解决非常大的数组的排序问题。那么相反的情况下,如果数组非常小,其实快排反而不如插入排序来的效果更好(直接插入排序是简单排序中性能效果最好的)。因为快排用了很多递归操作,在大量数据排序时,算法优势胜过递归影响,但如果只有几个记录,可以选择插入排序。


2.3优化递归操作

递归对性能是有一定影响的,Quicksort函数在其尾部有两次递归操作。如果待排序的序列划分极端不平衡,递归深度将趋近于n,而不是平衡时的log2n,这就不仅仅是速度快排的问题了。栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。因此如果能够减少递归,将会大大提高性能。于是对QuickSort实施尾递归优化。

template <typename T>void QuickSort1(T *a,int low , int high){int pivot ;<strong>while</strong>(low < high){pivot = Partition(a,low,high);//算出枢轴将数组a以pivot一分为二 QuickSort1(a,low,pivot-1);     //对低子表递归排序 <strong>low = pivot + 1 ;             //尾递归 </strong>}}
当我们将if改成while后,因为第一次递归以后,变量low就没有用处了,所以可以将pivot+1赋值给low,在循环后,来一次Partition(a,low,high),其效果等同于"QuickSort(a,pivot+1,high)"。结果相同,但因为采用迭代而不是递归的方法可以缩减堆栈深度,从而提高整体性能。关于尾递归,笔者理解的也不是十分透彻,希望读者可以不吝赐教。


3.中位数问题:现在给你n个数,让你找到这n个数的中位数。有哪些方法?(假设n个数可以一次装入到内存中)

方法一:这个n个数是无序的,那么就去将这n个数进行排序,利用快速排序,平均时间复杂度为O(nlogn),然后用O(1)的时间找到中位数。具体代码就不写了。只是在n很大的情况下,效率非常的低,那么有木有线性复杂度的方法呢?


方法二:快排的变形。我们知道可以通过分治的方法将数组按照枢轴分为两个部分,一个是大于枢轴的部分,另一个是小于枢轴的部分,那么找到中位数就相当于找到枢轴等于(n-1)/2时候对应的数组的值,因此在每次得到一个枢轴值的时候,都和(n-1)/2进行比较,如果小于(n-1)/2,那么就去处理枢轴右面的数组序列;否则处理枢轴左面的数组序列,这样就相当于是一个线性的搜索过程,时间复杂度为O(n)。

而查找中位数也是另外一个问题的具体情况,那就是"The max/min Nth",数组中第N个最大/最小数的问题,其实也是相当于TopN问题,那么我们下面分析这个问题,并将代码呈上。


4.如何找到数组中最大(小)的第K个数?又如何找到数组中的前K个最大(小)的数,即TopN问题?(为方便讨论,下面都是找到最大的数)

方法一:首先可以使用堆排序

找到第k大的数以及TopK最大的数,可以使用堆排序,建立大顶推,不断的调整,经过k次,就可以找到最大的k个数,第k大的数自然也就得到了。经过k次调整,平均的时间复杂度为O(klogn)。代码在这里就不贴了。重点介绍方法二。

方法二:这种方法类似于3中介绍的方法二,就不再细说,上代码。

#include<iostream>using namespace std;template <typename T>T findTheKthNum(T *a,int low , int high, int nth){int pivot = Partition(a,low,high);if(pivot == nth) return a[pivot];else if( pivot > nth ) return findTheKthNum(a,low,pivot-1,nth);else return findTheKthNum(a,pivot+1,high,nth);}template <typename T>int Partition(T *a,int low,int high){int pivot_key = a[low];while( low < high ){while( low<high && a[high]>=pivot_key ){high--;}a[low] = a[high];while( low<high && a[low]<=pivot_key ){low++;}a[high] = a[low];}a[low] = pivot_key;return low;}template <typename T>void print(T *a , int len){for(int i=0;i<len;i++){cout<<a[i]<<" ";}cout<<endl;}int main(){int a[] = {50,10,90,30,70,40,80,60,20};char b[] = {'e','a','i','c','g','d','h','f','b'};cout<< findTheKthNum( a,0,sizeof(a)/sizeof(a[0])-1 , 4 )<<endl;print( a,sizeof(a)/sizeof(a[0]) );cout<< findTheKthNum( b,0,sizeof(b)/sizeof(b[0])-1 , 4 )<<endl;print( b,sizeof(b)/sizeof(b[0]) );    system("pause") ;return 0;}
结果:



分析:这段代码就相当于找到了第K大的数,同时左边都是比它小的数,右边都是比它大的数,自然就能知道TopK小(大)的数了。

PS:现在有n个数,不能够一次性的装入到内存中,如何找到TopK大的数?这是一个大数据的算法问题,在此不做具体分了,大概的步骤是先对每个数hash取余到若干文件中,然后对每个文件中的用堆排序或者分治的方法得到最大的K个数,最后将每个文件中最大的K个数归并,得到整体的K个数。


5.最后讲一下C语言的里面的qsort函数以及C++中的sort函数,主要还是讲用法。

5.1.qsort

qsort的定义为:

void qsort(void *base,size_t num,size_t size,int(*compar)(const void*,const void*) );
其中compar为函数指针,需要传递一个函数名来调用该函数,一般这种函数的原型为:

<span style="font-size:14px;">int compar(const void* a , const void *b) {    return ( *(int*)a - *(int*)b );}</span>

关于函数指针在此就不做过多解释,主要还是写一下qsort的几个用法:

1).对一维数组进行排序:
对一个长为1000的数组进行排序,int a[1000];
qsort( a , 1000 , sizeof(int) , cmp);int cmp( const void *a , const void *b ){        return *(int *)a-*(int *)b;//由大到小排序,return *(int*)b-*(int*)a;}
2).对二维数组进行排序:
int a[1000][2];其中按照a[0]的大小进行一个整体的排序,其中a[1]必须和a[0]一起移动交换;
qsort( a , 1000 , sizeof(int)*2 , cmp );int cmp( const void *a , const void *b ){    return ( (int *)a)[0] - ( (int *)b)[0]  ;}

char a[1000][20];进行排序:
qsort( a , 1000 , sizeof(char)*20 , cmp );int cmp( const void *a , const void *b ){    return strcmp( (char*)a - (char*)b )  ;}

3).对结构体进行排序:
        

typedef struct str{    char str1[11];    char str2[11];}str;str s[1000];int cmp(const void *a, const void *b){    return strcmp( ((str*)a)->str2 ,  ((str*)b)->str2 );}qsort( s , 1000  , sizeof(str) , cmp );

②对结构体进行排序,cmp函数实现了,先对dis从大到小排序,然后在dis相同的情况下,按照cost从大到小进行排序。
typedef struct point{    int dis;    int cost;        }tPoint ;tPoint p[10001];bool cmp(point a,point b){     if(a.dis < b.dis)              return true;     else if(a.dis == b.dis)         return a.cost<b.cost;     else return false;}sort(p,p+n,cmp);
③用的是qsort,效果应该和②是一样的。
typedef struct point{    int dis;    int cost;        }tPoint ;tPoint p[10001];int cmp_dis(const void *a , const void *b){    if ( ( ((tPoint*)a)->dis ) > ( ((tPoint*)b)->dis ) )          return true;    else if ( ( ((tPoint*)a)->dis ) == ( ((tPoint*)b)->dis ) )         return ( ((tPoint*)a)->cost ) > ( ((tPoint*)b)->cost );    else         return false;} qsort( p,n,sizeof(tPoint),cmp_dis );//sort by dis 

4).对double型进行排序:
int cmp( const void *a, const void *b ){        return ( (*(double*)a - *(double*)b >0 )?1:-1 ;}qsort( s,n,sizeof(int ),cmp );
5).对char*类型字符串进行排序:
以下是摘自stackoverflow的内容,讲的还算清楚。

Suppose I have an array of pointers to char in C:

char *data[5] = { "boda", "cydo", "washington", "dc", "obama" };

And I wish to sort this array using qsort:

qsort(data, 5, sizeof(char *), compare_function);

I am unable to come up with the compare function. For some reason this doesn't work:

int compare_function(const void *name1, const void *name2){    const char *name1_ = (const char *)name1;    const char *name2_ = (const char *)name2;    return strcmp(name1_, name2_);}

I did a lot of searching and found that I had to use ** inside of qsort:

int compare_function(const void *name1, const void *name2){    const char *name1_ = *(const char **)name1;    const char *name2_ = *(const char **)name2;    return strcmp(name1_, name2_);}
Then works.


5.2.sort

qsort似乎不能体现出范型编程的优势,而C++中的sort相对来讲更简单易用写。

基本用法参考:http://www.cplusplus.com/reference/algorithm/sort/?kw=sort 

关于sort中的第二个版本,定义仿函数来自己定义排序方法中的仿函数理解还是很模糊,根据书上的说法就是:

以sort()为例,其第一版本是以operator<为排序时的元素位置调整依据,第二版本则允许用户指定任何"操作",务必排序后的两两相邻元素都能令该结果为true。要将这种"操作"当做算法的参数,唯一办法就是先将该"操作"设计为一个所谓的仿函数(就语言层面而言是个class),再以该仿函数产生一个对象,并以此对象作为算法的一个参数。

根据以上陈述,既然函数指针可以达到"将整组操作当做算法的参数",那又何必有所谓的仿函数呢?原因在于函数指针毕竟不能满足STL对抽象性的要求,也不能满足软件积木的要求--函数指针无法和STL其他组件(如配接器adapter)搭配,产生更灵活的变化。

就是先观点而言,仿函数其实上就是一个"行为类似函数"的对象,为了能够"行为类似函数",其类别定义中必须自定义(改写,重载)functional call 运算子(operator())。拥有这样的运算子后,我们就可以在仿函数对象后而加上一对小括号,一次调用仿函数所定义的operator()。下面贴个代码,用到了sort和for_each。

#include<iostream>#include<vector>#include<algorithm>using namespace std;bool pfunc(int i,int j){    return i < j;}class cfunctor{public:    bool operator()(int i,int j)        {         return i < j;        }}mycfunctor;void pprint(int i){    cout<<i<<" ";}class cprint{public:    void operator()(int i)        {        cout<<i<<" ";    }}mycprint;int main(){    int a[] = {32,71,12,45,26,80,53,33};    int b[] = {32,71,12,45,26,80,53,33};        vector<int> vec1( a,a+sizeof(a)/sizeof(a[0]) );    vector<int> vec2( b,b+sizeof(b)/sizeof(b[0]) );        sort( vec1.begin(),vec1.end(),pfunc );    sort( vec2.begin(),vec2.end(),mycfunctor );    for_each(vec1.begin(),vec1.end(),pprint );    cout<<endl;    for_each(vec2.begin(),vec2.end(),mycprint);    cout<<endl;        system("pause");    return 0;} 

体会一下这个代码,知道怎么用,然后感受一些就行了。


转载请注明:http://blog.csdn.net/lavorange/article/details/38896519





0 0