内存移动算法
个人笔记,转载请联系本人
本篇笔记也被放在我的github上,那里有详细的代码。
概述
算法课上老师介绍了内存移动算法。说实话,当时并没有听懂。原因是对这个没什么感触,体会不到关键点在哪里。
问题背景
如何高效的把一块内存上的数据循环移动k位?要求时间复杂度和空间复杂度尽可能小。
由于课上已经讲过右移的情形,这里只讨论左移。
在平常的编程中,循环移动不是个很难的问题。可以每次把所有的元素移动一位,总体移动k次就可以完成任务。但显然可以通过计算来避免多次移动这个重复操作。
问题分析
现在来做简要的分析。设待移动的序列为:
对于元素 ai ,向左移动k位后,位置在 i−k 。记做:
NewIndex(i,n,k)=i−k
考虑到越界情况,应该修正为:
NewIndex(i,n,k)=(i−k)modn
其中,设定取模运算的结果为正数。
这个结论是显而意见的,因为这个问题的移动本来就是循环移动。从而可以得到初步的移动思路:
move_right(col,n,k):
new_col=col.clone()
for i in [0,col.size()):
new_pos=NewIndex(i,n,k)
new_col[new_pos]=col[i]
return new_col
这个思路看起来挺合理的。但想想,一个内存移动的算法能允许O(n)的额外空间消耗吗?只怕是还需要改进一下才行。
在上面的算法中,没有利用好已有的空间,也就是col的空间。每一个col中的存储空间,在提供了元素值后,实际上是没有其他作用的。在这个元素的移动完成后,并没有其他作用,完全可以复用它的空间。如果限定只能使用常数个存储空间,就只有充分利用这个想法,钻钻空子。
考虑这个实例:
如果要求左移5位,可以得出这样的移动序列
0 -> 3 -> 6 -> 1 -> 4 -> 7 -> 2 -> 5 -> 0
可以看到,整个序列形成了循环。那么,是不是任意情况下,都可以形成一个循环呢?
对于元素 ai ,移动后位置是:
i -> (i-k) mod n
而元素 a(i−k)modn 移动后位置是:
(i-k) mod n -> ((i-k) mod n - k) mod n
为了方便理解,不妨再假设k是小于n的。即 k=kmodn 实际上,对于下面的推到,也并不失k大于n时的一般性。
现在,根据摸运算的规律(可以证明):
那么,
((i−k)modn−k)modn=((i−k)modn−kmodn)modn=(i−2k)modn
即,
i -> (i-k) mod n -> (i-2k) mod n
这就得到了:
i -> (i-k) mod n ->....-> (i-p*k) mod n
在p等于n时,明显开头结尾两项是重合的。也就形成了一个循环。
但这不表示,循环序列一定包含了所有的元素。也就是说,移动过程不一定由一个循环确定,还可能需要多个循环。在附上的代码 move-oder.cpp 中,就有这种情况。
对于几个循环可以确定一个移动序列,我确实没有办法。老师的解法确实不错,我想不出来。
设y=q*k/n,k=a*e,n=b*e,e是k和n的最大公约数,那么
kn=yq=ab
因为a b 互质,为确保循环序列的长度最小,q一定是最小的,那么
q=b
也就是说,q个循环可以照顾到每个元素。其中,q是n除以k和n的最大公约数的所得到。
算法描述
现在已经可以给出改进后的算法的位代码了。
move_left(col,n,k):
e=最大公约数(k,n)
for j in [0,e):
backup=col[i]
new_pos=NextIndex(j,n,k)
for i in [0,n/e):
temp=backup
backup=col[new_pos]
col[new_pos]=temp
new_pos=NextIndex(new_pos,n,k)
return col
额外消耗的空间复杂度为O(1)
时间复杂度
T(n)=∑n/eq=1(∑qi=04)
(不求了,但是根据伪代码,没有做多余的操作,每个内循环都是对应一个元素的移动哦,总体时间复杂度应该是O(n))
代码实现
见memory-copy.cpp