Pages

Monday, May 16, 2011

算法总结系列之二: 快速排序(QuickSort)


快速排序是现有的比较排序算法中,效率最好的一种排序算法.
所谓比较排序,是对排序对象的值的比较, 它不特定于排序对象本身的额外特征或排序对象因特定的数据结构而获得的额外条件.
快速排序主要体现分而治之的思想, 主要做法是不断的"选取基准点 - 划分子序列",直至子序列长度为1.
假定数组A[p.......r] - p,r都是数组下标.


算法的C#实现:
public void QuickSort( int[] A, int begin, int end )
{
if ( begin < end )
{
//Procedure Partition split the array into 2 subarry.
int q = Partition( A, begin ,end );
QuickSort( A, begin, q-1 );
QuickSort( A, q, end );
}
}
// 1 sort to get lower partition - judge point - larger partition
public int Partition( int[] A, int bengin, int end )
{
// take the last element of array for judge point
int judgeValue = A[ end ]; 
// i is the bound of elements lower than judgeValue.
int i = begin -1;
for( int j = begin ; j < end ; j ++ )
{
if( A[j] <= judgeValue )
{
//expand the lower bound
i += 1;
//exchange A[i] with current element.
int tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
}
// exchange the judge element with the beginner of larger elements
int tmp = A[ i+1 ];
A[i+1] = A[end];
A[end] = tmp;

return i+1;

}

以上Partition函数的一次排序的过程如下:
原数组: 2 1 7 8 3 4 5
1.选中最后一个元素 5 为判断标准, 此时 bengin = 0 ; end = 6; i = -1; j 从0 到5
2. j = 0 : 判断A[0]小于5, i此时为0, 交换A[0]自身.
    2 1 7 8 3 4 5
3. j = 1 : 判断A[1]小于5, i此时为1, 交换A[1]自身.
    2 1 7 8 3 4 5
4. j = 2 : 判断A[2]大于5, i此时为1, 没有交换行为.
    2 1 7 8 3 4 5
5. j = 3 : 判断A[3]大于5, i此时为1, 没有交换行为.
    2 1 7 8 3 4 5
6. j = 4 : 判断A[4]小于5, i此时为2, 交换A[2]和A[4], 即交换7和3;
    2 1 3 8 7 4 5
7. j = 5 : 判断A[5]小于5, i此时为3, 交换A[3]和A[5], 即交换8和4;
    2 1 3 4 7 8 5
8. 完成循环, 交换A[3+1]和A[6];
    2 1 3 4 5 8 7
此时数组分成 {2, 1, 3, 4}, {5}, {8,7} 三个部分.

Note: 由Step 2, 3 可以看出, 当开始的元素小于基准值时, 自身被迫与自身交换. 可以做简化至 判断 j 和 i当前是否相等 决定是否进行交换. 不过这事实上不会影响排序的效率.
快速排序的平均时间复杂度是O(nlgn), 最坏可能达到O(n2)

上述的Partition算法是我们比较常用的. 它采用每个序列的最后一个元素来作为划分基准. 没有任何理由判定划分以后的两个子序列是否平衡(长度是否相差不多或者相等). 理论上来说, 越平衡的划分, 快速排序的时间复杂度就越无限逼近O(nlgn). 所以优化快速排序的基本方向,就是追求更加平衡的partition函数.
另外一种我们教材常见的partition函数由Hoare给出, C#实现如下:
public void Partition( int[] A, int begin, int end )
{
 
int judgeValue = A[begin];
int i = begin - 1 ;
int j = end + 1 ;
 
while(true)
{
do
{
j --; 
}while( A[j] > judgeValue )
do
{
i ++;
}while( A[i] < judgeValue ) 
 
if ( i < j )
{
int tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
else
return j;
}
}
 
以上Partition函数的一次排序的过程如下:
原数组: 3 1 7 8 2 4 5 (为了示例方便,调整了一下上面的示例数组)
1.选中第一个元素 3 为判断标准, 此时 bengin = 0 ; end = 6; i = -1; j = 7
2. j从最后向前寻找第一个不大于3的数, 找到A[4] = 2; 此时j = 4;
    i从最前向后寻找第一个不小于3的数, 找到A[2] = 7; 此时i = 2;
3. 交换A[4]和A[2], 数组成为 3 1 2 8 7 4 5
4. j继续向前寻找不大于3的数, 找到A[2]= 1; 此时j = 2;
    i继续向后寻找不小于3的数,找到A[3] = 8; 此时i = 3;
5. 此时i > j, 数组被分为3, 1,2 和8, 7, 4, 5两部分.返回2.

实际上,在寻求优化分区函数的过程中,常使用随即函数来在当前数组中随机取得一个判断基准点. 这样可以使快速排序更好的接近平均效率.
一个可能的版本是:
public void RandomPartition( int[] A, int begin , int end )
{
int i = Random( begin , end ) ;
 
// exchange the random element with the last element.
int tmp = A [i];
A[i] = A[end];
A[end] = tmp;
 
return Partition( A, begin, end );
}
作者:Jeffrey Sun
出处:http://sun.cnblogs.com/
本文以“现状”提供且没有任何担保,同时也没有授予任何权利。本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

No comments:

Post a Comment