优化版快速排序
在原始的快速排序上追加了两个点。
- 引入了“分界点”,但递归进行到后期,数组足够小时,用插入排序(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
即便有重复元素,也没有出错