Java(.NET)经典排序算法之快速排序

一、算法思想
      快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。


(1) 分治法的基本思想
      分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。


(2)快速排序的基本思想
      设当前待排序的无序区为R[low..high],利用分治法可将快速排序的基本思想描述为:
①分解: 
     在R[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)和R[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字pivot.key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置
(pivotpos)上,它无须参加后续的排序。
  注意:
     划分的关键是要求出基准记录所在的位置pivotpos。划分的结果可以简单地表示为(注意pivot=R[pivotpos]):
      R[low..pivotpos-1].keys≤R[pivotpos].key≤R[pivotpos+1..high].keys
                  其中low≤pivotpos≤high。


②求解: 

     通过递归调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。


③组合: 

     因为当"求解"步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,"组合"步骤无须做什么,可看作是空操作。


二、快速排序算法QuickSort

[cpp]  view plain copy
  1. void QuickSort(SeqList R,int low,int high)  
  2.  { //对R[low..high]快速排序  
  3.    int pivotpos; //划分后的基准记录的位置  
  4.    if(low<high){//仅当区间长度大于1时才须排序  
  5.       pivotpos=Partition(R,low,high); //对R[low..high]做划分  
  6.       QuickSort(R,low,pivotpos-1); //对左区间递归排序  
  7.       QuickSort(R,pivotpos+1,high); //对右区间递归排序  
  8.     }  
  9.   } //QuickSort  


  注意:
     为排序整个文件,只须调用QuickSort(R,1,n)即可完成对R[l..n]的排序。


三、划分算法Partition
(1)简单的划分方法

具体做法
  第一步:(初始化)设置两个指针i和j,它们的初值分别为区间的下界和上界,即i=low,i=high;选取无序区的第一个记录R[i](即R[low])作为基准记录,并将它保存在变量pivot中;
  第二步:令j自high起向左扫描,直到找到第1个关键字小于pivot.key的记录R[j],将R[j])移至i所指的位置上,这相当于R[j]和基准R[i](即pivot)进行了交换,使关键字小于基准关键字pivot.key的记录移到了基准的左边,交换后R[j]中相当于是pivot;然后,令i指针自i+1位置开始向右扫描,直至找到第1个关键字大于pivot.key的记录R[i],将R[i]移到i所指的位置上,这相当于交换了R[i]和基准R[j],使关键字大于基准关键字的记录移到了基准的右边,交换后R[i]中又相当于存放了pivot;接着令指针j自位置j-1开始向左扫描,如此交替改变扫描方向,从两端各自往中间靠拢,直至i=j时,i便是基准pivot最终的位置,将pivot放在此位置上就完成了一次划分。


②一次划分过程
     一次划分过程中,具体变化情况【参见动画演示

( http://student.zjzk.cn/course_ware/data_structure/web/flashhtml/kuaisupaixu.htm)

③划分算法:
 

[cpp]  view plain copy
  1. int Partition(SeqList R,int i,int j)  
  2.    {//调用Partition(R,low,high)时,对R[low..high]做划分,  
  3.     //并返回基准记录的位置  
  4.      ReceType pivot=R[i]; //用区间的第1个记录作为基准 '  
  5.      while(i<j){ //从区间两端交替向中间扫描,直至i=j为止  
  6.        while(i<j&&R[j].key>=pivot.key) //pivot相当于在位置i上  
  7.          j--; //从右向左扫描,查找第1个关键字小于pivot.key的记录R[j]  
  8.        if(i<j) //表示找到的R[j]的关键字<pivot.key  
  9.            R[i++]=R[j]; //相当于交换R[i]和R[j],交换后i指针加1  
  10.        while(i<j&&R[i].key<=pivot.key) //pivot相当于在位置j上  
  11.            i++; //从左向右扫描,查找第1个关键字大于pivot.key的记录R[i]  
  12.        if(i<j) //表示找到了R[i],使R[i].key>pivot.key  
  13.            R[j--]=R[i]; //相当于交换R[i]和R[j],交换后j指针减1  
  14.       } //endwhile  
  15.      R[i]=pivot; //基准记录已被最后定位  
  16.      return i;  
  17.    } //partition  


四、快速排序执行过程
     快速排序执行的全过程可用递归树来描述。





分析: 
     (1)递归执行的路线如图中带箭头的包络线所示。
     (2) 递归树上每一结点左旁方括号表示当前待排序的区间,结点内的关键字是划分的基准关键字
  注意:
     叶结点对应的子区间只有一个关键字,无须划分,故叶结点内没有基准关键字
  (3) 划分后得到的左、右两个子区间分别标在该结点的左、右两个孩子结点的左边方括号内。
【例】根结点左旁方括号[49,38,65,97,76,13,27,49]表示初始待排序的关键字,根内的49表示所选的划分基准记录的关键字,划分结果是[27,28,13]49[76,97,65,49_],其左右子区间分别标在根结点的两个孩子的左边。
     (4) 每个分支结点右旁圆括号中的内容表示对该结点左旁区间的排序过程结束之后返回的结果。它是其左右孩子对应的区间排序完成之后,将左右孩子对应的排序结果分别放在该分支结点的关键字前后所得到的关键字序列。
【例】分支结点76的左右孩子对应的区间排序后的结果分别是(49_,65)和(97),将它们分别放在76的前后即得(49,65,76,97),这是对结点76左旁区间[76,97,,65,49]排序的结果。
     (5) 算法的执行顺序是递归树中的箭头顺序,实际上当把划分操作视为访问结点的操作时,快速排序的执行过程相当于是先序遍历其递归树。
  注意:
     任何递归算法均可用递归树来描述其执行过程。


五、快速排序各次划分后的状态变化
[49 38 65 97 76 13 27 49] //初始关键字
[27 38 13] 49 [76 97 65 49] //第1次划分完成之后,对应递归树第2层
[13] 27 [38] 49 [49 65] 76 [97] //对上一层各无序区划分完成后,对应递归树第3层
13 27 38 49 49 [65] 76 97 //对上一层各无序区划分完成后,对应递归树第4层
13 27 38 49 49 65 76 97 //最后的排序结果




六、算法分析
     快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。


(1)最坏时间复杂度
     最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。
     因此,快速排序必须做n-1次划分,第i次划分开始时区间长度为n-i+1,所需的比较次数为n-i(1≤i≤n-1),故总的比较次数达到最大值:
               Cmax = n(n-1)/2=O(n2)
     如果按上面给出的划分算法,每次取当前无序区的第1个记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多。


(2)最好时间复杂度
     在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数:
        0(nlgn)
注意:
     用递归树来分析最好情况下的比较次数更简单。因为每次划分后左、右子区间长度大致相等,故递归树的高度为O(lgn),而递归树每一层上各结点所对应的划分过程中所需要的关键字比较次数总和不超过n,故整个排序过程所需要的关键字比较总次数C(n)=O(nlgn)。
     因为快速排序的记录移动次数不大于比较的次数,所以快速排序的最坏时间复杂度应为0(n2),最好时间复杂度为O(nlgn)。


(3)基准关键字的选取
     在当前无序区中选取划分的基准关键字是决定算法性能的关键。
  ①"三者取中"的规则
     "三者取中"规则,即在当前区间里,将该区间首、尾和中间位置上的关键字比较,取三者之中值所对应的记录作为基准,在划分开始前将该基准记录和该区伺的第1个记录进行交换,此后的划分过程与上面所给的Partition算法完全相同。
  ②取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准
     选取基准最好的方法是用一个随机函数产生一个取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准,这相当于强迫R[low..high]中的记录是随机分布的。用此方法所得到的快速排序一般称为随机的快速排序。
注意:
     随机化的快速排序与一般的快速排序算法差别很小。但随机化后,算法的性能大大地提高了,尤其是对初始有序的文件,一般不可能导致最坏情况的发生。算法的随机化不仅仅适用于快速排序,也适用于其它需要数据随机分布的算法。


(4)平均时间复杂度
     尽管快速排序的最坏时间为O(n2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。它的平均时间复杂度为O(nlgn)。


(5)空间复杂度
     快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为O(lgn),故递归后需栈空间为O(lgn)。最坏情况下,递归树的高度为O(n),所需的栈空间为O(n)。


(6)稳定性
     快速排序是非稳定的,例如[2,2,1]。



七、代码实现

[java]  view plain copy
  1. /** 
  2.  * 一趟快速排序的算法是:  
  3.         1)、设置两个变量I、J,排序开始的时候I:=1,J:=N;  
  4.         2)以第一个数组元素作为关键数据,赋值给X,即X:=A[1];  
  5.         3)、从J开始向前搜索,即由后开始向前搜索(J:=J-1),找到第一个小于X的值,两者交换;  
  6.         4)、从I开始向后搜索,即由前开始向后搜索(I:=I+1),找到第一个大于X的值,两者交换;  
  7.         5)、重复第3、4步,直到I=J;  
  8.  */  
  9. public class QuickSort {  
  10.     public static void main(String[] args) {  
  11.         int[] source = { 49386597761327};  
  12.         System.out.print("初始关键字:");  
  13.         printArray(source);  
  14.         System.out.println("");  
  15.           
  16.         quickSort(source, 0, source.length - 1);  
  17.           
  18.         System.out.print("\n\n排序后结果:");  
  19.         printArray(source);  
  20.     }  
  21.   
  22.     /* 
  23.      * 先按照数组为数据原型写出算法,再写出扩展性算法。数组{49,38,65,97,76,13,27} 
  24.      */  
  25.     public static void quickSort(int[] source, int low, int high) {  
  26.         int pivotPos;// 划分后的基准记录的位置  
  27.         if (low < high) {  
  28.             pivotPos = partition(source, low, high);//对source数组做划分  
  29.             quickSort(source, low, pivotPos - 1);// 对左区间递归排序  
  30.             quickSort(source, pivotPos + 1, high);// 对右区间递归排序  
  31.         }  
  32.     }  
  33.   
  34.     public static int partition(int[] source, int low, int high) {  
  35.         int pivot = source[low];// 用区间的第1个记录作为基准  
  36.         while (low < high) {// 从区间两端交替向中间扫描,直至low=high为止  
  37.             while (low < high && source[high] >= pivot) { // 高位找到比povite大,则符合要求,继续寻找    
  38.                 high--;//从右向左扫描,查找第1个关键字小于pivot的记录source[j]  
  39.             }  
  40.             if (low < high) {//表示找到的source[j]的关键字<pivot  
  41.                 source[low++] = source[high];// 相当于交换source[i]和source[j],交换后i加1  
  42.             }  
  43.             System.out.print("从右向左扫描<----"+"  这趟排序结果:");  
  44.             printArray(source);  
  45.             while (low < high && source[low] <= pivot) {//低位开始找到比povite小,符合要求,继续寻找    
  46.                 low++; //从左向右扫描,查找第1个关键字大于pivot的记录source[i]  
  47.             }  
  48.             if (low<high) {//表示找到了source[i],使source[i]>pivot  
  49.                 source[high--]=source[low];//相当于交换source[i]和source[j],交换后j减1  
  50.             }  
  51.             System.out.print("从左向右扫描---->"+"  这趟排序结果:");  
  52.             printArray(source);  
  53.         }  
  54.         // 当low == high,完成一趟快速排序,此时low位相当于空,等待pivot补上  
  55.         source[low] = pivot;//基准记录已被最后定位  
  56.           
  57.         System.out.print("pivot="+pivot+"          这趟排序结果:");  
  58.         printArray(source);  
  59.         System.out.println("\n");  
  60.         return low;  
  61.     }  
  62.       
  63.       
  64.     public static void printArray(int[] source) {  
  65.         for (int i = 0; i < source.length; i++) {  
  66.             System.out.print("\t" + source[i]);  
  67.         }  
  68.         System.out.println();  
  69.     }  
  70. }  




八、运行结果

[java]  view plain copy
  1. 初始关键字:  49  38  65  97  76  13  27  
  2.   
  3. 从右向左扫描<----  这趟排序结果: 27  38  65  97  76  13  27  
  4. 从左向右扫描---->  这趟排序结果: 27  38  65  97  76  13  65  
  5. 从右向左扫描<----  这趟排序结果: 27  38  13  97  76  13  65  
  6. 从左向右扫描---->  这趟排序结果: 27  38  13  97  76  97  65  
  7. 从右向左扫描<----  这趟排序结果: 27  38  13  97  76  97  65  
  8. 从左向右扫描---->  这趟排序结果: 27  38  13  97  76  97  65  
  9. pivot=49          这趟排序结果:   27  38  13  49  76  97  65  
  10.   
  11.   
  12. 从右向左扫描<----  这趟排序结果: 13  38  13  49  76  97  65  
  13. 从左向右扫描---->  这趟排序结果: 13  38  38  49  76  97  65  
  14. pivot=27          这趟排序结果:   13  27  38  49  76  97  65  
  15.   
  16.   
  17. 从右向左扫描<----  这趟排序结果: 13  27  38  49  65  97  65  
  18. 从左向右扫描---->  这趟排序结果: 13  27  38  49  65  97  97  
  19. pivot=76          这趟排序结果:   13  27  38  49  65  76  97  
  20.   
  21.   
  22.   
  23.   
  24. 排序后结果:  13  27  38  49  65  76  97  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值