最近花了几天重温了一下《算法(第四版)》, 重新把书中的算法实现了一下, 并且思索了一下, 把常见的比较排序算法都给优化了一下, 然后进行了性能测试. 也算是复习了一下排序算法吧.
阅读本篇之前最好有常见几种基于的比较排序算法的基础
本文内容包括:
归并排序优化及性能测试
快速排序优化及性能测试
堆排序序优化及性能测试
非比较排序介绍
源代码:
如果觉得文章写的不错, 可以关注微信公众号: Coder张小凯
内容和博客同步更新~
一.归并排序优化及性能测试 归并排序是建立在归并操作上 的一种有效的排序算法
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用:
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序; 特别的, 若将两个有序表合并成一个有序表,称为2-路归并
1.算法描述
把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列
2.动图演示(自底向上)
3.代码实现(自顶向下) MergeSort.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 package algorithm.sort;import java.util.Arrays;@SuppressWarnings("unchecked") public class MergeSort extends BaseSort { private static Comparable[] aux; private MergeSort () { } public static <K extends Comparable <K>> void sortTopDown (K[] a) { aux = new Comparable [a.length]; sortTopDown(a, 0 , a.length - 1 ); } private static <K extends Comparable <K>> void sortTopDown (K[] a, int lo, int hi) { if (hi <= lo) return ; int mid = lo + (hi - lo) / 2 ; sortTopDown(a, lo, mid); sortTopDown(a, mid + 1 , hi); merge(a, lo, mid, hi); } private static <K extends Comparable <K>> void merge (K[] a, int lo, int mid, int hi) { int i = lo, j = mid + 1 ; if (hi + 1 - lo >= 0 ) { System.arraycopy(a, lo, aux, lo, hi + 1 - lo); } for (int k = lo; k <= hi; k++) { if (i > mid) a[k] = (K) aux[j++]; else if (j > hi) a[k] = (K) aux[i++]; else if (less(aux[j], aux[i])) a[k] = (K) aux[j++]; else a[k] = (K) aux[i++]; } } }
归并排序是一种稳定的排序方法
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度, 代价是需要额外的内存空间
4.优化方法 ① 自底向上的归并排序
自底向上的归并排序(和上述图例对应的算法)不算是上述自顶向下算法的优化, 事实上在我的测试上显示, 在一般情况下它的性能反而下降;
提出这个算法的原因在于: 在链表型数据结构使用自底向上的归并排序时, 可以通过操作指针实现空间复杂度O(1)的排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public static <K extends Comparable <K>> void sortBottomUp (K[] a) { aux = new Comparable [a.length]; for (int sz = 1 , len = a.length; sz < len; sz *= 2 ) for (int lo = 0 ; lo < len - sz; lo += sz + sz) merge(a, lo, lo + sz - 1 , Math.min(lo + sz + sz - 1 , len - 1 )); } private static <K extends Comparable <K>> void merge (K[] a, int lo, int mid, int hi) { int i = lo, j = mid + 1 ; if (hi + 1 - lo >= 0 ) { System.arraycopy(a, lo, aux, lo, hi + 1 - lo); } for (int k = lo; k <= hi; k++) { if (i > mid) a[k] = (K) aux[j++]; else if (j > hi) a[k] = (K) aux[i++]; else if (less(aux[j], aux[i])) a[k] = (K) aux[j++]; else a[k] = (K) aux[i++]; } }
② 针对多个方面进行归并排序的优化
针对自底向上和自顶向下的归并排序可以进行的优化有:
对小规模子数组采用插入排序:
递归对于小规模的数组将产生过多的小数组甚至是空数组调用栈
测试数组是否已经有序:
若a[mid]<=a[mid+1], 认为数组已经有序, 可以跳过merge()方法
不将元素复制到辅助数组aux:
调用两种排序方法: 一种将数据从输入数组排序到辅助数组, 另一个方法反之;
将这三个优化作用在自顶向下的归并排序中, 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 private static final int THRESHOLD = 7 ;public static <K extends Comparable <K>> void advancedSort (K[] a) { var helper = Arrays.copyOf(a, a.length); advancedSort(helper, a, 0 , a.length - 1 ); } private static <K extends Comparable <K>> void advancedSort (K[] src, K[] dst, int lo, int hi) { if (hi <= lo + THRESHOLD) { insertionSort(dst, lo, hi); return ; } int mid = lo + (hi - lo) / 2 ; advancedSort(dst, src, lo, mid); advancedSort(dst, src, mid + 1 , hi); if (!less(src[mid + 1 ], src[mid])) { System.arraycopy(src, lo, dst, lo, hi - lo + 1 ); return ; } merge(src, dst, lo, mid, hi); } private static <K extends Comparable <K>> void merge (K[] src, K[] dst, int lo, int mid, int hi) { int i = lo, j = mid + 1 ; for (int k = lo; k <= hi; k++) { if (i > mid) dst[k] = src[j++]; else if (j > hi) dst[k] = src[i++]; else if (less(src[j], src[i])) dst[k] = src[j++]; else dst[k] = src[i++]; } } private static void insertionSort (Comparable[] a, int lo, int hi) { for (int i = lo; i <= hi; i++) for (int j = i; j > lo && less(a[j], a[j - 1 ]); j--) exch(a, j, j - 1 ); }
5.性能测试 对归并排序的各种方法进行性能测试
分别对一千万个随机数组和随机重复数组排序
MergeSortTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package algorithm.sort;import algorithm.util.iostream.StdOut;import algorithm.util.random.RandomArrayUtil;import algorithm.util.watch.Stopwatch;import org.junit.Test;import java.util.Arrays;import static algorithm.sort.BaseSort.isSorted;import static algorithm.sort.BaseSort.show;import static algorithm.sort.MergeSort.advancedSort;import static algorithm.sort.MergeSort.sortTopDown;import static algorithm.sort.MergeSort.sortBottomUp;public class MergeSortTest { @Test public void compareSort () { Integer [] a11 = RandomArrayUtil.getRandomBoxedIntArray(0 , 100000000 , 10000000 ); Integer [] a12 = Arrays.copyOf(a11, a11.length); Integer [] a13 = Arrays.copyOf(a11, a11.length); Integer [] a21 = RandomArrayUtil.getRandomBoxedIntArray(0 , 1000 , 10000000 ); Integer [] a22 = Arrays.copyOf(a21, a21.length); Integer [] a23 = Arrays.copyOf(a21, a21.length); System.out.println("Array created!" ); Stopwatch stopwatch = new Stopwatch (); sortTopDown(a11); StdOut.printf("%s (%.2f seconds)\n" , "sortTopDown [random]:" , stopwatch.elapsedTime()); sortTopDown(a21); StdOut.printf("%s (%.2f seconds)\n" , "sortTopDown [random + duplicate]:" , stopwatch.elapsedTime()); assert isSorted(a11); assert isSorted(a21); System.out.println(); stopwatch = new Stopwatch (); sortBottomUp(a12); StdOut.printf("%s (%.2f seconds)\n" , "sortBottomUp [random]:" , stopwatch.elapsedTime()); sortBottomUp(a22); StdOut.printf("%s (%.2f seconds)\n" , "sortBottomUp [random + duplicate]:" , stopwatch.elapsedTime()); assert isSorted(a12); assert isSorted(a22); System.out.println(); stopwatch = new Stopwatch (); advancedSort(a13); StdOut.printf("%s (%.2f seconds)\n" , "advancedSort [random]:" , stopwatch.elapsedTime()); advancedSort(a23); StdOut.printf("%s (%.2f seconds)\n" , "advancedSort [random + duplicate]:" , stopwatch.elapsedTime()); assert isSorted(a13); assert isSorted(a23); System.out.println(); } }
排序结果如下:
1 2 3 4 5 6 7 8 sortTopDown [random]: (4.22 seconds) sortTopDown [random + duplicate]: (7.40 seconds) sortBottomUp [random]: (5.06 seconds) sortBottomUp [random + duplicate]: (9.26 seconds) advancedSort [random]: (3.80 seconds) advancedSort [random + duplicate]: (6.56 seconds)
6.性能测试总结 可知:
含有重复元素的排序反而稍快!
自顶向下的排序稍快于自底向上的排序(但是自底向上归并的优势在于: 针对链表排序的空间复杂度为O(n))
优化后的自顶向下的归并排序显然性能更加优越(一千万个数的排序用了3.8秒!)
归并排序性能冠军: advancedSort
性能优化原因:
对小规模子数组采用插入排序: 避免了对小规模的数组进行递归而产生过多的小数组甚至是空数组调用栈
测试数组是否已经有序: 不需要对有序的两个子数组进行归并操作
不将元素复制到辅助数组aux: 优化了空间复杂度
二.快速排序优化及性能测试 快速排序的基本思想:
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序
1.算法描述 快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)
具体算法描述如下:
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
2.动图演示
3.代码实现 QuickSort.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 package algorithm.sort;import algorithm.util.random.StdRandom;public class QuickSort extends BaseSort { private QuickSort () { } public static <K extends Comparable <K>> void sort (K[] a) { StdRandom.shuffle(a); sort(a, 0 , a.length - 1 ); } private static <K extends Comparable <K>> void sort (K[] a, int lo, int hi) { if (hi <= lo) return ; int j = partition(a, lo, hi); sort(a, lo, j - 1 ); sort(a, j + 1 , hi); } private static <K extends Comparable <K>> int partition (K[] a, int lo, int hi) { int i = lo, j = hi + 1 ; K key = a[lo]; while (true ) { while (less(a[++i], key)) if (i == hi) break ; while (less(key, a[--j])) if (j == lo) break ; if (i >= j) break ; exch(a, i, j); } exch(a, lo, j); return j; } }
除此之外, 快排还有另一种算法, 即通过挖坑法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public static <K extends Comparable <K>> void sort2 (K[] a) { StdRandom.shuffle(a); sort2(a, 0 , a.length - 1 ); } private static <K extends Comparable <K>> void sort2 (K[] a, int lo, int hi) { if (a.length <= 1 || lo >= hi) return ; int left = lo, right = hi; var key = a[left]; while (left < right) { while (left < right && !less(a[right], key)) right--; a[left] = a[right]; while (left < right && !less(key, a[left])) left++; a[right] = a[left]; } a[left] = key; sort2(a, lo, left - 1 ); sort2(a, left + 1 , hi); }
说明:
关于这个方法, 在我的另一篇文章有更详细的说明: QuickSort总结
需要注意的是, 在后来的性能测试中显示这个算法的性能并不是很好(虽然很好写)
4.优化方法 ① 针对排序基准轴优化
针对基本快排进行优化的思路主要有以下几个:
在排序之前进行打乱操作(上述代码已经使用)
小数组切换到插入排序: 通常THRESHOLD选择5~15均可
三取样切分, 选择子数组一小部分的中位数来切分(保证尽量均匀)
下面为使用上述代码优化的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 private static final int INSERTION_SORT_THRESHOLD = 8 ;public static <K extends Comparable <K>> void advancedSort (K[] a) { advancedSort(a, 0 , a.length - 1 ); } private static <K extends Comparable <K>> void advancedSort (K[] a, int lo, int hi) { if (hi <= lo) return ; int n = hi - lo + 1 ; if (n <= INSERTION_SORT_THRESHOLD) { insertionSort(a, lo, hi); return ; } int j = advancedPartition(a, lo, hi); advancedSort(a, lo, j - 1 ); advancedSort(a, j + 1 , hi); } private static <K extends Comparable <K>> int advancedPartition (K[] a, int lo, int hi) { int n = hi - lo + 1 , m = median3(a, lo, lo + n / 2 , hi); exch(a, m, lo); int i = lo, j = hi + 1 ; var key = a[lo]; while (less(a[++i], key)) { if (i == hi) { exch(a, lo, hi); return hi; } } while (less(key, a[--j])) { if (j == lo + 1 ) return lo; } while (i < j) { exch(a, i, j); while (less(a[++i], key)) ; while (less(key, a[--j])) ; } exch(a, lo, j); return j; } private static <K extends Comparable <K>> int median3 (K[] a, int i, int j, int k) { return (less(a[i], a[j]) ? (less(a[j], a[k]) ? j : less(a[i], a[k]) ? k : i) : (less(a[k], a[j]) ? j : less(a[k], a[i]) ? k : i)); } private static <K extends Comparable <K>> void insertionSort (K[] a, int lo, int hi) { for (int i = lo; i <= hi; i++) for (int j = i; j > lo && less(a[j], a[j - 1 ]); j--) exch(a, j, j - 1 ); }
另一个版本的实现(挖坑法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 public static <K extends Comparable <K>> void advancedSort2 (K[] a) { advancedSort2(a, 0 , a.length - 1 ); } private static <K extends Comparable <K>> void advancedSort2 (K[] a, int lo, int hi) { if (hi <= lo) return ; int n = hi - lo + 1 ; if (n <= INSERTION_SORT_THRESHOLD) { insertionSort(a, lo, hi); return ; } int m = median3(a, lo, lo + (hi - lo + 1 ) / 2 , hi); exch(a, m, lo); int left = lo, right = hi; var key = a[left]; while (left < right) { while (left < right && !less(a[right], key)) right--; a[left] = a[right]; while (left < right && !less(key, a[left])) left++; a[right] = a[left]; } a[left] = key; advancedSort2(a, lo, left - 1 ); advancedSort2(a, left + 1 , hi); } private static <K extends Comparable <K>> void insertionSort (K[] a, int lo, int hi) { for (int i = lo; i <= hi; i++) for (int j = i; j > lo && less(a[j], a[j - 1 ]); j--) exch(a, j, j - 1 ); } private static <K extends Comparable <K>> int median3 (K[] a, int i, int j, int k) { return (less(a[i], a[j]) ? (less(a[j], a[k]) ? j : less(a[i], a[k]) ? k : i) : (less(a[k], a[j]) ? j : less(a[k], a[i]) ? k : i)); }
需要再次强调的是: 挖坑法在后面的测试中性能并不令人满意
② 三向切分的快排
三向切分的快排(信息量最优的快速排序): 适用于大量重复元素的排序
从左至右遍历数组一次:
处理时一开始i和lo相等, 进行三向比较:
a[i] < key: 将a[lt]和a[i]交换, 将lt和i加一;
a[i] > key: 将a[gt]和a[i]交换, 将gt减一;
a[i] = key: 将i加一;
上述操作均会保证数组元素不变并且缩小gt-i的值(这样循环才会结束)
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 public static <K extends Comparable <K>> void threeWaySort (K[] a) { threeWaySort(a, 0 , a.length - 1 ); } private static <K extends Comparable <K>> void threeWaySort (K[] a, int lo, int hi) { if (hi <= lo) return ; int n = hi - lo + 1 ; if (n <= INSERTION_SORT_THRESHOLD) { insertionSort(a, lo, hi); return ; } int m = median3(a, lo, lo + (hi - lo + 1 ) / 2 , hi); exch(a, m, lo); int lt = lo, gt = hi; var key = a[lt]; int i = lo + 1 ; while (i <= gt) { int cmp = a[i].compareTo(key); if (cmp < 0 ) exch(a, lt++, i++); else if (cmp > 0 ) exch(a, i, gt--); else ++i; } threeWaySort(a, lo, lt -1 ); threeWaySort(a, gt + 1 , hi); } private static <K extends Comparable <K>> int median3 (K[] a, int i, int j, int k) { return (less(a[i], a[j]) ? (less(a[j], a[k]) ? j : less(a[i], a[k]) ? k : i) : (less(a[k], a[j]) ? j : less(a[k], a[i]) ? k : i)); } private static <K extends Comparable <K>> void insertionSort (K[] a, int lo, int hi) { for (int i = lo; i <= hi; i++) for (int j = i; j > lo && less(a[j], a[j - 1 ]); j--) exch(a, j, j - 1 ); }
5.性能测试 对快排的各种方法进行性能测试(注意测试同时对比了快排的另一种实现)
分别对五百万个随机数组和随机重复数组排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 package algorithm.sort;import algorithm.util.iostream.StdOut;import algorithm.util.random.RandomArrayUtil;import algorithm.util.watch.Stopwatch;import org.junit.Test;import java.util.Arrays;import static algorithm.sort.BaseSort.isSorted;import static algorithm.sort.BaseSort.show;import static algorithm.sort.QuickSort.advancedSort;import static algorithm.sort.QuickSort.advancedSort2;import static algorithm.sort.QuickSort.sort;import static algorithm.sort.QuickSort.sort2;import static algorithm.sort.QuickSort.threeWaySort;public class QuickSortTest { @Test public void compareSort () { Integer [] a11 = RandomArrayUtil.getRandomBoxedIntArray(0 , 1000000000 , 5000000 ); Integer [] a12 = Arrays.copyOf(a11, a11.length); Integer [] a13 = Arrays.copyOf(a11, a11.length); Integer [] a14 = Arrays.copyOf(a11, a11.length); Integer [] a15 = Arrays.copyOf(a11, a11.length); Integer [] a21 = RandomArrayUtil.getRandomBoxedIntArray(0 , 1000 , 5000000 ); Integer [] a22 = Arrays.copyOf(a21, a21.length); Integer [] a23 = Arrays.copyOf(a21, a21.length); Integer [] a24 = Arrays.copyOf(a21, a21.length); Integer [] a25 = Arrays.copyOf(a21, a21.length); System.out.println("Array created!" ); Stopwatch stopwatch = new Stopwatch (); sort(a11); StdOut.printf("%s (%.2f seconds)\n" , "sort [random]:" , stopwatch.elapsedTime()); sort(a21); StdOut.printf("%s (%.2f seconds)\n" , "sort [random + duplicate]:" , stopwatch.elapsedTime()); assert isSorted(a11); assert isSorted(a21); System.out.println(); stopwatch = new Stopwatch (); sort2(a12); StdOut.printf("%s (%.2f seconds)\n" , "sort2 [random]:" , stopwatch.elapsedTime()); sort2(a22); StdOut.printf("%s (%.2f seconds)\n" , "sort2 [random + duplicate]:" , stopwatch.elapsedTime()); assert isSorted(a12); assert isSorted(a22); System.out.println(); stopwatch = new Stopwatch (); advancedSort(a13); StdOut.printf("%s (%.2f seconds)\n" , "advancedSort [random]:" , stopwatch.elapsedTime()); advancedSort(a23); StdOut.printf("%s (%.2f seconds)\n" , "advancedSort [random + duplicate]:" , stopwatch.elapsedTime()); assert isSorted(a13); assert isSorted(a23); System.out.println(); stopwatch = new Stopwatch (); advancedSort2(a14); StdOut.printf("%s (%.2f seconds)\n" , "advancedSort2 [random]:" , stopwatch.elapsedTime()); advancedSort2(a24); StdOut.printf("%s (%.2f seconds)\n" , "advancedSort2 [random + duplicate]:" , stopwatch.elapsedTime()); assert isSorted(a14); assert isSorted(a24); System.out.println(); stopwatch = new Stopwatch (); threeWaySort(a15); StdOut.printf("%s (%.2f seconds)\n" , "threeWaySort [random]:" , stopwatch.elapsedTime()); threeWaySort(a25); StdOut.printf("%s (%.2f seconds)\n" , "threeWaySort [random + duplicate]:" , stopwatch.elapsedTime()); assert isSorted(a15); assert isSorted(a25); System.out.println(); } }
测试的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 sort [random]: (2.23 seconds) sort [random + duplicate]: (4.22 seconds) sort2 [random]: (2.58 seconds) sort2 [random + duplicate]: (32.91 seconds) advancedSort [random]: (1.16 seconds) advancedSort [random + duplicate]: (2.14 seconds) advancedSort2 [random]: (1.55 seconds) advancedSort2 [random + duplicate]: (26.64 seconds) threeWaySort [random]: (2.38 seconds) threeWaySort [random + duplicate]: (3.73 seconds)
6.性能测试总结 由测试可见:
advancedSort性能最优(因为优化的最好)
threeWaySort处理重复数组的能力与advancedSort几乎相同, 且优于普通的快排
对于sort2系列, 优于采用的是挖坑法, 且没有对重复子数组进行特殊处理, 所以很容易陷入N^2复杂度!
快速排序的性能优胜者是: advancedSort
性能优化原因:
对小规模子数组采用插入排序: 避免了对小规模的数组进行递归而产生过多的小数组甚至是空数组调用栈
三取样切分, 选择子数组一小部分的中位数来切分(保证尽量切分均匀)
三.堆排序序优化及性能测试 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法
堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
1.算法描述
将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)
不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成
2.动图演示
3.代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package algorithm.sort;public class HeapSort extends BaseSort { public static <K extends Comparable <K>> void sort (K[] a) { int len = a.length; buildMaxHeap(a); for (int i = len - 1 ; i > 0 ; i--) { exch(a, 0 , i); len--; heapify(a, 0 , len); } } private static <K extends Comparable <K>> void buildMaxHeap (K[] a) { for (int len = a.length, i = len >> 1 ; i >= 0 ; i--) { heapify(a, i, a.length); } } private static <K extends Comparable <K>> void heapify (K[] a, int i, int bound) { int largest = i; int left = 2 * i + 1 , right = 2 * i + 2 ; if (left < bound && less(a[largest], a[left])) { largest = left; } if (right < bound && less(a[largest], a[right])) { largest = right; } if (largest != i) { exch(a, i, largest); heapify(a, largest, bound); } } }
4.排序实例 1 2 3 4 5 6 public void sortTest () { Integer [] a = RandomArrayUtil.getRandomBoxedIntArray(0 , 1000 , 50 ); sort(a); show(a); assert isSorted(a); }
四.非比较排序介绍 本章介绍一些非比较算法, 这些非比较算法有时可以达到线性的时间复杂度(而已证明基于比较的排序不可能超过nlogn!)
但是需要注意的是, 这些非比较排序算法或多或少都有一定的局限性
1.计数排序 计数排序不是基于比较的排序算法,其核心在于: 将输入的数据值转化为键存储在额外开辟的数组空间中
作为一种线性时间复杂度 的排序,计数排序要求输入的数据必须是有确定范围的整数
① 算法描述
找出待排序的数组中最大和最小的元素;
统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
② 动图演示
③ 代码实现
代码说明:
为简单起见, 示例代码仅仅对int数组排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package algorithm.sort;public class CountSort extends BaseSort { public static void sort (int [] a) { int max = a[0 ], min = a[0 ]; for (int i : a) { if (i > max) { max = i; } if (i < min) { min = i; } } int [] aux = new int [max - min + 1 ]; for (int i : a) { aux[i - min] += 1 ; } int index = 0 ; for (int i = 0 ; i < aux.length; i++) { for (int j = 0 ; j < aux[i]; j++) { a[index++] = i + min; } } } }
计数排序是一个稳定的排序算法
当输入的元素是 n 个0~k之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法
当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法
④ 排序实例 1 2 3 4 5 6 7 8 9 10 11 12 13 public void sortTest () { int [] a = RandomArrayUtil.getRandomIntArray(0 , 1000 , 20 ); sort(a); System.out.println(Arrays.toString(a)); assert isSorted(a); } public void sortTest2 () { int [] a = RandomArrayUtil.getRandomIntArray(0 , 10 , 100 ); sort(a); System.out.println(Arrays.toString(a)); assert isSorted(a); }
2.桶排序 桶排序是计数排序的升级版, 它利用了函数的映射关系 ,高效与否的关键就在于: 这个映射函数的确定
桶排序 (Bucket sort)的工作原理:
假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序 (有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)
① 算法描述
设置一个定量的数组当作空桶;
遍历输入数据,并且把数据一个一个放到对应的桶里去;
对每个不是空的桶进行排序;
从不是空的桶里把排好序的数据拼接起来
② 图片演示
③ 代码实现
算法说明:
作为演示, 排序要求: 输入的桶大小bucketSize, 应当大于待排序浮点数的整数部分, 且浮点数均大于零才行
(因为在getBucketIndex方法中仅仅取了浮点数的整数部分作为桶的index)
实际使用时, 应当使用不同的getBucketIndex()方法进行桶切分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 package algorithm.sort;import java.util.ArrayList;import java.util.LinkedList;import java.util.List;import java.util.ListIterator;public class BucketSort extends BaseSort { private static final int DEFAULT_BUCKET_SIZE = 5 ; public static void sort (double [] arr, int bucketSize) { ArrayList <LinkedList<Double>> buckets = new ArrayList <>(); bucketSize = Math.max(bucketSize, DEFAULT_BUCKET_SIZE); for (int i = 0 ; i < bucketSize; i++) { buckets.add(new LinkedList <>()); } for (double data : arr) { int index = getBucketIndex(data); insertSort(buckets.get(index), data); } int index = 0 ; for (LinkedList<Double> bucket : buckets) { for (Double data : bucket) { arr[index++] = data; } } } private static int getBucketIndex (double data) { return (int ) data; } private static void insertSort (List<Double> bucket, double data) { ListIterator <Double> it = bucket.listIterator(); boolean insertFlag = true ; while (it.hasNext()) { if (data <= it.next()) { it.previous(); it.add(data); insertFlag = false ; break ; } } if (insertFlag) { bucket.add(data); } } }
桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)
桶排序最好情况下使用线性时间O(n);
很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大
④ 排序实例 1 2 3 4 5 6 7 public void bucketSortTest () { int maxBound = 1000 ; double [] a = RandomArrayUtil.getRandomDoubleArray(0 , maxBound, 20 ); sort(a, maxBound + 1 ); System.out.println(Arrays.toString(a)); assert isSorted(a); }
3.基数排序 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序, 最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前
① 算法描述
取得数组中的最大数,并取得位数;
arr为原始数组,从最低位开始取每个位组成radix数组;
对radix进行计数排序(利用计数排序适用于小范围数的特点);
② 动图演示
③ 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 package algorithm.sort;public class RadixSort extends BaseSort { private static final int [] RADIX_DICT = {1 , 10 , 100 , 1000 , 10000 , 1000000 , 1000000 , 10000000 , 100000000 , 100000000 }; public static void sort (int [] arr, int max) { int [] count = new int [arr.length]; int [] bucket = new int [arr.length]; for (int k = 1 ; k <= max; k++) { for (int i = 0 ; i < arr.length; i++) { count[i] = 0 ; } for (int value : arr) { count[getFigure(value, k)]++; } for (int i = 1 ; i < arr.length; i++) { count[i] = count[i] + count[i - 1 ]; } for (int i = arr.length - 1 ; i >= 0 ; i--) { int j = getFigure(arr[i], k); bucket[count[j] - 1 ] = arr[i]; count[j]--; } for (int i = 0 , j = 0 ; i < arr.length; i++, j++) { arr[i] = bucket[j]; } } } private static int getFigure (int i, int k) { return (i / RADIX_DICT[k - 1 ]) % 10 ; } }
④ 排序实例 1 2 3 4 5 6 public void sortTest () { int [] a = RandomArrayUtil.getRandomIntArray(0 , 9999 , 20 ); sort(a, 4 ); System.out.println(Arrays.toString(a));; assert isSorted(a); }
附录 上篇见: 几种常见排序方法的优化(上)
文中部分说明内容转自: 十大经典排序算法(动图演示)
源代码:
如果觉得文章写的不错, 可以关注微信公众号: Coder张小凯
内容和博客同步更新~