优化版快速排序

在原始的快速排序上追加了两个点。

  • 引入了“分界点”,但递归进行到后期,数组足够小时,用插入排序(InsertionSort)效率更好
  • 准基元素并不是单纯的选择数组的第0位,而是使用三数取中法求出基准元素
  1 /**
  2  * 快速排序(三数取中法 + 插入排序优化)
  3  */
  4 public class QuickSort<T extends Comparable<? super T>> {
  5 
  6     /**
  7      * 分界点
  8      * 数字5~20皆可,这里选择了数字10
  9      */
 10     private static final int CUTOFF = 10; 
 11 
 12     /** 
 13      * 入口方法
 14      * 参数个数仅一个,对外友好
 15      */
 16     public void sort(T[] arr) {
 17         sort(arr, 0, arr.length - 1);
 18     }
 19 
 20     /**
 21      * 快速排序
 22      * 此方法会不断递归自身,一次递归代表一趟排序
 23      */
 24     private void sort(T[] arr, int left, int right) {
 25         // 数组长度在10以上时,使用快速排序更合适,否则使用插入排序
 26         if (left + CUTOFF < right) {
 27             // 算出基准元素
 28             T pivot = median3(arr, left, right);
 29             // 定义游标i, j,由于最左和最右的元素经过三数取中法之后
 30             // 已经确保了前者小于基准元素,后者大于基准元素
 31             // 所以游标应该越过他们
 32             int i = left;
 33             int j = right - 1;
 34             // 游标开始移动,直到i不在j的左侧
 35             for(;;) {
 36                 // 游标i不断向右侧移动,直到所指向的元素不再小于基准元素,才停止
 37                 while (arr[++i].compareTo(pivot) < 0) {}
 38                 // 游标j不断向左侧移动,直到所指向的元素不再大于基准元素,才停止
 39                 while (arr[--j].compareTo(pivot) > 0) {}
 40                 // i还在j的左侧,而且游标各自指向一个小于基准和大于基准的元素,
 41                 if (i < j) {
 42                     // 大于基准的元素换到右侧,小于基准的元素换到左侧
 43                     swapRef(arr, i, j);
 44                 // 这种情况指的是i, j擦身而过才停止,那么i已经进入了j曾走过的路径,j也如此,
 45                 // 说明小于基准元素的元素和大于基准元素的元素已经分离,分离点正是i
 46                 } else {
 47                     // 既然已经分离了,那么游标的工作就结束了
 48                     break;
 49                 }
 50             }
 51             // 将i指向的元素,和基准元素互换,达到以下示意的效果
 52             // [  *小于基准的元素们*  ], [基准元素], [  *大于基准的元素们*  ]
 53             swapRef(arr, i, right - 1);
 54             // 以基准元素为分割,前后分治
 55             sort(arr, left, i - 1);
 56             sort(arr, i + 1, right);
 57         } else {
 58             insertionSort(arr, left, right);
 59         }
 60     }
 61 
 62     /** 插入排序 */
 63     private void insertionSort(T[] arr, int left, int right) {
 64         // 声明一下索引j,应该循环外要用到。
 65         int j;
 66         // 这里一次循环代表选择排序一趟
 67         // 选择排序的核心表示第i趟确保从头至第i号元素顺序是正确的
 68         // 所以应该从从头开始的left+1排序到right
 69         // 而i = left那趟是没有必要的,因为只有一个元素
 70         for (int i = left + 1; i <= right; i++) {    // 这里犯了一次错,i=right的情况也要考虑到,否则最后那个元素的那趟就漏过去了。
 71             // 每一趟都会比上一趟多一个元素,它希望去“插队”,插入合适的位置
 72             T selected = arr[i];
 73             // 找位置中……(并准备插队)
 74             for (j = i; j > 0 && arr[j - 1].compareTo(selected) > 0; j--) {
 75                 // 准备插队的过程中,比插队元素大的元素应该往后走一位,效果就是一个“空位”不断向左移动。
 76                 arr[j] = arr[j - 1];
 77             }
 78             // “空位”停止了移动,因为“空位”前方的元素比插队元素小,
 79             // 而“空位”后方也是比插队元素大的最后一个元素,
 80             // 插队元素完成了插队,该趟插入排序结束
 81             arr[j] = selected;
 82         }
 83     }
 84 
 85     /** 三分法取基准元素 */
 86     private T median3(T[] arr, int left, int right) {
 87         int center = (left + right) / 2;
 88         // 1、确保最左元素小于中间的元素
 89         if (arr[left].compareTo(arr[center]) > 0) {
 90             swapRef(arr, left, center);
 91         }
 92         // 2、确保最左元素小于最右元素(结合1、2,最左元素是最小的了)
 93         if (arr[left].compareTo(arr[right]) > 0) {
 94             swapRef(arr, left, right);
 95         }
 96         // 3:确保中间元素大于最右元素(结合2、3,最右元素是最大的了)(结合2、3括号里的内容,中间元素是中间值)
 97         if (arr[center].compareTo(arr[right]) > 0) {
 98             swapRef(arr, center, right);
 99         }
100         // 基准元素移动到倒数第二位,为了不阻挡后续游标i, j的移动。 
101         swapRef(arr, center, right - 1);
102         return arr[right - 1];
103     }
104 
105     /** 元素引用交换 */
106     private void swapRef(T[] arr, int index, int indexEx) {
107         T temp = arr[index];
108         arr[index] = arr[indexEx];
109         arr[indexEx] = temp;
110     }
111 
112 }

测试一下

    public static void main(String[] args) {
        // 生成一个长度为100,元素在0~50随机分布的列表
        Random random = new Random();
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < 100; i++) {
            list.add(random.nextInt(50));
        }
        System.out.println(list);

        Integer[] arr = list.toArray(new Integer[0]);
        QuickSort<Integer> sorter = new QuickSort<Integer>();
        sorter.sort(arr);
        for (Integer ele : arr) {
            System.out.print(ele + " ");
        }
    }

结果

[23, 11, 40, 30, 17, 27, 23, 11, 7, 44, 48, 34, 13, 8, 39, 44, 12, 7, 8, 18, 0, 34, 1, 2, 4, 9, 30, 4, 21, 22, 33, 0, 6, 3, 33, 20, 5, 13, 30, 8, 38, 45, 39, 10, 28, 39, 17, 24, 45, 37, 48, 4, 20, 20, 24, 47, 32, 23, 7, 18, 13, 22, 47, 28, 41, 5, 26, 10, 3, 32, 30, 14, 12, 31, 3, 18, 26, 6, 17, 40, 6, 1, 47, 36, 14, 46, 8, 5, 34, 8, 4, 2, 18, 40, 28, 1, 0, 47, 34, 21]
0 0 0 1 1 1 2 2 3 3 3 4 4 4 4 5 5 5 6 6 6 7 7 7 8 8 8 8 8 9 10 10 11 11 12 12 13 13 13 14 14 17 17 17 18 18 18 18 20 20 20 21 21 22 22 23 23 23 24 24 26 26 27 28 28 28 30 30 30 30 31 32 32 33 33 34 34 34 34 36 37 38 39 39 39 40 40 40 41 44 44 45 45 46 47 47 47 47 48 48 

即便有重复元素,也没有出错

原文地址:https://www.cnblogs.com/deolin/p/6832398.html