算法与数据结构(归并排序)
歸并排序 Merge Sort
利用分治策略來解決排序問題
分治法 Divide and Conquer:
分治法的基本思想如下:
- 分解:將原問題分解為若干子問題,這些子問題是原問題的規模較小的實例。
- 解決:遞歸地求解這些子問題。當子問題規模足夠小時,可直接求解。
- 合并:合并子問題的解成原問題的解。
歸并排序算法完全遵循分治模式。其操作步驟如下:
- 分解:將原序列分解為兩個各占 n/2n/2n/2 個元素的子序列。
- 解決:使用歸并排序遞歸地排序兩個子序列。這里的策略是不斷分解直到子序列只包含一個元素,此時子序列必然有序。
- 合并:合并兩個已排序的子序列以產生有序序列。
歸并排序的思路:
如上所述,歸并排序的基本思路是先將序列分為兩部分,讓左右兩部分分別有序,然后合并。但是左右兩部分的排序又重復上述過程,不斷地分裂成兩部分,直到最終都只有一個元素,此時所有的數組都是有序的。然后不斷合并兩個有序數組,直到最終成為一個整體。
其中的重點是合并步驟。假設數組為 AAA,ppp、qqq 和 rrr 是數組下標?,F在子數組 A[p..q]A[p..q]A[p..q] 和
A[q+1..r]A[q+1..r]A[q+1..r] 都已排好序,我們要合并這兩個有序子數組以形成單一的有序數組并替代當前的 A[p..r]A[p..r]A[p..r]。
還是以撲克牌為例,假設現在有兩堆牌面朝上的牌,每堆都已從小到大排好序,最小的牌位于頂端。我們每一次都從兩堆牌的頂上選擇更小的那一張放入輸出堆中,這樣不斷循環直到一個牌堆為空,然后將剩余的牌堆全部放入輸出堆即可。合并過程用偽代碼表示如下:
MERGE(A, p, q, r) n1 = q-p+1 n2 = r-q let L[1..n1=1] and R[1..n2+1] be new arrays for i = 1 to n1L[i] = A[p+i-1] for j = 1 to n2R[j] = A[q+j] L[n1+1] = inf R[n2+1] = inf i = 1 j = 1 for k = p to rif L[i] <= R[i]A[k] = L[i]i = i+1else A[k] = R[j]j = j+1值得注意的是可以使用哨兵來避免判斷數組溢出。在第 9-10 行,我們將兩個臨時列表的最后一位元素設置為無窮大,當一個牌堆已經到底的時候,其哨兵牌將大于另一牌堆的所有牌(哨兵牌除外)。
之后我們就可以把上述的合并過程當作歸并排序的一個子程序來使用。我們使用 MERGE?SORT(A,p,r)\mathrm{MERGE-SORT}(A, p, r)MERGE?SORT(A,p,r) 來對子數組 A[p..r]A[p..r]A[p..r] 排序:
MERGE_SORT(A, p, r) if p < rq = floor((p+r)/2) // 向下取整MERGE_SORT(A, p, q)MERGE_SORT(A, q+1, r)MERGE(A, p, q, r)第 4-5 行對原序列不斷分解,當全部子序列都只包含一個元素時,就已經完成了對最小子序列的排序工作,然后就開始合并。
圖示如下:
分解:
合并:
實現:
以 Java 為例,C++代碼見文末。
首先實現并的部分,當我們有兩個有序數組需要合并時(這里采取的策略是用一個額外的數組 temptemptemp 來保存排好序的數組,然后將這個 temptemptemp 內的值返回到原數組),我們需要三個指針 left,midleft, midleft,mid 和 rightrightright , 分別比較兩個數組中較小的那個,保存到 temptemptemp 中。排序完畢后,將 temptemptemp 中的數據返回到原數組中。
這里用最后一次的排序舉例:
然后是拆分部分,主要是遞歸
代碼如下:
// javapublic static void mergeSort(int[] arr, int left, int right, int[] temp) {if (left < right) {int mid = (left + right)/2;// 向左分解mergeSort(arr, left, mid, temp);// 向右分解mergeSort(arr, mid+1, right, temp);// 合并merge(arr, left, mid, right, temp);}}下圖是邏輯順序:
時間復雜度:
歸并排序的時間復雜度主要考慮兩個函數需要的時間,即 mergeSort 和 merge
merge 合并的時間復雜度為 O(n)O(n)O(n),因為全部都是長度為 nnn 的一次循環
利用之前說過的遞歸的時間復雜度公式,歸并排序的時間復雜度為 T(n)=2(T(n/2))+O(n)T(n) = 2(T(n/2))+O(n)T(n)=2(T(n/2))+O(n)
這個遞歸式可以用遞歸樹來解(假設解決最后一步子問題的時間是常數 ccc,則 nnn 個子問題花費的時間為 cncncn):
我們可以看到遞歸的每一層需要的時間都是 cncncn,總共有 logn+1logn+1logn+1 層,總的時間為 cn(logn+1)cn(logn+1)cn(logn+1) ,時間復雜度是 O(nlogn)O(nlogn)O(nlogn)。
空間復雜度:
歸并排序在每一次合并時需要臨時數組來儲存排好序的序列,最后一次排序,要儲存的數字最多,為 nnn,所以空間復雜度為 O(n)O(n)O(n)。
穩定性:
歸并排序可以保證等值元素之間的順序,因此是穩定的
相關章節
第一節 簡述
第二節 稀疏數組 Sparse Array
第三節 隊列 Queue
第四節 單鏈表 Single Linked List
第五節 雙向鏈表 Double Linked List
第六節 單向環形鏈表 Circular Linked List
第七節 棧 Stack
第八節 遞歸 Recursion
第九節 時間復雜度 Time Complexity
第十節 排序算法 Sort Algorithm
第十一節 冒泡排序 Bubble Sort
第十二節 選擇排序 Select Sort
第十三節 插入排序 Insertion Sort
第十四節 冒泡排序,選擇排序和插入排序的總結
第十五節 希爾排序 Shell’s Sort
第十六節 快速排序 Quick Sort
第十七節 歸并排序 Merge Sort
C++ 實現:
#include <iostream> #include <vector> using namespace std;/*** @brief Merge two ordered lists(nums[low..mid], nums[mid+1..high]).* * @param nums * @param low * @param mid * @param high */ void merge(vector<int>& nums, int low, int mid, int high) {int len1 = mid-low+1;int len2 = high-mid;vector<int> left(len1+1);vector<int> right(len2+1);for (int i=0;i<len1;i++) {left[i] = nums[i+low];}for (int j=0;j<len2;j++) {right[j] = nums[j+mid+1];}// 設置哨兵left[len1] = INT_MAX;right[len2] = INT_MAX;// 合并 int i = 0;int j = 0;for (int k=low;k<=high;k++) {if (left[i] <= right[j]) {nums[k] = left[i++];} else {nums[k] = right[j++];}}}void mergeSort(vector<int>& nums, int low, int high) {if (low < high) {int mid = (high-low)/2+low;mergeSort(nums, low, mid);mergeSort(nums, mid+1, high);merge(nums, low, mid, high);} }// test int main(int argc, char const *argv[]) {vector<int> nums = {4,7,2,9};mergeSort(nums, 0, nums.size()-1);for (auto num : nums) {cout << num << endl;}return 0; }總結
以上是生活随笔為你收集整理的算法与数据结构(归并排序)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法与数据结构(快速排序)
- 下一篇: 二、【绪论】算法和算法评价