歸併排序與分治算法詳解

每日一算法,今天我們來談談分治算法,再結合算法看看歸併排序的實現。同時進一步探討一下如果從分治算法的結構算出算法的時間複雜度,這點尤爲重要。

首先分治算法模型有三個基本步驟:

1.分解:將原問題分解成若干個子問題,這些子問題是原問題的規模較小的實例。

2.解決:將這些子問題再進一步遞歸的分解。當若干子問題的規模足夠小時,就直接求解。

3.合併:將上述子問題的解合併成最終問題的解。(這一步至關重要!)


上面是分治算法的步驟,也就是說任何用分治思想實現的各種算法都可以用上面3步分解出來看。

我們下面以歸併排序看一下,就結合上面3步,歸併排序我們分成3步:

1.分解:將n元素的數組分成n/2個元素的兩個子序列。

2.解決:將這些子序列再分解成更小規模的序列,遞歸地排序兩個子序列。

3.合併:合併這兩個已排好序的子序列生成最終答案。


文字說的差不多了,下面show code!!

<span style="font-size:18px;">/*
 * mergeSort.cpp
 *
 *  Created on: Dec 4, 2015
 *      Author: freestyle4568
 */

#include <iostream>
#include <vector>

using namespace std;

void print(vector<int> &A)
{
	for (size_t i = 0; i < A.size(); i++)
		cout << A[i] << " ";
	cout << endl;
}

void merge(vector<int> &A, int p, int q, int r)
{
	vector<int>::iterator iter = A.begin();
	vector<int> L(iter+p, iter+q+1);
	vector<int> R(iter+q+1, iter+r+1);
	size_t i = 0, j = 0;
	while (i < L.size() && j < R.size()) {
		if (L[i] < R[j])
			A[p++] = L[i++];
		else
			A[p++] = R[j++];
	}

	if (i == L.size()) {
		while (j < R.size())
			A[p++] = R[j++];
	} else {
		while (i < L.size())
			A[p++] = L[i++];
	}
}

void mergeSort(vector<int> &A, int p, int r)
{
	if (p < r) {
		int q = (p + r) / 2;
		cout << "mergeSort " << p << " " << q << endl;
		mergeSort(A, p, q);
		cout << "mergeSort " << q+1 << " " << r << endl;
		mergeSort(A, q+1, r);
        cout << "merge from " << p << " to " << r << endl;
		merge(A, p, q, r);
	}
}

int main()
{
	size_t n = 0;
	cout << "input the numbers of arrays: ";
	cin >> n;
	vector<int> A(n, 0);
	for (size_t i = 0; i < n; i++) {
		cin >> A[i];
	}
	//print(A);

	mergeSort(A, 0, n-1);

	print(A);

	return 0;
}
</span>


merge是合併的過程。

mergeSort是分解和解決的過程。

這裏和算法導論上面實現的略微不一樣。導論上面用到了哨兵元素,即比實際數據大很多的元素,這樣帶來的好處是不用判斷兩個數組有沒有到頭了。可以直接一個for循環結束,判斷條件就是將較小者放入原數組中。我覺得merge代碼看起來不是很費力氣,但是遞歸的過程我希望大家能做的心中有數,這個很重要,就是原數組的哪個部分先merge,哪個部分再merge。理解這個mergeSort過程對我們掌握遞歸思想很有幫助。我特地在mergeSort之前加了print內容,從輸出結果可以一目瞭然遞歸過程。















可以結合上圖看看遞歸的過程,下面我們分析一下歸併排序的複雜度。

還是先回歸分治算法,我們來總的看一下分治算法的一般複雜性解決思路。

設解決規模爲n的問題的時間爲T(n), 將原問題分解爲a個子問題,每個子問題的規模是原來的1/b倍。注意a和b不一定相等哦!然後可以得到下面遞歸方程表現:

T(n) = aT(n/b) + D(n) +C(n)。

其中D(n)表示分解子問題的時間,這個大多是常數時間

C(n)表示合併子問題的時間,這個很容易看出。


具體如何計算上面的遞歸方程呢?其實有多種方式,這裏介紹兩種,一種是遞歸數方式,一種是主方法。當然在數學上有更多的方法求解,感興趣的同學可以試着用不同的方法去解解看大笑

在上面的歸併排序中,a = b = 2;D(n)是一個常數O(1),可以忽略,C(n)是O(n)。

所以T(n) = 2T(n/2) + n;

合併n/2規模的子問題到最終答案時,用掉n時間。

形成n/2子問題的解用掉n/2時間。

形成n/4子問題的解用掉n/4時間。

..............

我們可以用數形表示:(由於沒有找到a=b=2的,勉強用a=b=3湊合把,原理都是一樣的)

















一共有log3^n列,所以複雜度爲O(nlog3^n)。

如果a = b = 2呢,O(nlog2^n)。

如果a = b = x呢,O(nlogx^n)。

所以我們可以發現,不管歸併排序分成多少子問題,複雜度都是一樣的,爲O(nlogn)。

下面總結一下:

歸併排序的時間複雜度爲:O(nlogn)。


有人說能不能用更簡單的方法一下子看出分治算法的複雜度啊!下面我們來介紹主方法:

主方法爲我們提供菜譜式的求解方法求解遞歸式:

T(n) = aT(n/b) + f(n)

定理:令a >= 1和b > 1是常數,f(n)是一個函數,T(n)是定義在非負整數上的遞歸式,如上式。那麼T(n)有如下漸進界:

1 )若f(n) = O(n^logb^a), 則T(n) = Θ(n^logb^a);

2 )若f(n) = Θ(n^logb^a), 則T(n) =Θ(n^logb^a * logn);

3 )若f(n) = Ω(n^logb^a), 且對於某個常數c<1和所有足夠大的n有af(n/b) <= cf(n),則T(n) =Θ(f(n))

下面解釋一下O,Θ, Ω的關係:

f(n) = O(g(n))表示 存在f(n) <= c*g(n), c爲常數。

f(n) = Ω(g(n))表示 存在f(n) >= c*g(n).

f(n) = Θ(g(n))表示 存在a*g(n) <= f(n) <= b*g(n).


有個主定理我們在看歸併排序,然後一目瞭然,f(n) = Θ(n^2),所以T(n) = Θ(nlogn)。


如果不滿足主定理的話,我們還是老老實實的用遞歸數做比較好!!!大笑


今天分治算法就講到這裏,以後會結合有趣的算法題來用用分治算法的。


發佈了65 篇原創文章 · 獲贊 75 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章