1.快速排序的介绍:
2.传统快速排序及其时间复杂度。
3.快速排序的几种改进方案。
1.快速排序的介绍
快速排序可能是应用的最为广泛的排序算法了,快速排序的主要优点有:
1.原地排序.
2.大多数情况下,它的时间复杂度都是NlogN.
但是它的缺点也很明显,它是一个不稳定的算法,在要排序的数组已经有序的情况下,它的效率能降到N的平方。
2.传统的快速排序实现及其时间复杂度
2.1递归实现的快速排序实现
递归实现的快速排序比较容易让人理解,代码也比较简单,其中最重要的就是要写好partition函数,partition函数的接口如下:
int partition( int data[], int start, int end ); //[start, end)
partition函数的主要功能是在data数组的start~end区间找到一个哨兵元素,找到这个哨兵元素排序后的位置,并且保证这个位置之前的所有元素都小于等于哨兵元素,这个位置之后的元素都大于哨兵元素。
cheat is cheap, show me your code…下面给出partition函数的一种实现方法:
int partition( int data[], int start, int end )
{
int i = start, j = end;
int sentry = start;
while ( 1 )
{
//从前往后找到小于哨兵的元素
while ( data[++i] < data[sentry] )
if ( i >= end )
break;
//从后往前找到大于哨兵的元素
while ( data[--j] > data[sentry] )
if ( j < start )
break;
if ( i >= j )
break;
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
//将data[start]放到合适的位置,
//这里注意一定是start和j交换。
int temp = data[j];
data[j] = data[start];
data[start] = temp;
return j;
}
图片往往比文字更能清楚的说明问题,这张图是使用快速排序排序数组{K, R, A, T, E, L, E, P, U, I, M, Q, C, X, O, S}的切分轨迹图。
partition函数完成之后切记要先测试一下再接着写,在写完每一个小功能之后都先要确保这个功能实现正确之后再进行下一步。
接下来就是快速排序的主体部分了,因为快速排序在递归的时候,需要一些适当的参数来传递信息,所以为了保证排序函数接口的统一性,额外增加了一个_sort函数来进行排序的主要过程。下面是代码实现。
//[start, end)
void _sort( int data[], int start, int end )
{
if ( start >= end ) return ;
int j = partition( data, start, end );
_sort( data, start, j );
_sort( data, j+1, end );
}
void quick_sort( int data[], int size )
{
_sort( data, 0, size );
}
2.2快速排序时间复杂度
对于一个随机数组,在最好的情况下,快速排序每次都能将数组各分成一半,这个时候快速排序的效率能达到最好,在最坏的情况下,快速排序划分的数组每次都有一个没有元素,这时候快速排序的效率最低。
但是总的来说,快速排序的时间复杂度能控制在1.39NlgN之内。
3.快速排序的几种改进方案
1.随机快排
随机快排的出现是为了应对快速排序的最坏情况的出现,因为如果被快排排序的数组是一个已经排好序的数组,那么普通快排每次选取的都是区间的第一个元素,每次分割之后的两个子数组中肯定会有一个子数组为空,所以快排的最坏情况就会出现。
随机快排为了应对这中情况,每次选取哨兵元素都是从要排序的区间中随机选取一个,所以最坏情况出现的几率会大大降低,随机快排应该算是一个比较稳定的排序方法。
为了实现快速排序,只需要实现一个_random函数,这个函数的接口如下:
int _random( int start, int end );
这个函数会返回区间[start, end)之间的一个随机数,产生随机数直接调用c语言的函数库即可。下面是一个参考的实现代码:
int _random( int start, int end )
{
int n = end - start;
//rand()%n会产生[0, n)之间的一个随机数, rand()%n+start会产生[start, end)之间的随机数
int randomNum = rand()%n + start;
return randomNum;
}
将普通快排变为随机快排只需要将上面给出的partition函数里面的
int sentry = start;
改为
int sentry = _random(start, end);
就行了。
2.快速排序中融入插入排序
毫无疑问,对于小数组排序来说,使用插入排序要比使用快速排序快的多,不管在使用快速排序处理多大的数组,在递归的底层我们都要对一些小数组进行排序,这个时候最好的解决方案并不是继续使用快速排序。因此在排序小数组的时候,我们采用插入排序的提高算法的运行效率。
将小数组的排序从快速排序改为插入排序也是比较容易的,只需要将_sort函数里面的
if (start >= end) return ;
改成
if (start + M >= end) { insert_sort( data, M ); return ; }
就行了。至于M,已经有人帮我们证明了M取5~15之间的任意值时算法效率是值得满意的。
3.通过三取样切分优化快速排序
三取样切分跟随机快排一样,也是从寻找最优的切分点这个方向上来优化快排。
三取样切分即是使用数组中的小部分元素的中位数来切分数组,这样做的切分更好,但是会带来计算中位数的负担,人们发现将取样大小设为3并用大小居中的元素切分效果最好。
4.熵最优的排序
熵最优的排序主要是为了处理数组中有大量重复元素的情况,如果数组中有大量重复的元素,如果不考虑对重复元素做特殊处理,就会少了一个优化的好机会,比如,一个元素全部重复的数组就不需要在进行排序了。
一个简单的想法就是将数组的元素分成三部分,大于哨兵的,小于哨兵的和等于哨兵的。这个问题有一个解法,就是Dijkstra解法。
Dijkstra解法的主要思路是:从左到右遍历数组一次,维护一个指针lt,使得data[lo…lt-1]的所有元素都小于哨兵,一个指针gt,使得data[gt+1…hi]之间的元素都大于哨兵,维护一个指针i,使得data[lt…i-1]之间的元素都等于哨兵,data[i…gt]之间的元素还未处理。
具体的处理过程如下,一开始i等于lo,哨兵值等于v:
(1).如果data[i]小于v,则交换data[i]和data[lt],lt++, i++;
(2).如果data[i]大于v,则交换data[i]和data[gt],gt–;
(3).如果data[i]等于v,i++;
这里给出一个大概的实现思路,具体实现细节,有兴趣的可以继续研究。
void Dijkstra_sort(int data[], int lo, int hi)
{
if ( hi <= lo )
return ;
int lt = lo, i = lo+1, gt = hi;
int v = a[lo];
while ( i <= gt )
{
int cmp = compare( a[i], v );
if ( cmp < 0 )
swap( data, lt++, i++ );
else if ( cmp > 0 )
swap( data, i, gt-- );
else
i++;
}
sort( data, lo, lt-1 );
sort( data, gt+1, hi );
}
这里给出一个Dijkstra解法的三向切分代码的轨迹图,可以便于理解。
关于快速排序还有几个版本,可以参考我在网上找的这篇文章:快速排序的几个版本
参考资料:《算法》第四版