cheney23reg

技术博客

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

快速排序的基本思想是基于分治策略的。对于输入的子序列L[p..r],如果规模足够小则直接进行排序,否则分三步处理:

1>分解(Divide):将输入的序列L[p..r]划分成两个非空子序列L[p..q]和L[q+1..r],使L[p..q]中任一元素的值不大于L[q+1..r]中任一元素的值。
2>递归求解(Conquer):通过递归调用快速排序算法分别对L[p..q]和L[q+1..r]进行排序。
3>合并(Merge):由于对分解出的两个子序列的排序是就地进行的,所以在L[p..q]和L[q+1..r]都排好序后不需要执行任何计算L[p..r]就已排好序。

这个解决流程是符合分治法的基本步骤的。因此,快速排序法是分治法的经典应用实例之一。

 

快速排序算法的常量因子比较小,所以在实际应用中,快速排序是利用得最多的比较排序算法。

 

算法的实现


算法Quick_Sort的实现:

注意:下面的记号L[p..r]代表线性表L从位置p到位置r的元素的集合,但是L并不一定要用数组来实现,可以是用任何一种实现方法(比如说链表),这里L[p..r]只是一种记号。

procedure Quick_Sort(p,r:position;var L:List);
const e=12;
var
q:position;
begin
(
1) if r-p<=e then Insertion_Sort(L,p,r) // 若L[p..r]足够小则直接对L[p..r]进行插入排序
else begin
(
2) q:=partition(p,r,L); // 将L[p..r]分解为L[p..q]和L[q+1..r]两部分
(3) Quick_Sort(p,q,L); // 递归排序L[p..q]
(4) Quick_Sort(q+1,r,L); // 递归排序L[q+1..r]
end;
end;

 

 

对线性表L[1..n]进行排序,只要调用Quick_Sort(1,n,L)就可以了。算法首先判断L[p..r]是否足够小,若足够小则直接对L[p..r]进行排序。

 

Sort可以是任何一种简单的排序法,一般用插入排序。这是因为,对于较小的表,快速排序中划分和递归的开销使得该算法的效率还不如其它的直接排序法好。

 

至于规模多小才算足够小,并没有一定的标准,因为这跟生成的代码和执行代码的计算机有关,可以采取试验的方法确定这个规模阈值。经验表明,在大多数计算机上,取这个阈值为12较好,也就是说,当r-p<=e=12即L[p..r]的规模不大于12时,直接采用插入排序法对L[p..r]进行排序(参见 Sorting and Searching Algorithms: A Cookbook)。当然,比较方便的方法是取该阈值为1,当待排序的表只有一个元素时,根本不用排序(其实还剩两个元素时就已经在Partition函数中排好序了),只要把第1行的if语句该为 "if p=r then exit else ...",这就是通常教科书上看到的快速排序的形式。

 

算法Quick_Sort中调用了一个函数partition,该函数主要实现以下两个功能:

1.在L[p..r]中选择一个支点元素pivot;
2.对L[p..r]中的元素进行整理,使得L[p..q]分为两部分L[p..q]和L[q+1..r],并且L[p..q]中的每一个元素的值不大于pivot,L[q+1..r]中的每一个元素的值不小于pivot,但是L[p..q]和L[q+1..r]中的元素并不要求排好序。

快速排序法改进性能的关键就在于上述的第二个功能,因为该功能并不要求L[p..q]和L[q+1..r]中的元素排好序。

 

函数partition可以实现如下。以下的实现方法是原地置换的,当然也有不是原地置换的方法,实现起来较为简单,这里就不介绍了。 

该算法的实现很精巧。其中,有一些细节需要注意。例如,算法中的位置i和j不会超出A[p..r]的位置界,并且该算法的循环不会出现死循环,如果将两个repeat语句换为while则要注意当L[i]=L[j]=pivot且i<j时i和j的值都不再变化,会出现死循环。

 

另外,最后一个if..then..语句很重要,因为如果pivot取的不好,使得Partition结束时j正好等于r,则如前所述,算法Quick_Sort会无限递归下去;因此必须判断j是否等于r,若j=r则返回j的前驱。

 

以上算法的一个执行实例如下图所示,其中pivot=L[p]=5:

 

Partition对L[p..r]进行划分时,以pivot作为划分的基准,然后分别从左、右两端开始,扩展两个区域L[p..i]和L[j..r],使得L[p..i]中元素的值小于或等于pivot,而L[j..r]中元素的值大于或等于pivot。初始时i=p-1,且j=i+1,从而这两个区域是空的。在while循环体中,位置j逐渐减小,i逐渐增大,直到L[i]≥pivot≥L[j]。如果这两个不等式是严格的,则L[i]不会是左边区域的元素,而L[j]不会是右边区域的元素。此时若i在j之前,就应该交换L[i]与L[j]的位置,扩展左右两个区域。 while循环重复至i不再j之前时结束。这时L[p..r]己被划分成L[p..q]和L[q+1..r],且满足L[p..q]中元素的值不大于L[q+1..r]中元素的值。在过程Partition结束时返回划分点q。

 

寻找支点元素select_pivot有多种实现方法,不同的实现方法会导致快速排序的不同性能。根据分治法平衡子问题的思想,我们希望支点元素可以使L[p..r]尽量平均地分为两部分,但实际上这是很难做到的。下面我们给出几种寻找pivot的方法。

1>选择L[p..r]的第一个元素L[p]的值作为pivot;
2>选择L[p..r]的最后一个元素L[r]的值作为pivot;
3>选择L[p..r]中间位置的元素L[m]的值作为pivot;
4>选择L[p..r]的某一个随机位置上的值L[random(r-p)+p]的值作为pivot;


按照第4种方法随机选择pivot的快速排序法又称为随机化版本的快速排序法,该方法具有平均情况下最好的性能,在实际应用中该方法的性能也是最好的。

 

性能分析

 

快速排序是非稳定的排序算法。它的排序时间复杂度如下:

 

1)最坏情况时间复杂度:T(n) = θ( n*n )

最坏情况发生在每次划分过程产生的两个区间分别包含n-1个元素和1个元素的时候(设输入的表有n个元素)

 

2)最好情况时间复杂度:T(n) = θ( n*log(n) )

最好情况发生在每次划分过程产生的区间大小都为n/2

 

3)平均情况时间复杂度:T(n) = θ( n*log(n) )

 

算法实现

 

 以下是快速排序算法的递归实现(C++/C): 

/**
* 将 list[p...r] 分解为 list[p...q] 和 list[q+1...r] 两部分
*/
template
<typename T>
static unsigned int partition(
T
* list,
unsigned
int low,
unsigned
int high,
int (*compare)(T a, T b)
);



 
//------------------------------------------------------------------------------
// quickSort
//------------------------------------------------------------------------------
//
//template <class T>
template <typename T>
EXPORT_C
int quickSort(
T
* list,
unsigned
int low,
unsigned
int high,
int (*compare)(T a, T b)
)
{
int result = 0;
int count = high - low + 1;
int q = 0;

if(count <= 0) // 错误: 数列里面没有元素
{
return -1;
}
else if(1 == count) // 数列里面只有一个元素, 默认就已经排好序了
{
return 0;
}

// 分割待排序的数列
q = partition(list, low, high, compare);


// 分别对子数列进行排序

if( (result = quickSort(list, low, q, compare)) != 0)
return result;

result
= quickSort(list, q, high, compare);

return result;
}




//------------------------------------------------------------------------------
// partition
//------------------------------------------------------------------------------
//
template <typename T>
unsigned
int partition(
T
* list,
unsigned
int low,
unsigned
int high,
int (*compare)(T a, T b)
)
{
// 取得基准元素
T * pPivot = ( list + ((low + high) / 2) );

unsigned
int i = low;
unsigned
int j = high;

T
* pA = list + i;
T
* pB = list + j;

T
* pTmp = (T *)(malloc( sizeof(T) ));

while(true)
{
// 从低位到高位寻找第一个大于(>)基准元素的元素
while( i <= high && compare( *pA, *pPivot ) <= 0 )
{
i
++;
pA
= list + i;
}
// 从高位到低位寻找第一个小于(<)基准元素的元素
while( j >= low && compare( *pB, *pPivot ) >= 0 )
{
j
--;
pB
= list + j;
}

// 交换元素
if(i < j) // swap
{
memcpy( pTmp, list
+ i, sizeof(T) );
memcpy( list
+ i, list + j, sizeof(T) );
memcpy( list
+ j, pTmp, sizeof(T) );
}
// 满足结束条件, 结束分割操作
else // i >= j
{
break;
}
};

free(pTmp);

// if(i > j)
// return j;
// else // i == j
return j; // 返回分割点
}

 

 

function partition(p,r:position;var L:List):position;
var
pivot:ElementType;
i,j:position;
begin
1 pivot:=Select_Pivot(p,r,L); // 在L[p..r]中选择一个支点元素pivot
2 i:=p-1;
3 j:=r+1;
4 while true do
5 begin
6 repeat j:=j-1 until L[j]<=pivot; // 移动左指针,注意这里不能用while循环
7 repeat i:=i+1 until L[i]>=pivot; // 移动右指针,注意这里不能用while循环
8 if i< j
9 then swap(L[i],L[j]) // 交换L[i]和L[j]
10 else if j<>r
11 then return j // 返回j的值作为分割点
12 else
13 return j-1; // 返回j前一个位置作为分割点
14 end;
end;

 

 

参考资料:

[1] http://www.ahhf45.com/info/Data_Structures_and_Algorithms/algorithm/commonalg/sort/internal_sorting/quick_sort/quick_sort.htm
[2] http://baike.baidu.com/view/19016.htm

[3] http://zh.wikipedia.org/zh-cn/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F

posted on 2010-08-11 20:17  cheney23reg  阅读(683)  评论(0编辑  收藏  举报