您的位置:首页 > 其它

排序算法之快速排序

2007-12-25 21:30 453 查看
顾名思义,快速排序(quicksort)是在实践中最快的已知排序算法,它的平均运行时间是O(NlogN)。该算法之所以特别快,主要是由于非常精炼和高度优化的内部循环。它的最坏情形的性能为O(N2),但稍加努力就可以避免这种情形。

像归并排序一样,快速排序也是一种分治的递归算法。将数组S排序的基本算法是有下列简单的四步组成:

(1) 如果S中元素个数是0或者1,则返回。
(2) 取S中人一个元素v,称之为枢纽元(pivot)。
(3) 将S-{v}(S中其余元素)划分为两个不相交的集合:S1={x∈S-{v}}|x<=v}和S2={x∈S-{v}|x>=v}。
(4) 返回{quicksort(S1),后跟v,继而quicksort(S2)}。
显然该算法成立,但是不清楚的是,为什么它比归并排序快。如同归并排序那样,快速排序递归地解决两个之问题并需要线性的附加工作(第3步),不过,与递归排序不同,这两个子问题并不保证具有相等的大小,这是个潜在的隐患。快速排序更快的原因在于,第3步划分成两组实际上是在适当的位置进行并且非常有效,它的高效不仅弥补了大小不等的递归的不足而且还超过了它。
这里介绍的方法是大量分析和经验研究的结果,它代表实现快速排序的非常有效的方法,哪怕是对该方法最微小的偏差都可能引起意想不到的不良结果。
选择枢纽元:

1、 一种错误的方法
通常的、没有经过充分考虑的选择是将第一个元素用作枢纽元。如果输入是随机的,那么这是可以接受的,但是如果输入是预排序的或者是反序的,那么这样的枢纽元就产生一个劣质的分割,因为所有的元素不是被划入S1就是被划入S2。更有甚者,这种情况发生在所有的递归调用中。实际上,如果第一个元素用作枢纽元而且输入是预先排序的,那么快速排序所花费的时间将是二次的。然而,预排序的输入(或者有一大段预排序数据的输入)是相当常见的,因此,使用第一个元素作为枢纽元是非常糟糕的,应该立即放弃这种想法。另一种想法是选取前两个互异的键中的较大者作为枢纽元,但这和只选取第一个元素作为枢纽元具有相同的害处。不要使用这两种选取枢纽元的策略。
2、 一种安全的作法
一种安全的方针是随机选取枢纽元。一般来说这种策略非常安全,除非随机数生成器有问题(这不像你所想象的那么罕见),因为随机的枢纽元不可能总在接连不断地产生劣质的分割。另一方面,随机数的生成一般是昂贵的,根本减少不了算法其余部分的平均运行时间。
3、 三数中分割法
一组N个数的中值是第[N/2]个最大的数。枢纽元的最好的选择是数组的中值。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形。

对于很小的数组(N<=20),快速排序不入插入排序好。而且,因为快速排序是递归的,所以这样的情形经常发生。通常的解决方法是,对于小的数组不递归地使用快速排序,而代之以诸如插入排序这样的对小数组有效的排序算法。一种好的截止范围是N=10。这种做法也避免了一些有害的退化情形,入取三个元素的中值而实际上却只有一个或两个元素的情况。

实际的快速排序例程:

/**

* Quicksort algorithm (driver)

*/

template <typename Comparable>

void quicksort( vector<Comparable> & a )

{

quicksort( a, 0, a.size( ) - 1 );

}

/**

* Return median of left, center, and right.

* Order these and hide the pivot.

*/

template <typename Comparable>

const Comparable & median3( vector<Comparable> & a, int left, int right )

{

int center = ( left + right ) / 2;

if( a[ center ] < a[ left ] )

{

swap( a[ center ], a[ left ] );

}

if( a[ right ] < a[ left ] )

{

swap( a[ right ], a[ left ] );

}

if( a[ right ] < a[ center ] )

{

swap( a[ right ], a[ center ] );

}

/***** Place pivot at position right - 1 ******/

swap( a[ center ], a[ right - 1 ] );

return a[ right -1 ];

}

/**

* Internal quicksort method that makes recursive calls.

* Uses median-of-three partitioning and a cutoff of 10.

* a is an arrray of Comparable items.

* left is the left-most index of the subarray.

* right is the right-most index of the subarray.

*/

template <typename Comparable>

void quicksort( vector<Comparable> & a, int left, int right )

{

if( left + 10 <= right )

{

Comparable pivot = median3( a, left, right );

/*********** Begin partitioning ****************/

int i = left, j = right - 1; /* From function median3, we can know that a[ left ] <= a[ right - 1 ] <= a[ right ]. */

for( ; ; )

{

while( a[ ++i ] < pivot ) { }

while( pivot < a[ --j ] ) { }

if( i < j )

{

swap( a[ i ], a[ j ] );

}

else

break;

}

swap( a[ i ], a[ right -1 ] ); /* Restore pivot */

quicksort( a, left, i - 1 );/* Sort small elements */

quicksort( a, i + 1, right );/* Sort large elements */

}

else /* Do an insertion sort on the subarray */

insertionSort( a, left, right );

}

顾名思义,快速排序(quicksort)是在实践中最快的已知排序算法,它的平均运行时间是O(NlogN)。该算法之所以特别快,主要是由于非常精炼和高度优化的内部循环。它的最坏情形的性能为O(N2),但稍加努力就可以避免这种情形。

像归并排序一样,快速排序也是一种分治的递归算法。将数组S排序的基本算法是有下列简单的四步组成:

(1) 如果S中元素个数是0或者1,则返回。
(2) 取S中人一个元素v,称之为枢纽元(pivot)。
(3) 将S-{v}(S中其余元素)划分为两个不相交的集合:S1={x∈S-{v}}|x<=v}和S2={x∈S-{v}|x>=v}。
(4) 返回{quicksort(S1),后跟v,继而quicksort(S2)}。
显然该算法成立,但是不清楚的是,为什么它比归并排序快。如同归并排序那样,快速排序递归地解决两个之问题并需要线性的附加工作(第3步),不过,与递归排序不同,这两个子问题并不保证具有相等的大小,这是个潜在的隐患。快速排序更快的原因在于,第3步划分成两组实际上是在适当的位置进行并且非常有效,它的高效不仅弥补了大小不等的递归的不足而且还超过了它。
这里介绍的方法是大量分析和经验研究的结果,它代表实现快速排序的非常有效的方法,哪怕是对该方法最微小的偏差都可能引起意想不到的不良结果。
选择枢纽元:

1、 一种错误的方法
通常的、没有经过充分考虑的选择是将第一个元素用作枢纽元。如果输入是随机的,那么这是可以接受的,但是如果输入是预排序的或者是反序的,那么这样的枢纽元就产生一个劣质的分割,因为所有的元素不是被划入S1就是被划入S2。更有甚者,这种情况发生在所有的递归调用中。实际上,如果第一个元素用作枢纽元而且输入是预先排序的,那么快速排序所花费的时间将是二次的。然而,预排序的输入(或者有一大段预排序数据的输入)是相当常见的,因此,使用第一个元素作为枢纽元是非常糟糕的,应该立即放弃这种想法。另一种想法是选取前两个互异的键中的较大者作为枢纽元,但这和只选取第一个元素作为枢纽元具有相同的害处。不要使用这两种选取枢纽元的策略。
2、 一种安全的作法
一种安全的方针是随机选取枢纽元。一般来说这种策略非常安全,除非随机数生成器有问题(这不像你所想象的那么罕见),因为随机的枢纽元不可能总在接连不断地产生劣质的分割。另一方面,随机数的生成一般是昂贵的,根本减少不了算法其余部分的平均运行时间。
3、 三数中分割法
一组N个数的中值是第[N/2]个最大的数。枢纽元的最好的选择是数组的中值。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形。

对于很小的数组(N<=20),快速排序不入插入排序好。而且,因为快速排序是递归的,所以这样的情形经常发生。通常的解决方法是,对于小的数组不递归地使用快速排序,而代之以诸如插入排序这样的对小数组有效的排序算法。一种好的截止范围是N=10。这种做法也避免了一些有害的退化情形,入取三个元素的中值而实际上却只有一个或两个元素的情况。

实际的快速排序例程:

/**

* Quicksort algorithm (driver)

*/

template <typename Comparable>

void quicksort( vector<Comparable> & a )

{

quicksort( a, 0, a.size( ) - 1 );

}

/**

* Return median of left, center, and right.

* Order these and hide the pivot.

*/

template <typename Comparable>

const Comparable & median3( vector<Comparable> & a, int left, int right )

{

int center = ( left + right ) / 2;

if( a[ center ] < a[ left ] )

{

swap( a[ center ], a[ left ] );

}

if( a[ right ] < a[ left ] )

{

swap( a[ right ], a[ left ] );

}

if( a[ right ] < a[ center ] )

{

swap( a[ right ], a[ center ] );

}

/***** Place pivot at position right - 1 ******/

swap( a[ center ], a[ right - 1 ] );

return a[ right -1 ];

}

/**

* Internal quicksort method that makes recursive calls.

* Uses median-of-three partitioning and a cutoff of 10.

* a is an arrray of Comparable items.

* left is the left-most index of the subarray.

* right is the right-most index of the subarray.

*/

template <typename Comparable>

void quicksort( vector<Comparable> & a, int left, int right )

{

if( left + 10 <= right )

{

Comparable pivot = median3( a, left, right );

/*********** Begin partitioning ****************/

int i = left, j = right - 1; /* From function median3, we can know that a[ left ] <= a[ right - 1 ] <= a[ right ]. */

for( ; ; )

{

while( a[ ++i ] < pivot ) { }

while( pivot < a[ --j ] ) { }

if( i < j )

{

swap( a[ i ], a[ j ] );

}

else

break;

}

swap( a[ i ], a[ right -1 ] ); /* Restore pivot */

quicksort( a, left, i - 1 );/* Sort small elements */

quicksort( a, i + 1, right );/* Sort large elements */

}

else /* Do an insertion sort on the subarray */

insertionSort( a, left, right );

}

/**
* Data Structures and Algorithm Analysis in C++, Third Edition, Mark Allen Weiss ,P283.
*/
时间复杂度简单分析:

1、 最坏情况的分析
枢纽元素始终是最小元素。如果我们忽略无关紧要的T( 0 ) = 1, 那么递推关系为
T( N ) = T( N -1 ) + cN , N>1, cN为分割N个元素数组所用时间。
递推求和得:T( N ) = O( n2 )。 这时与冒泡排序类似。
2、 最佳情形的分析
在最佳情形下,枢纽元正好位于中间。T( N ) = cNlogN + N = O( NlogN )。
这和归并排序的分析分析完全相同,依次得到完全相同的答案。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: