原地歸併

問題I:

傳統歸併排序需要O(n)的空間發雜度,但是否能夠實現原地歸併排序呢?即O(1)的空間複雜度。時間複雜度還是否是O(nlogn)?


對於這個問題,網上有很多資料,講的比較清楚的有下面這個文章:

http://www.ahathinking.com/archives/103.html

在瞭解原地歸併的思想之前,先回憶一下一般的歸併算法,先是將有序子序列分別放入臨時數組,然後設置兩個指針依次從兩個子序列的開始尋找最小元素放入歸併數組中;那麼原地歸併的思想亦是如此,就是歸併時要保證指針i之前的數字始終是兩個子序列中最小的那些元素。文字敘述多了無用,見示例圖解,一看就明白。

假設我們現在有兩個有序子序列如圖a,進行原地合併的圖解示例如圖b開始


如圖b,首先第一個子序列的值與第二個子序列的第一個值20比較,如果序列一的值小於20,則指針i向後移,直到找到比20大的值,即指針i移動到30;經過b,我們知道指針i之前的值一定是兩個子序列中最小的塊

如圖c,先用一個臨時指針記錄j的位置,然後用第二個子序列的值與序列一i所指的值30比較,如果序列二的值小於30,則j後移,直到找到比30大的值,即j移動到55的下標;

如圖d,經過圖c的過程,我們知道數組塊 [index, j) 中的值一定是全部都小於指針i所指的值30,即數組塊 [index, j) 中的值全部小於數組塊 [i, index) 中的值,爲了滿足原地歸併的原則:始終保證指針i之前的元素爲兩個序列中最小的那些元素,即i之前爲已經歸併好的元素。我們交換這兩塊數組的內存塊,交換後i移動相應的步數,這個“步數”實際就是該步歸併好的數值個數,即數組塊[index, j)的個數。從而得到圖e如下:


重複上述的過程,如圖f,相當於圖b的過程,直到最後,這就是原地歸併的一種實現思想。

這裏A、B兩塊的交換,可以通過對A翻轉變爲A‘,對B翻轉變爲B’,再對A‘B'翻轉來求得。


上面的算法空間複雜度確實O(1),但時間複雜度就不再是O(nlogn)了,不知道是否有更好的原地快排?(注意:這裏指的是對數組歸併,不是鏈表)

此算法最壞的情況就是對[1,3,5,7,9,11]和 [2,4,6,8,10,12]類型的數據進行原地歸併,就需要O(n^2)的時間複雜度。


問題II:

輸入數組[a1,a2,...,an,b1,b2,...,bn],做最少的操作,使得輸出爲,[a1,b1,a2,b2,...,an,bn],注意:方法要是in-place的。


分析I:

這個問題正好是上面“原地歸併”的最壞的情況,可以用上面的方法求解,但時間複雜度是O(n^2),其實,也就變成了插入排序了:

a1,a2,...,an的相對位置是對的,不需要做插入排序,

對b1,將b1向前移動n-1個位置,即插入到正確位置。

對b2,將b2向前移動n-2個位置,即插入到正確位置。

依次對b3,..bn做相同的操作(插入排序),即可得到要求的輸出。

時間複雜度仍然是O(n^2)


分析II:

利用快排的思想:

假設區間由4段組成【A1,A2,B1,B2】。這裏:

A1,A2分別對應於原來a1,a2,...,an的前半段和後半段。

B1,B2分別對應於原來b1,b2,...,bn的前半段和後半段。

將A2、B1交換位置,變爲【A1,B1,A2,B2】。

這樣,就可將問題轉化爲對【A1,B1】和【A2,B2】分別處理的兩個子問題。

這樣遞歸地求解下去,就可in-place地得到所要求的輸出。

此算法的本質是快排,時間複雜度O(nlogn)。


分析III:

循環將元素放到最終的位置上,即

將a2放到a3的位置,將a3放到a5的位置,將a5放到a9的位置,這樣依次做下去,直到所有元素都放到最終位置。

但對有些數據不是一個circle就可以解決的,如對下圖A中的數據:

a2放到a3的位置,a3放到a5的位置,a5放到b4的位置,b4放到b3的位置,b3放到b1的位置,b1放到a2的位置,這是一個circle。經過這個circle的處理,序列變爲圖B所示,彩色元素均已交換過,即已經在最終位置了。但是還有部分元素沒有處理,還需要對剩下的元素做循環shift的處理。


但,問題是:如何判斷一個元素有沒有在最終位置上,其實就是如何判找出下一個circle的起始位置。

這就需要對每個元素做標記(用位存儲),需要O(n)的空間複雜度。

此算法的時間複雜度爲O(n),空間複雜度爲O(n)。


特殊情況:如果要處理的數據比較特殊,比如處理的是都大於零的數,那麼:

將每個放到最終位置的元素取反,這樣就可以通過元素的正負來判斷是否在最終位置。

當所有元素都在最終位置上時,所有元素都被取反了。

然後再將每個元素取反,變回原來的值,就得到所要求的輸出了,

時間複雜度爲O(n),空間複雜度僅爲O(1)。


分析IV:

在分析III中,由於需要找出下一個circle的起始位置,所以用了O(n)的空間複雜度。

其實,下一個circle的起始位置可以在常數時間內找出,不需要額外的存儲空間。

具體分析,可參考以下資料:

參考論文:A Simple In-Place Algorithm for In-Shuffle

參考資料:http://www.newsmth.net/bbscon.php?bid=1032&id=47005

稍有不同的是,論文中處理的數組下標從1開始,而且每個元素都會移動位置。

其實,只要如下圖所示:去掉兩端元素,僅處理中間元素;這樣就將問題轉化爲論文中的問題了。



算法的本質就要是運用數論的知識,找出規律:當2n=3^k-1時,每個circle的起始位置都爲3^i。

然後,對於任意數組,只需將其分成若干滿足2n=3^k-1的子數組,每個子數組單獨處理即可。

算法流程:


時間複雜度O(n),空間複雜度O(1)

代碼實現:

void rightShift(vector<int>::iterator it, int n, int m){
	reverse(it, it + (n - m));
	reverse(it + (n - m), it + n);
	reverse(it, it + n);
}
void circleShift(vector<int>::iterator it, int n){
	for(int i = 1; i <= n; i *= 3){
		int next = i * 2 % (2 * n + 1);
		int tmp = *(it + i - 1);
		while(true){
			swap(tmp, *(it + next -1));
			if(next == i)
				break;
			next = (next * 2) % (2 * n + 1);
		}
	}
}
void merger(vector<int>::iterator it, int n){
	int m = 0, tmp = 1;
	while(tmp * 3 - 1 <= 2 * n){
		m = (tmp * 3 - 1) / 2;
		tmp *= 3;
	}
	if(m == n){
		circleShift(it, n);
		return;
	}
	rightShift(it + m, n, m);
	circleShift(it, m);
	merger(it + 2 * m, n - m);
}


測試代碼:

int main() {
	int n = 7;
	vector<int> v(2*n);
	for(int i = 0; i < n; ++i)
		v[i] = 2 * i;
	for(int i = n; i < 2*n; ++i)
		v[i] = 2 * (i - n) + 1;
	merger(v.begin()+1, n -1);
	for(auto i : v)
		cout << i << " ";
	cout << endl;
	return 0;
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章