• 全部
  • 问答

上海交大算法版的一个O(1)空间merge sort算法,大家看看有bug吗??

azhen 2008-04-07 06:24:16
最开始是有人发了一个帖子,内容如下:
设子数组a[0:k]和a[k+1:n-1]已排序好(0<=k<=n-1).试设计一个合并这两个 子数组为排序
的数组a[0:n-1]的算法。要求算法在最坏情况下所用时间为O (n),且只用到O(1)的辅助空
间。
我晓得这是程序设计艺术上面的,但还是没有满意的答案。
请给个稍微详细的答案,能用普通人智商看懂的那种。
谢谢
##################################################
然后有一位仁兄回帖,思路很牛,但我觉得有bug,你们觉得,怎么修改这个bug呢??
代码如下:
void main()
{
int i = 0, min1 = 0, min2 = k+1;

while (a[i] <= a[min2]) i++;

Swap(a, i, min2);
min1 = min2++;

for (i = i+1; i<n; ++i)
{
if (a[min1] <= a[min2])
{
Swap(a, i, min1);
min1 = min1 == min2 - 1 ?
(i <= k ? k+1 : i+1) : min1+1;
}
else
{
Swap(a, i, min2);
min1 = i == min1 ? min2 : min1;
min2 = min2 + 1;
}
}
printA();
}
...全文
493 点赞 收藏 13
写回复
13 条回复
切换为时间正序
当前发帖距今超过3年,不再开放新的回复
发表回复
springtty 2008-10-16
mark
回复
azhen 2008-04-09
这个
相当相当佩服Vtin
我在网上查inplace sort,知道taocp上有解答
后来翻了下它的庞大解答
就没耐心看下去
没想到今天
先佩服下
................
inplace sort好像是有三种方法,目前
但是都很复杂

有简单一点的算法吗?
回复
Vitin 2008-04-09
查阅了 TAOCP,是 P168,V3 的练习 5.2.4-18,一道难度[40]的题。答案在 P648,学习一下。
TAOCP 提供的答案是1969年的方案,最新的进展则不包含在内。这里简要介绍一下算法过程。

设序列R[1:N]中,子列1--R[1:M]和子列2--R[M+1:N-1]已排序,对整个序列作归并排序,要求时间复杂度O(N),空间复杂度O(1)。
(这里采用TAOCP上的标识,以便与答案一致,方便介绍)

算法分以下几个步骤:
1、令n为N的平方根的近似整数(如,对N的平方根作floor取整),则可以将R[1:N]分为m+2个区(zones),记为Z(1)、Z(2)...,Z(m+2),其中Z(m+2)包含 N mod n 个元素,其他均包含n个元素。然后,将包含R[M]的某Z与Z[m+1]交换,并将Z(m+1)和Z(m+2)合并成一个区域,记为A(Auxiliary area)。设A包含s个元素,则n<=s<2n.
举例:如 N=103,M=37 。则可取 n=10,m=9,将R[1:103]分为11个区,其中Z(11)为末尾的3个元素(即R[101:103]),其他每个Z均为10个元素。然后,将Z(4)与Z(10)交换(因为Z(4)为R[31:40],包含子列1的最后一个元素R[37])。A指的是R[91:103],它的长度s=13,满足10<=s<20.
这一步的时间复杂度为O(n) < O(N).
注意:这一步完成后,Z(1)...Z(m)都是已排序的,并且每个Z都是子列1或子列2的一个连续子列。

2、考虑Z(1)...Z(m),按照每个Z的第一个元素作排序(如相等则再比较Z的最后一个元素)。具体方法为:找出R[1]、R[n+1]、...、R[(m-1)n+1]的最小值,如为R[(k-1)n+1],则将Z(1)和Z(k)作交换。然后在Z(2)...Z(m)中重复,直到排序完成。此时R[1]<=R[n+1]<=...<=R[(m-1)n+1].
每一次的时间复杂度为O(m+n),所以这一步的整体时间复杂度为O(m(m+n))=O(N).
注意:这一步完成后,对R[1:mn]的每个元素而言,都只有小于n个倒置(inversions),即对任意R[k]而言,R[1:k-1]中大于R[k]的元素数目小于n.
简单证明如下:设R[k]属于Z(t)(易知t = (k+n-1)/n),由于Z(1)、Z(2)、...、Z(t-1)、Z(t)或者属于子列1,或者属于子列2.不失一般性,设Z(t)属于子列1.现在将Z(1)、Z(2)、...、Z(t-1)分成两组:属于子列1的一组中所有元素一定都不大于R[k](因为它们的首元素都小于Z(t)的首元素,并且子列1是已排序的);而属于子列2的一组中,除了首元素最大的Z'外,其他Z的元素也都不大于R[k](因为它们都不大于Z'的首元素,而后者不大于Z(t)的首元素)。此外,Z(t)中排在R[k]前的元素也都不大于R[k]。因而大于R[k]的元素都在Z'中(除首元素之外),最多为n-1个。证毕。

3、对Z(1)和Z(2)利用A的空间作归并排序。具体方法为:首先交换Z(1)与A的前n个元素A'.然后对Z(2)和A'作常规的归并排序并输出到Z(1)Z(2)。此处TAOCP举了一个例子,看一下就理解了:
例:设n=3并且x1<y2<x2<y2<x3<y3,排序过程如下:
Z(1) Z(2) A'
初始状态: x1 x2 x3 y1 y2 y3 a1 a2 a3
交换Z(1): a1 a2 a3 y1 y2 y3 x1 x2 x3
交换x1: x1 a2 a3 y1 y2 y3 a1 x2 x3
交换y1: x1 y1 a3 a2 y2 y3 a1 x2 x3
交换x2: x1 y1 x2 a2 y2 y3 a1 a3 x3
交换y2: x1 y1 x2 y2 a2 y3 a1 a3 x3
交换x3: x1 y1 x2 y2 x3 y3 a1 a3 a2
当A'中的元素全部交换完毕后,排序完成,此时A'中的元素顺序可以和原来不同。
使用同样的方法排序Z(2)和Z(3)、Z(3)和Z(4)、...、Z(m-1)和Z(m)。
每一次的时间复杂度为O(n),所以这一步的整体时间复杂度为O(mn)=O(N).
注意:因为上一步所证明的性质,这一步完成后,R[1:mn]已完成排序。

4、对R[N+1-2s:N](即R中的末尾2s个元素,注意s为A的长度)作插入排序。
这一步的时间复杂度为O(s^2)=O(N).
注意:这一步完成后,R[1:N-2s]和R[N+1-2s:N]已分别排序,并且R[N+1-s:N](R中的末尾s个元素)为R中最大的s个元素。

5、使用第3步的方法对R[1:N-2s]和R[N+1-2s:N-s]作归并排序,使用R[N+1-s:N]作辅助空间。只不过此时是R[N+1-s:N]和R[N+1-2s:N-s]交换,并且从右往左(从大到小)作归并。
这一步的时间复杂度为O(N-s)=O(N)。
注意:这一步完成后R[1:N-s]已排序,R[N+1-s:N]为R中最大的s个元素(但次序已打乱)。

6、再次对R[N+1-2s:N]作插入排序。
这一步的时间复杂度为O(s^2)=O(N).
至此整个排序完成。由于上述1到6步的时间复杂度均不超过O(N),故整个算法的复杂度为O(N).

呼,写完了。TAOCP 果然很强大,单单理解答案就花了我不少时间。希望能够对大家有所启示,寻找到其他好的方案。
回复
jinwei1984 2008-04-09
mark!
回复
hoomey 2008-04-08
mark
回复
flyingscv 2008-04-08
我倒想出一个实现的算法了,实现和解释起来都挺麻烦的,改天给出代码
回复
Vitin 2008-04-08
仔细想了一下,tailzhou是对的。当出现这种情况时,原来i的值应该加到段III的末尾,而非段IV的末尾。这样当段III的长度不为0时就出错了。
修改方式:
1、交换段III与段IV,形成新的段IV,从而令段III长度为0;或者
2、将段IV所有元素后移一个位置,空出来给i;或者
3、将i插入到段V的合适位置。
但是无论是哪一种方案,算法的整体复杂度都超过O(n)了。

感谢tailzhou的指正,令我认识到错误。学习,呵呵。
回复
tailzhou 2008-04-08
[Quote=引用 5 楼 Vitin 的回复:]
只要考虑for循环时的程序不变量就可以了:
1、称子数组a[0:k]为子列1,子数组a[k+1:n-1]为子列2,则整个数组a[0:n-1]可分为五段:
段I:a[0:i-1],已排序子列,当i==>n时排序完成;
段II:a[i:k],子列1未移动的剩余元素,当i>k时,该段消失;
段III:a[x:min1-1],子列1中已移动的剩余元素的一部分,段III的所有元素均 <=段II的任意元素,其中x=max(k+1,i);
段IV:a[min1:min2-1],子列1中已移动的剩余元…
[/Quote]

我觉得当元素比较多的时候,如果不重新排列子列1的已移动的元素,是可能不止5个段的;

比如:
min2比min1小的时候,交换min2与i;
那么交换后的min2(即原来的i)记入哪个段?
1)如果是段IV,那么由于"原来的i"必定比段III的元素大,这样就违反了“段IV的所有元素均 <=段III的任意元素”约束;
2)如果是段V,那么如果"原来的i"比min2+1位置的元素大,必定违反"各段都是有序的"的约束;

但如果重新排列,又很难保证0(n);
回复
liangbch 2008-04-08
mark
回复
Vitin 2008-04-08
只要考虑for循环时的程序不变量就可以了:
1、称子数组a[0:k]为子列1,子数组a[k+1:n-1]为子列2,则整个数组a[0:n-1]可分为五段:
段I:a[0:i-1],已排序子列,当i==>n时排序完成;
段II:a[i:k],子列1未移动的剩余元素,当i>k时,该段消失;
段III:a[x:min1-1],子列1中已移动的剩余元素的一部分,段III的所有元素均<=段II的任意元素,其中x=max(k+1,i);
段IV:a[min1:min2-1],子列1中已移动的剩余元素的另一部分,段IV的所有元素均<=段III的任意元素;
段V:a[min2:n-1],子列2中的剩余元素。
各段都是有序的。
2、min1始终指向子列1剩余元素中的最小值,min2始终指向子列2剩余元素中的最小值,i始终指向已排序子列末尾的后一个值。
因此,每次比较min1和min2,将其中较小的与i交换即可。
3、当段IV长度减为0时,min1重定向到段III的起始位置,旧的段III成为新的段IV,新的段III长度为0。
4、当i>k时,段II消失,此后i指向段III(或段IV)的起始位置。

程序是有问题的,主要包括以下几点:
1、[性能问题,重要性低] 当 i==min1 且 a[min1]<=a[min2] 时,不需要交换。
修改方式:增加一个判断。不过这不太重要,特别是交换和比较的花费相差不大时。
2、[性能问题,重要性中] 当 i==min2 时,子列1已无剩余元素,排序已完成,循环可终止。
修改方式:循环条件 i<n 改为 i<min2 。不过即使不修改,程序也不会出错(虽然已不符合min1的含义),只是性能较低。
3、[逻辑问题,致命错误] 当 min2==n-1 且 a[min1]>a[min2] 时,交换后子列2已无剩余元素,此时增加min2会引发致命错误
  修改方式:出现这种情况后,将子列1剩余元素重新排序(将[段II,段III,段IV]的顺序交换为[段IV,段III,段II]),终止循环
总之程序的主要问题是终止处理,包括子列1先终止和子列2先终止。
回复
无病呻吟2 2008-04-08
mark
回复
oo 2008-04-08
mark
回复
barenx 2008-04-07

void main()
{
int i = 0, min1 = 0, min2 = k+1;

while (a[i] <= a[min2]) i++;

Swap(a, i, min2);
min1 = min2++;

for (i = i+1; i <n; ++i)
{
if (a[min1] <= a[min2])
{
Swap(a, i, min1);
min1 = min1 == min2 - 1 ?
(i <= k ? k+1 : i+1) : min1+1;
}
else
{
Swap(a, i, min2);
min1 = i == min1 ? min2 : min1;
min2 = min2 + 1;
}
}
printA();
}

先看下代码
回复
相关推荐
发帖
数据结构与算法
创建于2007-08-27

3.2w+

社区成员

数据结构与算法相关内容讨论专区
申请成为版主
帖子事件
创建了帖子
2008-04-07 06:24
社区公告
暂无公告