分治法之合併排序演算法理解介紹
複習分治法,藉著這個機會將用到分治法的合併排序和快速排序演算法好好梳理一下並作出總結。
分治法求解問題的三要素是分解、求解、合併。分解是指將一個難以直接求解的複雜問題按照某種方式分解成若干個規模較小、相互獨立且和原問題型別相同的子問題,求解是指子問題分解至可直接求解,合併是指將子問題的解以某種方式合併成原問題的解。
分治法求解排序問題的思想很簡單,只需以某種方式將序列分成兩個或多個子序列,分別進行排序,再將已排序的子序列合併成一個有序序列即可。合併排序和快速排序是兩種典型的運用了分治策略的排序演算法。
首先介紹最基本的合併排序演算法——兩路合併排序。
兩路合併排序的基本運算是將兩個有序序列合併成一個有序序列。以數列一(5,25,55)和數列二(10,20,30)合併成有序序列(5,10,20,25,30,55)。實現這種合併的演算法:比較兩個數列的最小值,輸出其中的較小者,重複此過程直至一數列為空,依次輸出另一非空數列的剩餘元素即可。下面演示此過程:
數列一:5,25,55 數列二:10,20,30 生成數列:
5<10 輸出5 數列一:25,55 數列二:10,20,30 生成數列:5
10<25 輸出10 數列一:25,55 數列二:20,30 生成數列:5,10
…… 數列一:55 數列二: 生成數列:5,10,20,25,30
輸出55 數列一:55 數列二: 生成數列:5,10,20,25,30,55
演算法執行完成。
筆者使用的是陳慧南版演算法設計與分析。其中演算法主要以類的形式實現,不過在理解好演算法之後相信是很容易將之獨立抽象成函式。
下面貼出以上合併函式的程式碼:
template <class T> void SortableList<T>::Merge(int left, int mid, int right) { //a陣列為待排序的陣列 T* temp =new T[right - left + 1]; //temp陣列用來暫時儲存排序好的陣列 int i = left, j = mid + 1, k = 0; //left 至mid為左半陣列 ,mid+1至right為右半陣列 while((i <= mid)&&(j <= right)) //對應第一步 當左半陣列和右半陣列都不為空時 if(a[i] <= a[j]) //輸出兩個陣列中的較小者 temp[k ++] = a[i ++]; else temp[k ++] = a[j ++]; while(i <= mid) //當一陣列為空時,輸出另一陣列的剩餘元素 temp[k ++] = a[i ++]; while(j <= right) //當一陣列為空時,輸出另一陣列的剩餘元素 temp[k ++] = a[j ++]; for(i = 0, k = left; k <= right;) //將temp陣列賦值給原陣列 a[k ++] = temp[i ++]; }
合併排序的分解很簡單,主要難點在於合併(merge)函式而快速排序難點在於分解,這個稍後再說。下面使用分治法求解,分治法兩路合併演算法可描述為三步:第一步,將待排序的元素序列一分為二,得到兩個長度基本相同的子序列;第二步,對兩個子序列分別排序,若子序列較長則繼續細分重複以上一分為二的步驟,直至子序列的長度不大於1為止,此為關鍵步驟,需要將數列二分到每個子數列的長度不超過一為止,因為只有不大於一的數列可直接求解,這一點在之後過程會演示;第三步將排好序的子序列合併成一個有序數列,實現函式即為以上的merge函式。
以陣列5,2,4,7,1,3,2,6為例,先將整個陣列分解至每個子序列長度不超過1,即分解至以下圖片的最底一行,然後呼叫merge函式向上層逐層合併遞迴成最終排序好的數列。
以下是mergesort函式程式碼:template<class T>
void SortableList<T>::MergeSort(int left, int right)
{
if(left < right) //控制 當呼叫至序列只剩一個元素結束函式向上層遞迴執行
{
int mid = (left + right) / 2;// 一分為二
MergeSort(left, mid); //二分左邊序列
MergeSort(mid + 1, right);//二分右邊序列
Merge(left, mid, right); //排序
}
}
最後貼上完整程式碼:
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
void Swap(int &a, int &b)
{
int t = a;
a = b;
b = t;
}
template <class T>
class SortableList
{
public:
SortableList(int m)
{
n = m;
}
void MergeSort();
void Merge(int left, int mid, int right);
void QuickSort();
void Input();
void Init();
void Output();
private:
int RPartition(int left, int right);
int Partition(int left, int right);
void MergeSort(int left, int right);
void QuickSort(int left, int right);
T l[1000];//輸入的陣列值
T a[1000];//實際排序物件
int n;
};
template<class T>
void SortableList<T>::Input()
{
for(int i = 0; i < n; i++)
cin >> l[i];
}
//Init()函式的作用是在兩路合併排序結束後將序列恢復到初始序列
//再進行快速排序
template<class T>
void SortableList<T>::Init()
{
for(int i =0; i < n; i++)
a[i] = l[i];
}
template<class T>
void SortableList<T>::Output()
{
for(int i = 0; i < n; i++)
cout << a[i] << " ";
cout << endl << endl;
}
//兩路合併排序
template<class T>
void SortableList<T>::MergeSort()
{
MergeSort(0, n - 1);
}
template<class T>
void SortableList<T>::MergeSort(int left, int right)
{
if(left < right) //控制 當呼叫至序列只剩一個元素結束函式向上層遞迴執行
{
int mid = (left + right) / 2;// 一分為二
MergeSort(left, mid); //二分左邊序列
MergeSort(mid + 1, right);//二分右邊序列
Merge(left, mid, right); //排序
}
}
template <class T>
void SortableList<T>::Merge(int left, int mid, int right)
{ //a陣列為待排序的陣列
T* temp =new T[right - left + 1]; //temp陣列用來暫時儲存排序好的陣列
int i = left, j = mid + 1, k = 0; //left 至mid為左半陣列 ,mid+1至right為右半陣列
while((i <= mid)&&(j <= right)) //對應第一步 當左半陣列和右半陣列都不為空時
if(a[i] <= a[j]) //輸出兩個陣列中的較小者
temp[k ++] = a[i ++];
else
temp[k ++] = a[j ++];
while(i <= mid) //當一陣列為空時,輸出另一陣列的剩餘元素
temp[k ++] = a[i ++];
while(j <= right) //當一陣列為空時,輸出另一陣列的剩餘元素
temp[k ++] = a[j ++];
for(i = 0, k = left; k <= right;) //將temp陣列賦值給原陣列
a[k ++] = temp[i ++];
}
int main()
{
int m;
cout << "陣列長度n: ";
cin >> m;
SortableList<int> List(m);
cout << "輸入" << m << "個數字:" << endl;
List.Input();
List.Init();//得到初始狀態
List.MergeSort();
cout << "兩路合併排序後:" << endl;
List.Output();
return 0;
}
在此簡單介紹了分治法應用在合併排序演算法中的原理和執行流程。
特記下,以備後日回顧。