快速排序(二) jdk源码中如何优化快速排序

  快速排序是一种相当棒的排序方案,相关理论内容可以参见快速排序(一) 原理介绍

     在jdk的[java.util.Arrays]类中,有一个sort的函数,它实现对很多数据结构进行的排序方法,其中sort(int[] a)中主要使用的是优化后的快速排序法,本文正是基于此来讲解如何优化快速排序算法。

 

java源代码:

[c-sharp] view plaincopy
  1. public class SourceSort {  
  2.     public static void main(String[] args){  
  3.         System.out.println("start");  
  4.           
  5.         int sum = 10;  
  6.         int[] nums = new int[sum];  
  7.         for(int i=0;i<sum;i++){  
  8.             nums[i] = sum-i;  
  9.         }  
  10.         System.out.println("排序前的第一个数:"+nums[0]);  
  11.         sort(nums);  
  12.         System.out.println("排序后的第一个数:"+nums[0]);  
  13.           
  14.         System.out.println("end");  
  15.     }  
  16.       
  17.       
  18.     /** 
  19.      * Tuning parameter: list size at or below which insertion sort will be 
  20.      * used in preference to mergesort or quicksort. 
  21.      */  
  22.     private static final int INSERTIONSORT_THRESHOLD = 7;  
  23.        
  24.     /** 
  25.      * Sorts the specified array of ints into ascending numerical order. 
  26.      * The sorting algorithm is a tuned quicksort, adapted from Jon 
  27.      * L. Bentley and M. Douglas McIlroy's "Engineering a Sort Function", 
  28.      * Software-Practice and Experience, Vol. 23(11) P. 1249-1265 (November 
  29.      * 1993).  This algorithm offers n*log(n) performance on many data sets 
  30.      * that cause other quicksorts to degrade to quadratic performance. 
  31.      * 
  32.      * @param a the array to be sorted 
  33.      *  
  34.      *  
  35.      */  
  36.     public static void sort(int[] a) {  
  37.         sort1(a, 0, a.length);  
  38.     }  
  39.       
  40.     /** 
  41.      * 排序的主要代买,数量少(<7)时插入排序,数量多时快速排序 
  42.      * Sorts the specified sub-array of integers into ascending order. 
  43.      */  
  44.     private static void sort1(int x[], int off, int len) {  
  45.     // Insertion sort on smallest arrays  
  46.     // 当需要排序的数量教少时,采用插入排序  
  47.     if (len < 7) {  
  48.         for (int i=off; i<len+off; i++)  
  49.         for (int j=i; j>off && x[j-1]>x[j]; j--)//这段代码很简练,重点看  
  50.             swap(x, j, j-1);  
  51.         return;  
  52.     }  
  53.       
  54.     //这里主要是用快速排序  
  55.     //精心选择划分元素,即枢轴    
  56.     //如果是小规模数组(size<=7),直接取中间元素作为枢轴    
  57.     //如果是中等规模数组(7=<size<=40),则在数组首、中、尾三个位置上的数中取中间大小的数作为枢轴    
  58.     //如果是大规模数组(size>40),则在9个指定的数中取一个伪中数(中间大小的数s)    
  59.     // Choose a partition element, v  
  60.     int m = off + (len >> 1);       // Small arrays, middle element中间位置,移位计算=”%2”  
  61.     if (len > 7) {  
  62.         int l = off;  
  63.         int n = off + len - 1;  
  64.         if (len > 40) {        // Big arrays, pseudomedian of 9  
  65.         int s = len/8;  
  66.         l = med3(x, l,     l+s, l+2*s);  
  67.         m = med3(x, m-s,   m,   m+s);  
  68.         n = med3(x, n-2*s, n-s, n);  
  69.         }  
  70.         m = med3(x, l, m, n); // Mid-size, med of 3  
  71.     }  
  72.     int v = x[m];  
  73.     // 以下是经过优化的快速排序主要代码  
  74.     // 优点:当数组中存在相同的组字时,每次排序形成 (<v)v*(>v)*的,步骤分1,2  
  75.     // 1.形成 v* (<v)* (>v)* v*  
  76.     // Establish Invariant: v* (<v)* (>v)* v*  
  77.     int a = off, b = a, c = off + len - 1, d = c;  
  78.     while(true) {  
  79.         while (b <= c && x[b] <= v) {  
  80.         if (x[b] == v)  
  81.             swap(x, a++, b);  
  82.         b++;  
  83.         }  
  84.         while (c >= b && x[c] >= v) {  
  85.         if (x[c] == v)  
  86.             swap(x, c, d--);  
  87.         c--;  
  88.         }  
  89.         if (b > c)  
  90.         break;  
  91.         swap(x, b++, c--);  
  92.     }  
  93.     // Swap partition elements back to middle  
  94.     //将前后两边的v移到中间  
  95.     int s, n = off + len;  
  96.     s = Math.min(a-off, b-a  );  vecswap(x, off, b-s, s);  
  97.     s = Math.min(d-c,   n-d-1);  vecswap(x, b,   n-s, s);  
  98.     // Recursively sort non-partition-elements  
  99.     if ((s = b-a) > 1)  
  100.         sort1(x, off, s);  
  101.     if ((s = d-c) > 1)  
  102.         sort1(x, n-s, s);  
  103.     }  
  104.       
  105.     /** 
  106.      * Swaps x[a] with x[b]. 
  107.      * 交换x[a]和x[b] 
  108.      */  
  109.     private static void swap(int x[], int a, int b) {  
  110.     int t = x[a];  
  111.     x[a] = x[b];  
  112.     x[b] = t;  
  113.     }  
  114.     /** 
  115.      * Swaps x[a .. (a+n-1)] with x[b .. (b+n-1)]. 
  116.      */  
  117.     private static void vecswap(int x[], int a, int b, int n) {  
  118.     for (int i=0; i<n; i++, a++, b++)  
  119.         swap(x, a, b);  
  120.     }  
  121.       
  122.     /** 
  123.      * Returns the index of the median of the three indexed integers. 
  124.      * 在x[a],x[b],x[c]中找到大小在中间的数字 
  125.      */  
  126.     private static int med3(int x[], int a, int b, int c) {  
  127.     return (x[a] < x[b] ?  
  128.         (x[b] < x[c] ? b : x[a] < x[c] ? c : a) :  
  129.         (x[b] > x[c] ? b : x[a] > x[c] ? c : a));  
  130.     }  
  131.       
  132. }  
 

 

★ 优化1:在小规模(size<7)数组中,直接插入排序的效率要比快速排序高。

      没有一种排序在任何情况下都是最优的《基于比较的内部排序总结 》。 O(N^2)级别的排序看起来似乎比所有先进排序要差的多。但实际上也并非如此,Arrays中的sort()算法就给了我们一个很好的例子。当待排数组规模非常小的时候(JDK中规模的阈值为INSERTIONSORT_THRESHOLD=7),直接插入排序反而要比快排,归并排序要好。

           这个道理很简单。数组规模小,简单算法的比较次数不会比先进算法多多少。相反,诸如快排,归并排序等先进算法使用递归操作,所付出的运行代价更高。

 

★ 优化2:精心选择划分元素,即枢轴。

      快排有一种最差的情况,即蜕化成效率最差的起跑排序(见《 交换排序 》)。 导致这种情况产生的主要原因就是枢轴的选择并不能把整个数组划分成两个大致相等的部分。比如对于基本有序的数组,选择第一个元素作为枢轴就会产生这种蜕化。

      既然如此,我们可以看看Arryas.sort()是如何为我们选择枢轴的。

      ● 如果是小规模数组(size<=7),直接取中间元素作为枢轴。      

      ● 如果是中等规模数组(7=<size<=40),则在数组首、中、尾三个位置上的数中取中间大小的数作为枢轴
      ● 如果是大规模数组(size>40),则在9个指定的数中取一个伪中数(中间大小的数s)

      中小规模时,这种取法尽量可以避免数组的较小数或者较大数成为枢轴。值得一提的是大规模的时候,首先在数组中寻找9个数据(可以通过源代码发现这9个数据的位置较为平均的分布在整个数组上);然后每3个数据找中位数;最后在3个中位数上再找出一个中位数作为枢轴。

      仔细想想,这种精心选择的枢轴,使得快排的最差情况成为了极小概率事件了。

 

★ 优化3:根据枢轴v划分,形成一个形如  (<v)*   v* (>v)* 的数组

      普通快排算法,都是使得枢轴元素移动到数组的较中间位置。枢轴之前的元素全部小于或等于枢轴,之后的元素全部大于枢轴。但与枢轴相等的元素并不能移动到枢轴附近位置。这一点在Arrays.sort()算法中有很大的优化。

      我们举个例子来说明Arrays的优化细节       15、93、15、41、6、15、22、7、15、20

      第一次枢轴:v=15

      阶段一,形成 v* (<v)* (>v)* v* 的数组:

                                          15、15、 7、6、 41、20、22、93、 15、15

                  我们发现,与枢轴相等的元素都移动到了数组的两边。而比枢轴小的元素和比枢轴大的元素也都区分开来了。

      阶段二,将枢轴和与枢轴相等的元素交换到数组中间的位置上

                                          7、6、 15、15、 15、15、 41、20、22、93

      阶段三,递归排序与枢轴不相等都元素区间{7、6}和{41、20、22、93}

      仔细想想,对于重复元素较多的数组,这种优化无疑能到达更好的效率。

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