java 排序算法之歸并排序
簡單介紹
歸并排序(merge sort)是利用 歸并 的思想實現(xiàn)的排序方法,該算法采用經(jīng)典的分治(divide-and-conquer)策略 :
- 分(divide):將問題分成一些小的問題,然后遞歸求解
- 治(conquer):將分的階段得到的各答案「修補」在一起
即:分而治之
該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合并,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合并成一個有序表,稱為二路歸并。
基本思想
- 分:分的過程只是為了分解
- 治:分組完成后,開始對每組進行排序,然后合并成一個有序序列,直到最后將所有分組合并成一個有序序列
可以看到這種結構很像一棵完全二叉樹,本文的歸并排序我們采用遞歸實現(xiàn)(也可采用迭代的方式實現(xiàn))。分階段可以理解為就是遞歸拆分子序列的過程。
注:這些數(shù)從始至終都在一個數(shù)組里(只是抽象的想想成兩個數(shù)組),除了排序時會把要排序的數(shù)copy到一個臨時數(shù)組里面,這里如果不懂后面看了代碼后,再返回來思考就懂了。一定要思考?。。。?/p>
下面的動圖可以很直觀的看到整個算法實現(xiàn)的過程,初體驗一下吧,后面的代碼可以結合上面的圖,和這個動圖,來整理一下邏輯
多個數(shù)排序動圖:
思路分析
對于分階段相對較簡單,下面著重來對治階段進行分析。
合并相鄰有序子序列分析:下圖以 歸并排序的最后一次合并 為例來分析,即對應上圖的 [4,5,7,8]
和 [1,2,3,6]
兩個已經(jīng)有序的子序列合并為最終序列 [1,2,3,4,5,6,7,8]
,下圖展示了實現(xiàn)步驟
如圖所示:這是最后一次的合并 操作,是兩個有序序列合并為最終的有序序列。
- 1.從有序序列開始挨個比較,這里比較 4 和 1,1 < 4,那么 1 進入臨時數(shù)組
temp
中,并且將自己的指針右移動一位 - 2.由于圖上綠色部分上一次 4 大于 1,指針并未移動,然后 4 和 2 對比,2 < 4 , 2 進入到臨時數(shù)組中,并且將自己的指針右移動一位
- 3. ...
- 4.如果某一組已經(jīng)全部進入了臨時數(shù)組,那么剩余一組的剩余有序序列直接追加到臨時數(shù)組中
- 5.最后,將 temp 內(nèi)容拷貝到原數(shù)組中去,完成排序
代碼實現(xiàn)
先實現(xiàn)合并方法,這個是該算法中最重要的,你也看可以直接看后面的完整算法代碼,再返回來思考,這個都隨你。此次是因為 分
的步驟需要用到 合
的這個,所以我這里就先放 合并的代碼了。
/** * * 最難的是合并,所以可以先完成合并的方法,此方法請參考 基本思想 和 思路分析中的圖解來完成 * 動腦筋 * * * * @param arr 要排序的原始數(shù)組 * @param left 因為是合并,所以要得到左右兩邊的的數(shù)組信息,這個是左側數(shù)組的第一個索引值 * @param mid 因為是一個數(shù)組,標識是第一個數(shù)組的最后一個索引,即 mid+1 是右側數(shù)組的第一個索引,即中間索引 * @param right 右側數(shù)組的結束索引,右邊數(shù)組的最后一個數(shù) * @param temp 臨時空間,臨時數(shù)組 */ public void merge(int[] arr, int left, int mid, int right, int[] temp) { // 定時臨時變量,用來遍歷數(shù)組比較 int l = left; // 左邊有序數(shù)組的初始索引 int r = mid + 1; // 右邊有序數(shù)組的初始索引 int t = 0; // temp 數(shù)組中當前最后一個有效數(shù)據(jù)的索引 // 1. 按思路先將兩個數(shù)組(有序的)有序的合并到 temp 中 // 因為是合并兩個數(shù)組,所以需要兩邊的數(shù)組都還有值的時候才能進行 比較合并 // 若其中一個數(shù)組的值遍歷完了(取完了),那么就跳出該循環(huán),把另一個數(shù)組所剩的值,直接copy進臨時數(shù)組即可 while (l <= mid && r <= right) { // 如果左邊的比右邊的小,則將左邊的該元素填充到 temp 中 // 并移動 t++,l++,好繼續(xù)下一個 if (arr[l] < arr[r]) { temp[t] = arr[l]; t++;//移動 temp 臨時數(shù)組中當前最后一個有效數(shù)據(jù)的索引 l++; //左邊原始數(shù)組的索引移動 } // 否則則將右邊的移動到 temp 中 else { temp[t] = arr[r]; t++;//移動 temp 臨時數(shù)組中當前最后一個有效數(shù)據(jù)的索引 r++;//右邊原始數(shù)組的索引移動 } } // 2. 如果有任意一邊的數(shù)組還有值,則依序?qū)⑹S鄶?shù)據(jù)填充到 temp 中 // 如果左側還有值 while (l <= mid) { temp[t] = arr[l]; t++; l++; } // 如果右側還有值 while (r <= right) { temp[t] = arr[r]; t++; r++; } // 3. 將 temp 數(shù)組,拷貝到原始數(shù)組 // 注意:只拷貝當前temp有效數(shù)據(jù)到對應的原始數(shù)據(jù)中,通俗點說,就是原始數(shù)組中要排序的數(shù)據(jù),通過temp排成了有序的,然后將temp中的有序數(shù)據(jù)將原始數(shù)組中原來要排序的那部分覆蓋了就行了 int tempL = left; // 從左邊開始拷貝,左邊第一個值的索引 t = 0; // temp 中的有效值索引,有效值邊界可以通過 right 判定,因為合并兩個數(shù)組到 temp 中,邊界就是 right ,這里自己好好想想 /* * 對于 8,4,5,7,1,3,6,2 這個數(shù)組, * 從棧頂開始合并:8,4 | 5,7 1,3 | 6,2 * 先左遞歸的話: * 第一次:8,4 合并:tempL=0,right=1 * 第二次:5,7 合并:tempL=2,right=3 * 第三次:4,8 | 5,7 進行合并,tempL=0,right=3 * 直到左遞歸完成,得到左側一個有序的序列:4,5,7,8 然后再開始遞歸右邊那個無序的 * 最后回到棧底分解成兩個數(shù)組的地方,將兩個數(shù)組合并成一個有序數(shù)組 * 這里真的得自己想了,我只能這么說了。 */ System.out.println("tempL=" + tempL + "; right=" + right); while (tempL <= right) { arr[tempL] = temp[t]; tempL++; t++; } }
需要注意的是:這個圖一定要看明白:
- 1.一直分解,到棧頂首次合并時,是兩個數(shù)字,也就是說,左側數(shù)組只有一個數(shù)字,右側數(shù)組只有一個數(shù)字
- 2.左側數(shù)組只有一個數(shù)字時,
l = 0,r = 1,那么 mid = 0,邊界判定時要用 l <= mid && r <= right
,否則就會跳過對比合并了
完整代碼如下
@Test public void sortTest() { int[] arr = new int[]{8, 4, 5, 7, 1, 3, 6, 2}; int[] temp = new int[arr.length]; mergeSort(arr, 0, arr.length - 1, temp); System.out.println("排序后:" + Arrays.toString(arr)); } /** * 分 和 合并 */ public void mergeSort(int[] arr, int left, int right, int[] temp) { //確保兩個數(shù)組中分別都存在至少一個數(shù)字 if (left < right) { int mid = (left + right) / 2; // 先分解左側 mergeSort(arr, left, mid, temp); // 再分解右側 mergeSort(arr, mid + 1, right, temp); // 最后合并 // 由于是遞歸,合并這里一定是棧頂?shù)南葓?zhí)行(兩邊數(shù)組各只有一個數(shù)時) // 第一次:left = 0,right=1 // 第二次:left = 2,right=3 // 第三次:left = 0,right=3 // System.out.println("left=" + left + ";right=" + right); merge(arr, left, mid, right, temp); } } /** * * 最難的是合并,所以可以先完成合并的方法,此方法請參考 基本思想 和 思路分析中的圖解來完成 * 動腦筋 * * * * @param arr 要排序的原始數(shù)組 * @param left 因為是合并,所以要得到左右兩邊的的數(shù)組信息,這個是左側數(shù)組的第一個索引值 * @param mid 因為是一個數(shù)組,標識是第一個數(shù)組的最后一個索引,即 mid+1 是右側數(shù)組的第一個索引,即中間索引 * @param right 右側數(shù)組的結束索引,右邊數(shù)組的最后一個數(shù) * @param temp 臨時空間,臨時數(shù)組 */ public void merge(int[] arr, int left, int mid, int right, int[] temp) { // 定時臨時變量,用來遍歷數(shù)組比較 int l = left; // 左邊有序數(shù)組的初始索引 int r = mid + 1; // 右邊有序數(shù)組的初始索引 int t = 0; // temp 數(shù)組中當前最后一個有效數(shù)據(jù)的索引 // 1. 按思路先將兩個數(shù)組(有序的)有序的合并到 temp 中 // 因為是合并兩個數(shù)組,所以需要兩邊的數(shù)組都還有值的時候才能進行 比較合并 // 若其中一個數(shù)組的值遍歷完了(取完了),那么就跳出該循環(huán),把另一個數(shù)組所剩的值,直接copy進臨時數(shù)組即可 while (l <= mid && r <= right) { // 如果左邊的比右邊的小,則將左邊的該元素填充到 temp 中 // 并移動 t++,l++,好繼續(xù)下一個 if (arr[l] < arr[r]) { temp[t] = arr[l]; t++;//移動 temp 臨時數(shù)組中當前最后一個有效數(shù)據(jù)的索引 l++; //左邊原始數(shù)組的索引移動 } // 否則則將右邊的移動到 temp 中 else { temp[t] = arr[r]; t++;//移動 temp 臨時數(shù)組中當前最后一個有效數(shù)據(jù)的索引 r++;//右邊原始數(shù)組的索引移動 } } // 2. 如果有任意一邊的數(shù)組還有值,則依序?qū)⑹S鄶?shù)據(jù)填充到 temp 中 // 如果左側還有值 while (l <= mid) { temp[t] = arr[l]; t++; l++; } // 如果右側還有值 while (r <= right) { temp[t] = arr[r]; t++; r++; } // 3. 將 temp 數(shù)組,拷貝到原始數(shù)組 // 注意:只拷貝當前temp有效數(shù)據(jù)到對應的原始數(shù)據(jù)中,通俗點說,就是原始數(shù)組中要排序的數(shù)據(jù),通過temp排成了有序的,然后將temp中的有序數(shù)據(jù)將原始數(shù)組中原來要排序的那部分覆蓋了就行了 int tempL = left; // 從左邊開始拷貝,左邊第一個值的索引 t = 0; // temp 中的有效值索引,有效值邊界可以通過 right 判定,因為合并兩個數(shù)組到 temp 中,邊界就是 right ,這里自己好好想想 /* * 對于 8,4,5,7,1,3,6,2 這個數(shù)組, * 從棧頂開始合并:8,4 | 5,7 1,3 | 6,2 * 先左遞歸的話: * 第一次:8,4 合并:tempL=0,right=1 * 第二次:5,7 合并:tempL=2,right=3 * 第三次:4,8 | 5,7 進行合并,tempL=0,right=3 * 直到左遞歸完成,得到左側一個有序的序列:4,5,7,8 然后再開始遞歸右邊那個無序的 * 最后回到棧底分解成兩個數(shù)組的地方,將兩個數(shù)組合并成一個有序數(shù)組 * 這里真的得自己想了,我只能這么說了。 */ System.out.println("tempL=" + tempL + "; right=" + right); while (tempL <= right) { arr[tempL] = temp[t]; tempL++; t++; } }
測試輸出
tempL=0; right=1
tempL=2; right=3
tempL=0; right=3
tempL=4; right=5
tempL=6; right=7
tempL=4; right=7
tempL=0; right=7
排序后:[1, 2, 3, 4, 5, 6, 7, 8]
從這里也可以看到,先左遞歸的話,可以看到最開始合并的索引是 0,1 也就是在棧頂開始首次遞歸時:兩個數(shù)組中分別只有一個數(shù)字。
最后一次合并:則是回到了最初棧底開始分解的方法,將兩個數(shù)組 0到7
進行排序到臨時數(shù)組 temp
,最后將temp中的數(shù)據(jù)再從 0到7
覆蓋到原始數(shù)組中。完成了排序 。
8 個數(shù)字,會進行 7 次 合并
結合上面動圖進行思考。
對代碼的一些改進
根據(jù)上述所講的基本思想和思路分析,對代碼進行了一些改進修改,減小代碼的臃腫。
public class MyMergeSortTest { @Test public void sortTest() { int[] arr = new int[]{8, 4, 5, 7, 1, 3, 6, 2}; mergeSort(arr); System.out.println("排序后:" + Arrays.toString(arr)); } public void mergeSort(int arr[]) { int[] temp = new int[arr.length]; doMergeSort(arr, 0, arr.length - 1, temp); } /** * 分治 與 合并 * * @param arr * @param left * @param right * @param temp */ private void doMergeSort(int[] arr, int left, int right, int[] temp) { // 當還可以分解時,就做分解操作 if (left < right) { int mid = (left + right) / 2; // 先左遞歸分解 doMergeSort(arr, left, mid, temp); // 再右遞歸分解 doMergeSort(arr, mid + 1, right, temp); // 左遞歸分解到棧頂時,其實也是分為左右數(shù)組 // 左右都到棧頂時,開始合并: // 第一次:合并的是 0,1 的索引,分解到最后的時候,其實只有一個數(shù)為一組,所以第一次合并是合并兩個數(shù)字 merge(arr, left, mid, right, temp); } } /** * 開始合并,每次合并都是兩組,第一次合并是兩個數(shù)字,左右一組都只有一個數(shù)字 * * @param arr * @param left * @param mid * @param right * @param temp */ private void merge(int[] arr, int left, int mid, int right, int[] temp) { // 1. 按照規(guī)則,將 temp 數(shù)組填充 // 2. 當任意一邊填充完成后,剩余未填充的依次填充到 temp 數(shù)組中 // 3. 將 temp 數(shù)組的有效內(nèi)容,拷貝會原數(shù)組,也就是將這次排序好的數(shù)組覆蓋回原來排序的原數(shù)組中 int l = left; // 左側數(shù)組初始索引 int r = mid + 1; // 右側數(shù)組初始索引 int t = 0; // 當前 temp 中有效數(shù)據(jù)的最后一個元素索引 // 1. 按照規(guī)則,將 temp 數(shù)組填充 // 當兩邊都還有數(shù)字可比較的時候,進行比較,然后填充 temp 數(shù)組 // 只要任意一邊沒有數(shù)值可用時,就僅需到下一步驟 while (l <= mid && r <= right) { // 當左邊比右邊小時,則填充到 temp 數(shù)組中 if (arr[l] < arr[r]) { // 賦值完成后,t 和 l 都需要 +1,往后移動一個位置 // t++ 是先把 t 進行賦值,再進行 t+1 操作 temp[t++] = arr[l++]; } else { // 當不滿足時,則說明 右側數(shù)字比左側的小,進行右側的填充 temp[t++] = arr[r++]; } } // 2. 有可能有其中一邊會有剩余數(shù)字未填充到 temp 中,進行收尾處理 while (l <= mid) { temp[t++] = arr[l++]; } while (r <= right) { temp[t++] = arr[r++]; } // 3. 將這個有序數(shù)組,覆蓋會原始排序的數(shù)組中 t = 0; int tempL = left; // 從左側開始,到右側結束,就是這一次要合并的兩組數(shù)據(jù) while (tempL <= right) { arr[tempL++] = temp[t++]; } } }
大數(shù)據(jù)量耗時測試
/** * 大量數(shù)據(jù)排序時間測試 */ @Test public void bulkDataSort() { int max = 80000; // max = 8; int[] arr = new int[max]; for (int i = 0; i < max; i++) { arr[i] = (int) (Math.random() * 80000); } if (arr.length < 10) { System.out.println("原始數(shù)組:" + Arrays.toString(arr)); } Instant startTime = Instant.now(); int[] temp = new int[arr.length]; mergeSort(arr, 0, arr.length - 1, temp); if (arr.length < 10) { System.out.println("排序后:" + Arrays.toString(arr)); } Instant endTime = Instant.now(); System.out.println("共耗時:" + Duration.between(startTime, endTime).toMillis() + " 毫秒"); }
多次測試輸出
共耗時:26 毫秒
共耗時:37 毫秒
共耗時:30 毫秒
共耗時:30 毫秒
復雜度
歸并排序比較占用內(nèi)存,但卻是一種效率高且穩(wěn)定的算法。
改進歸并排序在歸并時先判斷前段序列的最大值與后段序列最小值的關系再確定是否進行復制比較。如果前段序列的最大值小于等于后段序列最小值,則說明序列可以直接形成一段有序序列不需要再歸并,反之則需要。所以在序列本身有序的情況下時間復雜度可以降至O(n)。
TimSort可以說是歸并排序的終極優(yōu)化版本,主要思想就是檢測序列中的天然有序子段(若檢測到嚴格降序子段則翻轉(zhuǎn)序列為升序子段)。在最好情況下無論升序還是降序都可以使時間復雜度降至為O(n),具有很強的自適應性。
最好時間復雜度 最壞時間復雜度 平均時間復雜度 空間復雜度 穩(wěn)定性 傳統(tǒng)歸并排序 O(nlogn) O(nlogn) O(nlogn) T(n) 穩(wěn)定 改進歸并排序 [1] O(n) O(nlogn) O(nlogn) T(n) 穩(wěn)定 TimSort O(n) O(nlogn) O(nlogn) T(n) 穩(wěn)定
我個人感覺歸并排序的邏輯相比快速排序來說更為容易理解,而且時間復雜度和快排一樣。關于快排的有關講解請看java 排序算法之快速排序。
到此這篇關于java 排序算法之歸并排序的文章就介紹到這了,更多相關java 歸并排序內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot中l(wèi)ogback日志保存到mongoDB的方法
這篇文章主要介紹了SpringBoot中l(wèi)ogback日志保存到mongoDB的方法,2017-11-11SpringBoot使用PropertiesLauncher加載外部jar包
這篇文章主要介紹了SpringBoot使用PropertiesLauncher加載外部jar包,本文結合實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07SpringBoot項目使用MDC給日志增加唯一標識的實現(xiàn)步驟
本文介紹了如何在SpringBoot項目中使用MDC(Mapped?Diagnostic?Context)為日志增加唯一標識,以便于日志追蹤,通過創(chuàng)建日志攔截器、配置攔截器以及修改日志配置文件,可以實現(xiàn)這一功能,文章還提供了源碼地址,方便讀者學習和參考,感興趣的朋友一起看看吧2025-03-03MyBatis-Plus Sequence主鍵的實現(xiàn)
這篇文章主要介紹了MyBatis-Plus Sequence主鍵的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-12-12Java中的FileWriter用法詳解與實戰(zhàn)記錄
這篇文章主要給大家介紹了關于Java中FileWriter用法的相關資料,包括寫入字符數(shù)據(jù)到文件、字符數(shù)組和部分字符寫入、配合BufferedWriter使用等方法,同時也解釋了其與OutputStreamWriter,BufferedWriter的異同特性,適合簡單的文件寫入操作,需要的朋友可以參考下2024-10-10