QuickSort

快速排序 Quick Sort

我们已经知道,在决策树计算模型下,任何一个基于比较来确定两个元素相对位置的排序算法需要Ω(nlogn)计算时间。如果我们能设计一个需要O(n1ogn)时间的排序算法,则在渐近的意义上,这个排序算法就是最优的。许多排序算法都是追求这个目标。

下面介绍快速排序算法,它在平均情况下需要O(nlogn)时间。这个算法是由C.A.R.Hoare发明的。

算法的基本思想

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

  • 分解(Divide):将输入的序列L[p..r]划分成两个非空子序列L[p..q]和L[q+1..r],使L[p..q]中任一元素的值不大于L[q+1..r]中任一元素的值。
  • 递归求解(Conquer):通过递归调用快速排序算法分别对L[p..q]和L[q+1..r]进行排序。
  • 合并(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中变量q的值一定不能等于r,否则该过程会无限递归下去,永远不能结束。因此下文中在partition函数里加了限制条件,避免q=r情况的出现。

算法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可以实现如下。以下的实现方法是原地置换的,当然也有不是原地置换的方法,实现起来较为简单,这里就不介绍了。

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
     begin
5      repeat j:=j-1 until L[j]<=pivot;  //移动左指针,注意这里不能用while循环
6      repeat i:=i+1 until L[i]>=pivot;  //移动右指针,注意这里不能用while循环
7      if i< j then swap(L[i],L[j])  //交换L[i]和L[j]
8              else if j<>r then return j        //返回j的值作为分割点
9                           else return j-1;     //返回j前一个位置作为分割点
     end;
end;

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

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

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

图1 Partition过程的一个执行实例

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的快速排序法又称为随机化版本的快速排序法,在下面的复杂性分析中我们将看到该方法具有平均情况下最好的性能,在实际应用中该方法的性能也是最好的。

 

// 快速排序一
private   static   void  quicksort(String[] a, int  lo0, int  hi0) {
  
int lo=lo0;
  
int hi=hi0;
  
  
if(lo>=hi)
  
return;
  
//取中间元素
  String mid=a[(lo+hi)/2];
  
while(lo<hi){
         
//从左边找到第一个a[lo]>mid的元素
         while(lo<hi&&a[lo].compareTo(mid)<0){
    
          lo
++;
          }

      
//从右边找到第一个a[hi]
      while(lo<hi&&a[hi].compareTo(mid)>0){
      
          hi
--;}

     
//如果lo在hi左边则交换2个元素 满足小的在左 大的在右
     if(lo<hi){
     String T
=a[lo];
     a[lo]
=a[hi];
     a[hi]
=T;
     
//继续找
     lo++;
     hi
--;
  }

  
  
if(hi<lo){
  
int T=hi;
  hi
=lo;
  lo
=T;
  }

  
//快排左
  quicksort(a,lo0,lo);
  
//快排右
  quicksort(a,lo==lo0?lo+1:lo,hi0);
  }

  }


  
     
// 快速排序之二
     
// 交换a[i],a[j]的值
      public   void  swap( int [] a, int  i, int  j)
       
{
        
int temp = a[i];
        a[i]
=a[j];
        a[j] 
= temp;
       }

       
// 找出中间点位置 使得中间点左边的值《=中间点〈=中间点右边的值
        public   int  partition( int [] a, int  low, int  high)
       
{
          
//中间点元素值
         int pivot;
        
//中间点位置
         int p_pos,tmp,i,j;
        
//假设中间点位置在第一位
        p_pos = low;
       
//假设中间点值为第一位元素的值 注意中间点元素的值是确定的 假设第一位 但是中间点元素的正确位置需要我们找
        pivot = a[p_pos];
        
//循环数组 
        for(i=low+1;i<=high;i++)
        
{
          
//从第二位开始查找 发现i位元素小于中间点值
         if(a[i]<pivot)
         
{
          
//中间点位置右移一位
          p_pos++;
         
//交换此刻中间点位置所在元素与第i位元素的值
          swap(a,p_pos,i);
         }

        }

        
//此时已经找到中间点元素所在正确位置 将中间点元素位置的值与假设第一位为中间点值交换 
        swap(a,low,p_pos);
        
//返回该中间点位置
       return p_pos;
       }

       
// 快排
        public   void  quicksort( int [] a, int  low, int  high)
       
{
        
        
int pivot;
        
//如果低位值还小于高位值
        if(low<high)
        
{
        
//得到低位与高位的中间点
         pivot = partition(a,low,high);
         
//第归快排这个中间点左边
         quicksort(a,low,pivot-1);
         
//第归快排这个中间点右边
         quicksort(a,pivot+1,high);
        }

        
       }

 

原文

http://algorithm.diy.myrice.com/algorithm/commonalg/sort/internal_sorting/quick_sort/quick_sort.htm

你可能感兴趣的:(QuickSort)