这是一个算法和工作代码。它就位,O(N),概念上很简单。
- 穿过阵列的前半部分,将项目交换到位。
- 从左半部分开始的项目将被交换到右半部分
在我们需要它们之前,所以我们使用一个技巧来确定它们在哪里
被换成了。
- 当我们到达中点时,整理已交换到右侧的未放置的左侧项目。
- 对剩余的半个数组重复。
这会通过阵列进行不超过 N+N/2 次交换,并且不需要临时存储。
诀窍是找到交换项目的索引。左边的项目在放置时被交换到由右边的项目腾出的交换空间中。交换空间按以下顺序增长:
- 在末尾添加一个项目(进入由右项目腾出的空间)
- 将一个项目与最旧的现有(左)项目交换。
按顺序添加项目 1..N 给出:
1 2 23 43 435 465 4657 ...
每一步改变的索引为:
0 0 1 0 2 1 3 ...
这个序列正好是OEIS A025480,可以在O(1)的摊销时间内计算出来:
def next_index(n):
while n&1: n=n>>1
return n>>1
一旦我们在交换 N 个项目后到达中点,我们就需要解读。交换空间将包含 N/2 个项目,其中应位于偏移量 i 的项目的实际索引由 next_index(N/2+i) 给出。我们可以通过交换空间前进,将物品放回原处。唯一的复杂之处在于,随着我们的前进,我们最终可能会找到一个位于目标索引左侧的源索引,因此已经在其他地方进行了交换。但是我们可以通过再次查找之前的索引来找出它在哪里。
def unscramble(start,i):
j = next_index(start+i)
while j<i: j = next_index(start+j)
return j
请注意,这只是索引计算,而不是数据移动。在实践中,对next_index 的调用总数对于所有 N 来说
这就是完整实现所需的全部内容:
def interleave(a, idx=0):
if (len(a)<2): return
midpt = len(a)//2
# the following line makes this an out-shuffle.
# add a `not` to make an in-shuffle
base = 1 if idx&1==0 else 0
for i in range(base,midpt):
j=next_index(i-base)
swap(a,i,midpt+j)
for i in range(larger_half(midpt)-1):
j = unscramble( (midpt-base)//2, i);
if (i!=j):
swap(a, midpt+i, midpt+j)
interleave(a[midpt:], idx+midpt)
最后的尾递归可以很容易地被循环替换。 Python 的数组语法就不那么优雅了。另请注意,对于此递归版本,输入必须是 numpy 数组而不是 python 列表,因为标准列表切片会创建未传播备份的索引副本。
这是一个验证正确性的快速测试。 (一副 52 张牌的 8 次完美洗牌将其恢复到原始顺序)。
A = numpy.arange(52)
B = A.copy()
C =numpy.empty(52)
for _ in range(8):
#manual interleave
C[0::2]=numpy.array(A[:26])
C[1::2]=numpy.array(A[26:])
#our interleave
interleave(A)
print(A)
assert(numpy.array_equal(A,C))
assert(numpy.array_equal(A, B))