(五) 資料結構 - 歸併排序
阿新 • • 發佈:2021-01-06
歸併排序
歸併排序是一種基於分而治之的排序技術。最壞情況下的時間複雜度為O(nlogn),它是最受人尊敬的演算法之一。歸併排序首先將陣列分成相等的兩半,然後以排序的方式將它們合併。
核心思想
為了理解合併排序,我們採用未排序的陣列,如下所示
我們知道歸併排序首先將整個陣列迭代地分成相等的一半,除非獲得原子值。我們在這裡看到一個由8個專案組成的陣列分為兩個大小為4的陣列。
這不會更改原件中專案出現的順序。現在我們將這兩個陣列分為兩半。
我們進一步劃分這些陣列,並獲得無法再劃分的原子值
現在,我們將它們分解時的方式完全相同。請注意提供給這些列表的顏色程式碼。
我們首先比較每個列表的元素,然後以排序的方式將它們組合到另一個列表中。我們看到14和33處於排序位置。我們比較27和10,在2個值的目標列表中,我們先放置10,然後是27。我們更改19和35的順序,而將42和44順序放置。
在合併階段的下一個迭代中,我們比較兩個資料值的列表,然後將它們合併為找到的資料值的列表,將所有資料按排序順序放置。
最終合併後,列表應如下所示:
程式碼開發
實現思路
歸併排序會繼續將列表分為相等的一半,直到無法再對其進行劃分為止。根據定義,如果它只是列表中的一個元素,則會對其進行排序。然後,合併排序將合併較小的排序列表,同時也將新列表排序。
Step 1−如果列表中的元素已被排序,則返回。 Step 2−將列表遞迴分為兩半,直到無法再將其劃分為止。 Step 3−將較小的列表按排序順序合併到新列表中。
虛擬碼
package com.paal.demo.c01SortingBasic;
import com.paal.demo.Sort;
import java.util.Arrays;
/**
* <p/>
* <li>title: 基礎排序-歸併排序</li>
* <li>@author: li.pan</li>
* <li>Date: 2019/12/7 12:15 下午</li>
* <li>Version: V1.0</li>
* <li>Description: </li>
*/
public class MergeSort implements Sort {
@Override
public void sort(Integer[] arr) {
int n = arr.length;
sort(arr, 0, n - 1);
}
//遞迴使用歸併排序,對arr[l....r]的範圍進行進行排序
private static void sort(Integer[] 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);
}
//將arr[l...mid]和arr[mid+1...r]兩部分進行歸併
private static void merge(Integer[] arr, int l, int mid, int r) {
// 開闢臨時空間,合併左半部分已經排好序的陣列和右半部分已經排好序的陣列
Integer[] aux = Arrays.copyOfRange(arr, l, r + 1);
// 初始化,i指向左半部分的起始位置索引;j指向右半部分起始索引位置mid+1
int i = l, j = mid + 1;
for (int k = l; k <= r; k++) { // k指向兩個元素比較後歸併下一個需要放置的位置
/**
* 考慮陣列越界
*/
if (i > mid) { // 如果左半部分元素已經全部處理完畢,
arr[k] = aux[j - l];
j++;
} else if (j > r) { // 如果右半部分元素已經全部處理完畢
arr[k] = aux[i - l];
i++; // l表示偏移
}
/**
* 真正比較
*/
else if (aux[i - l] < aux[j - l]) { // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i - l];
i++;
} else { // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j - l];
j++;
}
}
}
}
/**
* 希爾排序
*/
@Test
public void mergeSortTest() {
Integer[] integers0 = SortTestHelper.generateRandomArray(100, 0, 1000000);
Integer[] integers1 = SortTestHelper.generateRandomArray(10000, 0, 1000000);
Integer[] integers2 = SortTestHelper.generateRandomArray(100000, 0, 1000000);
System.out.println("------------------------------隨機陣列--------------------------------");
System.out.println("插入排序測試1資料量為100"+SortTestHelper.testSort(integers0, new InsertionSort()));
System.out.println("插入排序測試2資料量為10000"+SortTestHelper.testSort(integers1, new InsertionSort()));
System.out.println("插入排序測試3資料量為100000"+SortTestHelper.testSort(integers2, new InsertionSort()));
System.out.println("氣泡排序測試1資料量為100"+SortTestHelper.testSort(integers0, new BubbleSort()));
System.out.println("氣泡排序測試2資料量為10000"+SortTestHelper.testSort(integers1, new BubbleSort()));
System.out.println("氣泡排序測試3資料量為100000"+SortTestHelper.testSort(integers2, new BubbleSort()));
System.out.println("希爾排序測試1資料量為100"+SortTestHelper.testSort(integers0, new ShellSort()));
System.out.println("希爾排序測試2資料量為10000"+SortTestHelper.testSort(integers1, new ShellSort()));
System.out.println("希爾排序測試3資料量為100000"+SortTestHelper.testSort(integers2, new ShellSort()));
System.out.println("歸併排序測試1資料量為100"+SortTestHelper.testSort(integers0, new MergeSort()));
System.out.println("歸併排序測試2資料量為10000"+SortTestHelper.testSort(integers1, new MergeSort()));
System.out.println("歸併排序測試3資料量為100000"+SortTestHelper.testSort(integers2, new MergeSort()));
}
執行結果
------------------------------隨機陣列--------------------------------
插入排序測試1資料量為100排序時長為: 0.001s
插入排序測試2資料量為10000排序時長為: 0.084s
插入排序測試3資料量為100000排序時長為: 5.868s
氣泡排序測試1資料量為100排序時長為: 0.0s
氣泡排序測試2資料量為10000排序時長為: 0.061s
氣泡排序測試3資料量為100000排序時長為: 9.069s
希爾排序測試1資料量為100排序時長為: 0.0s
希爾排序測試2資料量為10000排序時長為: 0.004s
希爾排序測試3資料量為100000排序時長為: 0.008s
程式碼優化
- 雖然歸併排序是nlogn級別的演算法, 但是在陣列資料量比較小的時候, 插入排序的效率仍然是高於歸併排序的, 所以可以在對陣列分解到足夠小之後, 使用插入排序, 然後再遞迴進行歸併排序。
- 如果一個數組是近乎有序的, 或者說是完全有序的, 上述步驟會有很多無用的merge操作, 所以可以在進行merge前增加一個判斷, 效率也會有一定的提高。
虛擬碼
package com.paal.demo.c01SortingBasic.optimize;
import com.paal.demo.Sort;
import com.paal.demo.c01SortingBasic.InsertionSort;
import java.util.Arrays;
/**
* <p/>
* <li>title: 歸併排序優化</li>
* <li>@author: li.pan</li>
* <li>Date: 2019/12/7 12:15 下午</li>
* <li>Version: V1.0</li>
* <li>Description:
* 優化1: 對於arr[mid] <= arr[mid+1]的情況,不進行merge
* 優化2: 對於小規模陣列, 使用插入排序
* </li>
*/
public class MergeSortOptimize implements Sort {
@Override
public void sort(Integer[] arr) {
int n = arr.length;
sort(arr, 0, n - 1);
}
//遞迴使用歸併排序,對arr[l....r]的範圍進行進行排序
private static void sort(Integer[] arr, int l, int r) {
/**
* 優化2: 對於小規模陣列, 使用插入排序
*/
if( r - l <= 15 ){
insertionSort(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] > arr[mid + 1])
merge(arr, l, mid, r);
}
//將arr[l...mid]和arr[mid+1...r]兩部分進行歸併
private static void merge(Integer[] arr, int l, int mid, int r) {
// 開闢臨時空間,合併左半部分已經排好序的陣列和右半部分已經排好序的陣列
Integer[] aux = Arrays.copyOfRange(arr, l, r + 1);
// 初始化,i指向左半部分的起始位置索引;j指向右半部分起始索引位置mid+1
int i = l, j = mid + 1;
for (int k = l; k <= r; k++) { // k指向兩個元素比較後歸併下一個需要放置的位置
/**
* 考慮陣列越界
*/
if (i > mid) { // 如果左半部分元素已經全部處理完畢,
arr[k] = aux[j - l];
j++;
} else if (j > r) { // 如果右半部分元素已經全部處理完畢
arr[k] = aux[i - l];
i++; // l表示偏移
}
/**
* 真正比較
*/
else if (aux[i - l] < aux[j - l]) { // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i - l];
i++;
} else { // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j - l];
j++;
}
}
}
// 對arr[l...r]的區間使用InsertionSort排序
public static void insertionSort(Integer[] arr, int l, int r){
for( int i = l + 1 ; i <= r ; i ++ ){
Integer e = arr[i];
int j = i;
for( ; j > l && arr[j-1]>e ; j--)
arr[j] = arr[j-1];
arr[j] = e;
}
}
}
/**
* 歸併排序優化測試
*/
@Test
public void mergeOptimizeSortTest() {
Integer[] integers0 = SortTestHelper.generateRandomArray(100, 0, 1000000);
Integer[] integers1 = SortTestHelper.generateRandomArray(10000, 0, 1000000);
Integer[] integers2 = SortTestHelper.generateRandomArray(100000, 0, 1000000);
System.out.println("------------------------------隨機陣列--------------------------------");
System.out.println("插入排序測試1資料量為100" + SortTestHelper.testSort(integers0, new InsertionSort()));
System.out.println("插入排序測試2資料量為10000" + SortTestHelper.testSort(integers1, new InsertionSort()));
System.out.println("插入排序測試3資料量為100000" + SortTestHelper.testSort(integers2, new InsertionSort()));
System.out.println("歸併排序測試1資料量為100" + SortTestHelper.testSort(integers0, new MergeSort()));
System.out.println("歸併排序測試2資料量為10000" + SortTestHelper.testSort(integers1, new MergeSort()));
System.out.println("歸併排序測試3資料量為100000" + SortTestHelper.testSort(integers2, new MergeSort()));
System.out.println("歸併排序優化測試1資料量為100" + SortTestHelper.testSort(integers0, new MergeSortOptimize()));
System.out.println("歸併排序優化測試2資料量為10000" + SortTestHelper.testSort(integers1, new MergeSortOptimize()));
System.out.println("歸併排序優化測試3資料量為100000" + SortTestHelper.testSort(integers2, new MergeSortOptimize()));
Integer[] integers00 = SortTestHelper.generateNearlyOrderedArray(100, 50);
Integer[] integers11 = SortTestHelper.generateNearlyOrderedArray(10000, 5000);
Integer[] integers22 = SortTestHelper.generateNearlyOrderedArray(100000, 50000);
System.out.println("------------------------------近乎有序陣列--------------------------------");
System.out.println("插入排序測試1資料量為100" + SortTestHelper.testSort(integers00, new InsertionSort()));
System.out.println("插入排序測試2資料量為10000" + SortTestHelper.testSort(integers11, new InsertionSort()));
System.out.println("插入排序測試3資料量為100000" + SortTestHelper.testSort(integers22, new InsertionSort()));
System.out.println("歸併排序測試1資料量為100" + SortTestHelper.testSort(integers00, new MergeSort()));
System.out.println("歸併排序測試2資料量為10000" + SortTestHelper.testSort(integers11, new MergeSort()));
System.out.println("歸併排序測試3資料量為100000" + SortTestHelper.testSort(integers22, new MergeSort()));
System.out.println("歸併排序優化測試1資料量為100" + SortTestHelper.testSort(integers00, new MergeSortOptimize()));
System.out.println("歸併排序優化測試2資料量為10000" + SortTestHelper.testSort(integers11, new MergeSortOptimize()));
System.out.println("歸併排序優化測試3資料量為100000" + SortTestHelper.testSort(integers22, new MergeSortOptimize()));
}
執行結果
------------------------------隨機陣列--------------------------------
插入排序測試1資料量為100排序時長為: 0.001s
插入排序測試2資料量為10000排序時長為: 0.12s
插入排序測試3資料量為100000排序時長為: 8.578s
歸併排序測試1資料量為100排序時長為: 0.0s
歸併排序測試2資料量為10000排序時長為: 0.013s
歸併排序測試3資料量為100000排序時長為: 0.025s
歸併排序優化測試1資料量為100排序時長為: 0.0s
歸併排序優化測試2資料量為10000排序時長為: 0.001s
歸併排序優化測試3資料量為100000排序時長為: 0.003s
------------------------------近乎有序陣列--------------------------------
插入排序測試1資料量為100排序時長為: 0.0s
插入排序測試2資料量為10000排序時長為: 0.039s
插入排序測試3資料量為100000排序時長為: 2.764s
歸併排序測試1資料量為100排序時長為: 0.0s
歸併排序測試2資料量為10000排序時長為: 0.0s
歸併排序測試3資料量為100000排序時長為: 0.009s
歸併排序優化測試1資料量為100排序時長為: 0.0s
歸併排序優化測試2資料量為10000排序時長為: 0.0s
歸併排序優化測試3資料量為100000排序時長為: 0.0s