转:http://www.cppblog.com/converse/archive/2008/09/28/63008.html
归并排序算法(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
=
0
, sec
=
pos;
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
>
using
namespace
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
));
return
0
;
}
补充说明:
其实前面采用"旋转"算法将元素前移不是必须的, 可以将所要移动的元素之前的部分后移, 再将元素插入到合适的位置.比如前面说的对于序列1, 3, 5, 7, 2, 4, 6, 8, 第一步要将元素2前移至3之前, 可以将3,5,7后移, 然后将2插入到合适的位置.
但是这样有一个问题, 如果要移动的元素多了, 那么就需要更多的临时空间保存要前移的元素, 这样对空间就不是O(1)的了.而旋转算法可以做到O(1)的空间达到要求.