快速排序(一) 原理介绍

一、基本思想 
      快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

 

二、具体实现

      设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。一趟快速排序的算法是: 

  1)设置两个变量I、J,排序开始的时候:I=0,J=N-1; 
  2)以第一个数组元素作为关键数据,赋值给X,即 X=A[0]; 
  3)从J开始向前搜索,即由后开始向前搜索(J=J-1),找到第一个小于X的值,让该值与X交换; 
  4)从I开始向后搜索,即由前开始向后搜索(I=I+1),找到第一个大于X的值,让该值与X交换; 

  5)重复第3、4步,直到 I=J;

 

特别说明:第3步和第4步,不同的文章有不同的描述,比如百度百科中的快速排序排序描述为:

3)从J开始向前搜索,即由后开始向前搜索(J=J-1),找到第一个小于key的值A[J],并与A[I]交换;

4)从I开始向后搜索,即由前开始向后搜索(I=I+1),找到第一个大于key的A[I],与A[J]交换;

其实两者表达的意思是一致的,因为第3步时,X就在A[I];第4步时,X就在A[J].


  例如:待排序的数组A的值分别是:(初始关键数据:X=49) 
  A[0] 、 A[1]、 A[2]、 A[3]、 A[4]、 A[5]、 A[6]: 
  49 38 65 97 76 13 27 
  进行第一次交换后: 27 38 65 97 76 13 49 
  ( 按照算法的第三步从后面开始找) 
  进行第二次交换后: 27 38 49 97 76 13 65 
  ( 按照算法的第四步从前面开始找>X的值,65>49,两者交换,此时:I=3 ) 
  进行第三次交换后: 27 38 13 97 76 49 65 
  ( 按照算法的第五步将又一次执行算法的第三步从后开始找 
  进行第四次交换后: 27 38 13 49 76 97 65 
  ( 按照算法的第四步从前面开始找大于X的值,97>49,两者交换,此时:J=4 ) 
  此时再执行第三步的时候就发现I=J,从而结束一躺快速排序,那么经过一趟快速排序之后的结果是:27 38 13 49 76 97 65,即所以大于49的数全部在49的后面,所以小于49的数全部在49的前面。 
  快速排序就是递归调用此过程——在以49为中点分割这个数据序列,分别对前面一部分和后面一部分进行类似的快速排序,从而完成全部数据序列的快速排序,最后把此数据序列变成一个有序的序列,根据这种思想对于上述数组A的快速排序的全过程如图6所示: 
  初始状态 {49 38 65 97 76 13 27} 
  进行一次快速排序之后划分为 {27 38 13} 49 {76 97 65} 
  分别对前后两部分进行快速排序 {27 38 13} 经第三步和第四步交换后变成 {13 27 38} 完成排序。 
  {76 97 65} 经第三步和第四步交换后变成 {65 76 97} 完成排序。


具体步骤如下

 

三、java实现的代码

[c-sharp] view plaincopy
  1. public class QuickSort {  
  2.       
  3.     public static void main(String[] args){  
  4.           
  5.         int size = 7;  
  6.         int[] nums = {49,38,65,97,76,13,27};  
  7.         System.out.println("原始数据:");  
  8.         System.out.println(arrayToStr(nums,size));  
  9.           
  10.           
  11.         System.out.println("排序开始");  
  12.         quickSort(nums);//排序的主函数  
  13.         System.out.println("排序结束");  
  14.         System.out.println("排序后数据:");  
  15.         System.out.println(arrayToStr(nums,size));  
  16.           
  17.     }  
  18.       
  19.     /** 
  20.      * 将数组打印出来的函数,辅助函数,无需过多关注 
  21.      * @param nums 
  22.      * @param size 
  23.      * @return 
  24.      */  
  25.     private static String arrayToStr(int[] nums,int size){  
  26.         String str = "";  
  27.         for(int i=0;i<size;i++){  
  28.             str += nums[i]+" ";  
  29.         }  
  30.         return str;  
  31.     }  
  32.       
  33.       
  34.     /** 
  35.      * 快速排序的主接口 
  36.      * @param nums 
  37.      * @param size 
  38.      */  
  39.     public static void quickSort(int[] nums){  
  40.         quickSort(nums,0,nums.length-1);  
  41.     }  
  42.       
  43.     /** 
  44.      * 快速排序的主接口,用于实现排序 
  45.      * @param nums 
  46.      * @param start 
  47.      * @param end 
  48.      */  
  49.     private static void quickSort(int[] nums,int start,int end){  
  50.         //函数的返回条件,当start=end时,排序结束  
  51.         if(end<=start){  
  52.             return;  
  53.         }  
  54.         else{  
  55.             //一遍排序,重点理解,midle表示已排好序的数字位置  
  56.             int midle = onceQuickSort(nums,start,end);  
  57.             quickSort(nums,start,midle-1);//对midle左边的数据,进行排序  
  58.             quickSort(nums,midle+1,end);//对midle右边的数据,进行排序        
  59.         }  
  60.       
  61.     }  
  62.       
  63.       
  64.     /** 
  65.      * 快速排序的重点,在一遍排序 
  66.      * 功能:数组nums[start-end],将小于nums[start]的树放在左边,大于nums[start]的树放在右边 
  67.      * 返回值是nums[start]的最终位置 
  68.      * @param nums 
  69.      * @param start 
  70.      * @param end 
  71.      * @return 
  72.      */  
  73.     private static int onceQuickSort(int[] nums,int start,int end){  
  74.         int povit = nums[start];//这次排序的参照数  
  75.         int left = start,right = end;  
  76.         //right从右到左,left从左到右移动,当left==right时,这遍排序结束  
  77.         while(left<right){  
  78.             //right从右到左开始遍历  
  79.             while(left<right && nums[right]>povit){  
  80.                 right--;  
  81.             }  
  82.             //left从左到右开始遍历  
  83.             while(left<right && nums[left]<povit){  
  84.                 left++;  
  85.             }  
  86.             //当遇到left和right都不能移动时,nums[right]和nums[left]交换位置  
  87.             if(left<right){  
  88.                 swap(nums,left,right);  
  89.             }  
  90.         }  
  91.         //此时left==right  
  92.         nums[left] = povit;  
  93.         return left;  
  94.           
  95.           
  96.     }  
  97.       
  98.       
  99.     /** 
  100.      * 交换nums[i]和nums[j]的位置 
  101.      * @param nums 
  102.      * @param i 
  103.      * @param j 
  104.      */  
  105.     private static void swap(int[] nums,int i,int j){  
  106.         int temp = nums[i];  
  107.         nums[i] = nums[j];  
  108.         nums[j] = temp;  
  109.     }  
  110.       
  111.       
  112. }  
 

 

四、时间复杂度

     快速排序的时间主要耗费在划分操作上,对长度为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)

 

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


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

原文地址:https://www.cnblogs.com/daichangya/p/12960009.html