再看快速排序(QuickSort)

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


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





你可能感兴趣的:(快速排序,sort,中位数,topN)