生成一个随机的数组 下面函数有两个功能: rangeL和rangeR的值的差距越大,则生成随机性非常大的数据,很少有重复数据 rangeL和rangeR的值的差很小,则生成很多重复的数据
// 生成有n个元素的随机数组,每个元素的随机范围为[rangeL, rangeR] public static Integer[] generateRandomArray(int n, int rangeL, int rangeR) { assert rangeL <= rangeR; Integer[] arr = new Integer[n]; for (int i = 0; i < n; i++) arr[i] = new Integer((int)(Math.random() * (rangeR - rangeL + 1) + rangeL)); return arr; }生成一个近乎有序的数组:
// 生成一个近乎有序的数组 // 首先生成一个含有[0...n-1]的完全有序数组, 之后随机交换swapTimes对数据 // swapTimes定义了数组的无序程度: // swapTimes == 0 时, 数组完全有序 // swapTimes 越大, 数组越趋向于无序 public static Integer[] generateNearlyOrderedArray(int n, int swapTimes){ Integer[] arr = new Integer[n]; for( int i = 0 ; i < n ; i ++ ) arr[i] = new Integer(i); for( int i = 0 ; i < swapTimes ; i ++ ){ int a = (int)(Math.random() * n); int b = (int)(Math.random() * n); int t = arr[a]; arr[a] = arr[b]; arr[b] = t; } return arr; }用于检测排序后是否真的有序了,可用一个assert语句进行判断:
public static boolean isSorted(Comparable[] arr){ for( int i = 0 ; i < arr.length - 1 ; i ++ ) if( arr[i].compareTo(arr[i+1]) > 0 ) return false; return true; }用于测试排序时间的函数:
public static void testSort(String sortClassName, Comparable[] arr){ // 通过Java的反射机制,通过排序的类名,运行排序函数 try{ // 通过sortClassName获得排序函数的Class对象 Class sortClass = Class.forName(sortClassName); // 通过排序函数的Class对象获得排序方法 Method sortMethod = sortClass.getMethod("sort",new Class[]{Comparable[].class}); // 排序参数只有一个,是可比较数组arr Object[] params = new Object[]{arr}; long startTime = System.currentTimeMillis(); // 调用排序函数 sortMethod.invoke(null,params); long endTime = System.currentTimeMillis(); assert isSorted( arr ); System.out.println( sortClass.getSimpleName()+ " : " + (endTime-startTime) + "ms" ); } catch(Exception e){ e.printStackTrace(); } }需要开辟一个新数组,复制原数组的数据,新数组左右两部分分别下标 i 和 j ,不断地选小的数复制到原数组上,左右两部分则可以归并成一个有序的数组。
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并 private static void merge(Comparable[] arr, int l, int mid, int r) { Comparable[] aux = Arrays.copyOfRange(arr, l, r+1); // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1 int i = l, j = mid+1; for( int k = l ; k <= r; k ++ ){ if( i > mid ){ // 如果左半部分元素已经全部处理完毕 arr[k] = aux[j-l]; j ++; } else if( j > r ){ // 如果右半部分元素已经全部处理完毕 arr[k] = aux[i-l]; i ++; } else if( aux[i-l].compareTo(aux[j-l]) < 0 ){ // 左半部分所指元素 < 右半部分所指元素 arr[k] = aux[i-l]; i ++; } else{ // 左半部分所指元素 >= 右半部分所指元素 arr[k] = aux[j-l]; j ++; } } } // 递归使用归并排序,对arr[l...r]的范围进行排序 private static void sort(Comparable[] arr, int l, int r) { if (l >= r) return; int mid = (l+r)/2; sort(arr, l, mid); sort(arr, mid + 1, r); merge(arr, l, mid, r); } public static void sort(Comparable[] arr){ int n = arr.length; sort(arr, 0, n-1); }1.尽管合并排序最坏情况运行时间为O(nlgn),插入排序的最坏运行时间为O(n^2),但是插入排序的常数因子使得它在n较小时,运行要更快一些。因此,在合并排序算法中,当子问题足够小时,采用插入排序就比较合适了。 2.需要归并的两个有序数组,如果左边的最大值比右边的最小值还小,则不进行merge。
private static void sort(Comparable[] arr, int l, int r) { // 优化2: 对于小规模数组, 使用插入排序 if( r - l <= 15 ){ InsertionSort.sort(arr, l, r); return; } int mid = (l+r)/2; sort(arr, l, mid); sort(arr, mid + 1, r); // 优化1: 对于arr[mid] <= arr[mid+1]的情况,不进行merge // 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失 if( arr[mid].compareTo(arr[mid+1]) > 0 ) merge(arr, l, mid, r); }归并可以用来求数组的逆序对数
比较6和5的时候,5比6小,则会有两个逆序对,6和5,8和5,累加器加2。然后5的光标移到7上。6比7小,6的光标移到8上,比较8和7,则会产生一个逆序对,累加器加1。
不足:当数据接近有序,会退化到O(n2) 解决办法:随机化数组的p值。
然而,又遇见了问题。 不足:当数据重复的值很多时,会退化到O(n2)
增加元素和取出元素:
// 像最大堆中插入一个新的元素 item public void insert(Item item){ assert count + 1 <= capacity; data[count+1] = item; count ++; shiftUp(count); } // 从最大堆中取出堆顶元素, 即堆中所存储的最大数据 public Item extractMax(){ assert count > 0; Item ret = data[1]; swap( 1 , count ); count --; shiftDown(1); return ret; }最重要的shiftUp和shiftDown:
//******************** //* 最大堆核心辅助函数 //******************** private void shiftUp(int k){ while( k > 1 && data[k/2].compareTo(data[k]) < 0 ){ swap(k, k/2); k /= 2; } } private void shiftDown(int k){ while( 2*k <= count ){ int j = 2*k; // 在此轮循环中,data[k]和data[j]交换位置 if( j+1 <= count && data[j+1].compareTo(data[j]) > 0 ) j ++; // data[j] 是 data[2*k]和data[2*k+1]中的最大值 if( data[k].compareTo(data[j]) >= 0 ) break; swap(k, j); k = j; } }HeapSort1:
// 对整个arr数组使用HeapSort1排序 // HeapSort1, 将所有的元素依次添加到堆中, 在将所有元素从堆中依次取出来, 即完成了排序 // 无论是创建堆的过程, 还是从堆中依次取出元素的过程, 时间复杂度均为O(nlogn) // 整个堆排序的整体时间复杂度为O(nlogn) public static void sort(Comparable[] arr){ int n = arr.length; MaxHeap<Comparable> maxHeap = new MaxHeap<Comparable>(n); for( int i = 0 ; i < n ; i ++ ) maxHeap.insert(arr[i]); for( int i = n-1 ; i >= 0 ; i -- ) arr[i] = maxHeap.extractMax(); }HeapSort2:
// 构造函数, 通过一个给定数组创建一个最大堆 // 该构造堆的过程, 时间复杂度为O(n) public MaxHeap(Item arr[]){ int n = arr.length; data = (Item[])new Comparable[n+1]; capacity = n; for( int i = 0 ; i < n ; i ++ ) data[i+1] = arr[i]; count = n; for( int i = count/2 ; i >= 1 ; i -- ) shiftDown(i); } // 对整个arr数组使用HeapSort2排序 // HeapSort2, 借助我们的heapify过程创建堆 // 此时, 创建堆的过程时间复杂度为O(n), 将所有元素依次从堆中取出来, 实践复杂度为O(nlogn) // 堆排序的总体时间复杂度依然是O(nlogn), 但是比HeapSort1性能更优, 因为创建堆的性能更优 public static void sort(Comparable[] arr){ int n = arr.length; MaxHeap<Comparable> maxHeap = new MaxHeap<Comparable>(arr); for( int i = n-1 ; i >= 0 ; i -- ) arr[i] = maxHeap.extractMax(); }比较百万级别的数据,O(n2)的算法不合适。
所以只比较O(nlogn)的算法 比较归并,二路快排,三路快排的效率:
public static void main(String[] args) { int N = 1000000; // 测试1 一般性测试 System.out.println("Test for random array, size = " + N + " , random range [0, " + N + "]"); Integer[] arr1 = SortTestHelper.generateRandomArray(N, 0, N); Integer[] arr2 = Arrays.copyOf(arr1, arr1.length); Integer[] arr3 = Arrays.copyOf(arr1, arr1.length); SortTestHelper.testSort("quick_three.MergeSort", arr1); SortTestHelper.testSort("quick_three.QuickSort2Ways", arr2); SortTestHelper.testSort("quick_three.QuickSort3Ways", arr3); System.out.println(); // 测试2 测试近乎有序的数组 int swapTimes = 100; assert swapTimes >= 0; System.out.println("Test for nearly ordered array, size = " + N + " , swap time = " + swapTimes); arr1 = SortTestHelper.generateNearlyOrderedArray(N, swapTimes); arr2 = Arrays.copyOf(arr1, arr1.length); arr3 = Arrays.copyOf(arr1, arr1.length); SortTestHelper.testSort("quick_three.MergeSort", arr1); SortTestHelper.testSort("quick_three.QuickSort2Ways", arr2); SortTestHelper.testSort("quick_three.QuickSort3Ways", arr3); System.out.println(); // 测试3 测试存在包含大量相同元素的数组 System.out.println("Test for random array, size = " + N + " , random range [0,10]"); arr1 = SortTestHelper.generateRandomArray(N, 0, 10); arr2 = Arrays.copyOf(arr1, arr1.length); arr3 = Arrays.copyOf(arr1, arr1.length); SortTestHelper.testSort("quick_three.MergeSort", arr1); SortTestHelper.testSort("quick_three.QuickSort2Ways", arr2); SortTestHelper.testSort("quick_three.QuickSort3Ways", arr3); return; }比较结果: 对于包含有大量重复数据的数组, 三路快排有巨大的优势,对于一般性的随机数组和近乎有序的数组, 三路快排的效率虽然不是最优的, 但是是在非常可以接受的范围里,因此, 在一些语言中, 三路快排是默认的语言库函数中使用的排序算法。比如Java:)
加上推排序进行比较: