排序演算法(六)、歸併排序
1、二路歸併排序
“歸併”即“合併”,是指將兩個或者兩個以上有序表組合成一個有序表。假如待排序表含有 n 個記錄,即可以視為 n 個有序的子表。每個子表長度為1,然後兩兩歸併,得到 n/2 個長度為 2 或者 1 的有序表,然後,再兩兩歸併,。。。。如此重複,直到合併成一個長度為 n 的有序表為止。這種排序方法稱為“二路歸併排序”。
遞迴形式的二路歸併演算法,主要包含兩個步驟:
(1)、分解:將長度為 n 的待排序表分解成兩個 n/2 大小的子表,然後,遞迴地對兩個子表進行排序。。
(2)、合併:將子表合併。
(1)部分程式碼如下:
void MergeSort(int a[], int low, int high) { if (low < high) { int mid = (low + high) / 2; MergeSort(a,low,mid); MergeSort(a,mid+1,high); Merge(a,low,mid,high); } }
(2)部分程式碼如下:
// MergeSort 歸併排序 void Merge(int a[], int low, int mid, int high) { int i = low, j = mid + 1, k = 0; // j 是從 mid + 1 開始的。 int *b = new(nothrow) int[high - low + 1]; if (!b) { cout << "分配失敗" << endl; return; } while (i <= mid && j <= high) // 分兩路進行比較,把資料較小的放到中介陣列 { if (a[i] <= a[j]) b[k++] = a[i++]; else b[k++] = a[j++]; } while (i <= mid) // 若某個區間的資料仍然有剩餘,就直接把剩餘的資料複製到中介陣列 b[k++] = a[i++];// 這兩個迴圈只會執行其中一個。。因為肯定不會兩邊都有剩餘 while (j <= high) b[k++] = a[j++]; for (int i = low,k=0; i <= high; i++,k++) // 將 b 陣列的元素複製到原陣列 a[i] = b[k]; delete[]b; }
測試:
int main()
{
int a[] = { 1, 5, 3, 4, 12, 35, 21, 9 };
mergesort(a,0,7);
for (int i = 0; i < 8; i++)
cout << a[i] << endl;
return 0;
}
對於非遞迴的形式,只需要更改 merge() 即可。
void MergeSort(int arr[], int n)//n代表陣列中元素個數,陣列最大下標是n-1 { int size = 1, low, mid, high; while (size <= n - 1) // 使用步長來控制 { low = 0; while (low + size <= n - 1) { mid = low + size - 1; high = mid + size; if (high>n - 1)//第二個序列個數不足size high = n - 1; Merge(arr, low, mid, high);//呼叫歸併子函式 //cout << "low:" << low << " mid:" << mid << " high:" << high << endl;//打印出每次歸併的區間 low = high + 1;//下一次歸併是第一關序列的下界 } size *= 2;//範圍擴大一倍 } }
舉例說明一下:
// 非遞迴版本的 MergeSort
// 使用一個例子來解釋這個排序,假設有 8 個數據,[48],[38],[65],[97],[76],[13],[27],[33],元素個數 n = 8
// 初始時,size = 1,此時,size <= 7,
// 第一次內迴圈時,即 low + size <= n - 1,因為每次 low 都會遞增,所以要保證不越界
// 一開始,low = 0,mid = 0,high = 1,這可以看成是元素的下標。呼叫歸併函式,將 [48],[38]變成 [38,48].
// low = high +1 = 2,mid = 2,high = 3,呼叫歸併函式,將 [69],[57]變成 [57,69].
//low = high +1 = 4,mid = 4,high = 5,呼叫歸併函式,將 [76],[13]變成 [13,76].
//low = high +1 = 6,mid = 6,high = 7,呼叫歸併函式,將 [27],[33]變成 [27,33].,這樣,序列變成了 [38,48],[57,69],[13,76],[27,33]
// 第二次,size = 2,size <=7
//一開始,low = 0,mid = 1,high = 3,[38,48],[57,69]變成了 [38,48,57,69]
//然後,low = 3+1 = 4, mid = 5, high = 7, [13,76],[27,33]變成了[13,27,33,76]
// 第三次,size = 4,size <=7
//一開始,low = 0,mid = 3,high = 7,這樣 [38,48,57,69],[13,27,33,76] 就變成了 [13,27,33,38,48,57,69,76]
效能分析:
空間複雜度:上面的 merge () 函式需要申請輔助單元, n 個單元。但是,每一趟歸併以後,這些空間就被釋放了,所以時間複雜度為 O(n)
時間複雜度: 每一趟歸併的複雜度為 O(n),總共進行 log2 (n) 趟歸併。所以時間複雜度為 O(nlog2 (n))。
升級——原地歸併排序
既然 merge() 函式需要輔助單元,那麼,不使用額外的儲存空間可以嗎?