原地歸併演算法(空間複雜度為O(1)的歸併排序)
歸併排序演算法(mergesort)是將一個序列劃分為同樣大小的兩個子序列,然後對兩個子序列分別進行排序,最後進行合併操作,將兩個子序列合成有序的序列.在合成的過程中,一般的實現都需要開闢一塊與原序列大小相同的空間,以進行合併操作,歸併排序演算法的示例在這裡.
原地歸併排序的時間複雜度為O(nlog2n),空間複雜度為O(logn),相對於傳統的非原地歸併排序(時間複雜度O(nlogn),空間複雜度O(n))而言,十分節約記憶體,但排序速度稍慢。在記憶體緊張且需要排序穩定的場合,原地穩定排序可以發揮其特長。
在介紹原地穩定排序的原理之前,需要先了解兩個基本演算法,旋轉和二分查詢。
a) 旋轉
旋轉又稱迴圈移動,假設有這樣一個序列:e0, e1, …, ei-1, ei, ei+1, …, en-1, en。現在我們需要把它向左迴圈移動i個位置變成:ei, ei+1, …, en-1, en, e0, e1, …, ei-1。為了儘可能的節約記憶體和保證較快的速度,我們可以在時間複雜度O(n),空間複雜度O(1)的情況下達到目的。一種解決方案如下:
把原始序列看成兩個子序列:e0, e1, …, ei-1和ei, ei+1, …, en-1, en
把這兩個子序列分別逆序得:ei-1, …, e1, e0和en, en-1, …, ei+1, ei
也就是得到了這樣一個序列:ei-1, …, e1, e0, en, en-1, …, ei+1, ei
再把上面的序列整體逆序得:ei, ei+1, …, en-1, en, e0, e1, …, ei-1
以上旋轉過程的時間複雜度為O(n/2) + O(n/2) + O(n) = O(2n) = O(n),逆序時僅需要一個元素的輔助空間,空間複雜度O(1)。
這裡介紹一種不需要開闢新的空間就可以進行歸併操作的演算法.演算法的核心部分是以下程式碼:
1 /**
2 * 演算法: 合併二已排序的連續序列
3 **/ 4 template<typename T> 5 void t_merge( T& v, size_t size, size_t pos )
6 {
7 size_t fir
8 while ( fir < sec && sec < size )
9 {
10 while ( fir < sec && v[fir] <= v[sec] ) fir++;
11 size_t maxMove =0;
12 while ( sec < size && v[fir] > v[sec] ) maxMove++, sec++;
13 t_exchange( &v[fir], sec - fir, sec - fir - maxMove );
14 fir += maxMove;
15 }
16 }
其中T是一個數組, size是陣列尺寸, pos是歸併劃分的位置.也就是說[0,pos)和[pos, size)都分別是有序的.比如對序列1, 3, 5, 7, 2, 4, 6, 8進行歸併操作, 此時size=8, pos = 4.
以<<演算法導論>>中介紹的通過迴圈不變數的方法證明演算法的正確性,在這個演算法中, 迴圈不變數為[fir, sec)中的元素都是有序的:
1) 初始:此時fir = 0, sec = pos, 以前面介紹的函式引數的說明來看,滿足迴圈不變數.
2) 迭代:來看看迴圈做了些什麼操作.行10進行的操作為, 只要滿足fir元素不大於sec元素, fir就一直遞增;行12進行的操作是隻要滿足fir大於sec, sec就一直遞增, 同時遞增maxMove計數.因此, 進行完前面兩個步驟之後, fir所指元素一定小於sec以及其後的所有元素.而位於sec之前的第二個子序列中的元素, 一定小於fir.因此, [sec-maxMove, sec)z中的元素小於所有[fir, sec - 1)的元素.通過呼叫t_exchange函式將位於[sec-maxMove, sec)中的元素"旋轉"到fir之前.
也就是說, 這個過程在第二個已經排序好的子序列中尋找在它之內的小於目前第一個已經排序好的子序列的序列, 將它"旋轉"到前面.
以序列 1, 3, 5, 7, 2, 4, 6, 8為例, 此時fir=1也就是指向3, sec=5也就是指向4, maxMove=1, 通過呼叫t_exchange函式之後將[sec-maxMove, sec)即[4,5)中的元素也就是2"旋轉"到子序列3,5,7之前,於是該迴圈結束之後序列變為1,2,3,5,7,4,6,8, 此時fir=2, sec =5, 滿足迴圈不變數.
3) 終止: 當迴圈終止時, fir或者sec之一超過了陣列的尺寸, 顯而易見, 此時序列變成了有序的.
完整的演算法如下所示, 需要特別說明的是, 這段程式碼不是我想出來的, 原作者在這裡:
#include <stdio.h>
#include <iostream>usingnamespace std;
//int array[] = {1, 3, 5, 7, 2, 4, 6, 8};int array[] = {3,5,7,8,1,2,4,6};
void display(int array[], int n)
{
for (int i =0; i < n; ++i)
{
printf("%d ", array[i]);
}
printf("\n");
}
/**
* 演算法: 交換二物件
**/
template<typename T>void t_swap( T& v1, T& v2 )
{
T t = v1; v1 = v2; v2 = t;
}
/**
* 演算法: 反轉序列
**/
template<typename T>void t_reverse( T* v, size_t size )
{
size_t s =0, e = size-1;
while( s < e && s < size && e >0 )
t_swap( v[s++], v[e--] );
}
/**
* 演算法: 手搖演算法,從指定位置旋轉序列(見程式設計珠璣第二章)
**/
template<typename T>void t_exchange( T* v, size_t size, size_t n )
{
t_reverse( v, n );
t_reverse( v + n, size - n );
t_reverse( v, size );
}
/**
* 演算法: 合併二已排序的連續序列
**/
template<typename T>void t_merge( T& v, size_t size, size_t pos )
{
size_t fir =0, sec = pos;
while ( fir < sec && sec < size )
{
while ( fir < sec && v[fir] <= v[sec] ) fir++;
size_t maxMove =0;
while ( sec < size && v[fir] > v[sec] ) maxMove++, sec++;
t_exchange( &v[fir], sec - fir, sec - fir - maxMove );
fir += maxMove;
display(array, sizeof(array)/sizeof(int));
}
}
/**
* 演算法: 歸併排序
**/
template<typename T>void t_merge_sort( T* v, size_t size )
{
if ( size <=1 ) return;
t_merge_sort( v, size/2 );
t_merge_sort( v + size/2, size - size/2 );
t_merge( v, size, size/2 );
}
int main()
{
display(array, sizeof(array)/sizeof(int));
t_merge(array, sizeof(array) /sizeof(int), (sizeof(array)/sizeof(int))/2);
//t_merge_sort(array, sizeof(array) / sizeof(int));
display(array, sizeof(array)/sizeof(int));
return0;
}
補充說明:
其實前面採用"旋轉"演算法將元素前移不是必須的, 可以將所要移動的元素之前的部分後移, 再將元素插入到合適的位置.比如前面說的對於序列1, 3, 5, 7, 2, 4, 6, 8, 第一步要將元素2前移至3之前, 可以將3,5,7後移, 然後將2插入到合適的位置.
但是這樣有一個問題, 如果要移動的元素多了, 那麼就需要更多的臨時空間儲存要前移的元素, 這樣對空間就不是O(1)的了.而旋轉演算法可以做到O(1)的空間達到要求.