一、什么是歸並排序
歸並排序又稱合並排序,它是成功應用分治技術的一個完美例子。對於一個需要排序的數組A[0..n-1],歸並排序把它一分為二:A[0..n/2-1]和A[n/2..n-1],並對每個子數組遞歸排序,然后把這兩個排好序的子數組合並為一個有序數組。下面是歸並排序的例子圖解:
二、單線程實現歸並排序
package com.bob.algorithms.sort; import java.util.Arrays; import com.bob.algorithms.SortStrategy; /** * 歸並排序 * * @author bob * */ public class SingleThreadMergeSort implements SortStrategy { public int[] sort(int[] rawArray) { mergeSort(rawArray); return rawArray; } /** * 分解並合並排序,升序 * * @param intArr */ private void mergeSort(int[] intArr) { if (intArr.length > 1) { // 如果數組長度大於1就分解稱兩份 int[] leftArray = Arrays.copyOfRange(intArr, 0, intArr.length / 2); int[] rightArray = Arrays.copyOfRange(intArr, intArr.length / 2, intArr.length); mergeSort(leftArray); mergeSort(rightArray); // 合並且排序 merge(leftArray, rightArray, intArr); } } /** * 合並排序 * * @param leftArray * @param rightArray * @param intArr */ private void merge(int[] leftArray, int[] rightArray, int[] intArr) { // i:leftArray數組索引,j:rightArray數組索引,k:intArr數組索引 int i = 0, j = 0, k = 0; while (i < leftArray.length && j < rightArray.length) { // 當兩個數組中都有值的時候,比較當前元素進行選擇 if (leftArray[i] < rightArray[j]) { intArr[k] = leftArray[i]; i++; } else { intArr[k] = rightArray[j]; j++; } k++; } // 將還剩余元素沒有遍歷完的數組直接追加到intArr后面 if (i == leftArray.length) { for (; j < rightArray.length; j++, k++) { intArr[k] = rightArray[j]; } } else { for (; i < leftArray.length; i++, k++) { intArr[k] = leftArray[i]; } } } }
三、使用Fork/Join框架實現歸並排序
Fork/Join是從JDK 1.7 加入的並發計算框架。
package com.bob.algorithms.sort; import java.util.Arrays; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveAction; import com.bob.algorithms.SortStrategy; public class ForkJoinMergeSort implements SortStrategy { public int[] sort(int[] rawArray) { ForkJoinPool pool = new ForkJoinPool(); pool.invoke(new MergeSort(rawArray)); return rawArray; } /** * 使用Fork/join的方式進行歸並排序,充分利用cpu * * @author zhangwensha * */ private static class MergeSort extends RecursiveAction { private static final long serialVersionUID = 425572392953885545L; private int[] intArr; public MergeSort(int[] intArr) { this.intArr = intArr; } @Override protected void compute() { if (intArr.length > 1) { // 如果數組長度大於1就分解稱兩份 int[] leftArray = Arrays.copyOfRange(intArr, 0, intArr.length / 2); int[] rightArray = Arrays.copyOfRange(intArr, intArr.length / 2, intArr.length); // 這里分成兩份執行 invokeAll(new MergeSort(leftArray), new MergeSort(rightArray)); // 合並且排序 merge(leftArray, rightArray, intArr); } } /** * 合並排序 * * @param leftArray * @param rightArray * @param intArr */ private void merge(int[] leftArray, int[] rightArray, int[] intArr) { // i:leftArray數組索引,j:rightArray數組索引,k:intArr數組索引 int i = 0, j = 0, k = 0; while (i < leftArray.length && j < rightArray.length) { // 當兩個數組中都有值的時候,比較當前元素進行選擇 if (leftArray[i] < rightArray[j]) { intArr[k] = leftArray[i]; i++; } else { intArr[k] = rightArray[j]; j++; } k++; } // 將還剩余元素沒有遍歷完的數組直接追加到intArr后面 if (i == leftArray.length) { for (; j < rightArray.length; j++, k++) { intArr[k] = rightArray[j]; } } else { for (; i < leftArray.length; i++, k++) { intArr[k] = leftArray[i]; } } } } }
四、單線程 pk 多線程
編寫了舞台類,通過調整generateIntArray(10000000)的輸入參數來設置待排序數組長度,試驗中沒有對堆容量進行設置。
package com.bob.algorithms; import java.util.Arrays; import java.util.Date; import com.bob.algorithms.common.CommonUtil; import com.bob.algorithms.sort.ForkJoinMergeSort; import com.bob.algorithms.sort.SingleThreadMergeSort; /** * 舞台類,專門用來測試算法的時間 * * @author bob * */ public class Stage { public static void main(String[] args) { // 變量定義 long begintime = 0; long endtime = 0; // 生成排序數據 int[] rawArr = generateIntArray(10000000); int[] rawArr2 = Arrays.copyOf(rawArr, rawArr.length); begintime = new Date().getTime(); new SingleThreadMergeSort().sort(rawArr); //System.out.println(Arrays.toString(new SingleThreadMergeSort().sort(rawArr))); endtime = new Date().getTime(); System.out.println("單線程歸並排序花費時間:" + (endtime - begintime)); System.out.println("是否升序:"+CommonUtil.isSorted(rawArr, true)); begintime = new Date().getTime(); new ForkJoinMergeSort().sort(rawArr2); //System.out.println(Arrays.toString(new ForkJoinMergeSort().sort(rawArr2))); endtime = new Date().getTime(); System.out.println("Fork/Join歸並排序花費時間:" + (endtime - begintime)); System.out.println("是否升序:"+CommonUtil.isSorted(rawArr2, true)); } /** * 生成int類型的數組 * * @return */ private static int[] generateIntArray(int length) { int[] intArr = new int[length]; for (int i = 0; i < length; i++) { intArr[i] = new Double(Math.random() * length).intValue(); } return intArr; } }
以下是數組容量在各個量級時,兩種方法效率對比:
數組長度 | 100 | 1000 | 10000 | 100000 | 1000000 | 10000000 |
---|---|---|---|---|---|---|
單線程 (ms) | 1 | 2 | 7 | 33 | 188 | 2139 |
Fork/Join (ms) | 8 | 9 | 17 | 63 | 358 | 1133 |
通過統計可以發現,當待排序序列長度較小時,使用單線程效率要高於多線程,但是隨着數量不斷增加,多線程執行時間越來越接近單線程的執行時間,最終在1000萬這個量級開始速率遠超單線程。工作中不能濫用多線程,在該使用的時候使用可以加快效率,充分利用多核。但是在不該用的時候使用徒增工作量,有可能效率還不如單線程。 感興趣的朋友可以通過下面代碼地址找到運行的全部源碼自己跑跑試試看。
五、本文代碼地址
包括本篇在內以后所有代碼統一存放地址為:
https://github.com/mingbozhang/algorithm
六、參考
https://docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html
《算法設計與分析基礎(第3版)》