给你一个元素的数组
任务是使用就地算法对数组进行交织,以使生成的数组看起来像
如果就地需求不存在,我们可以轻松地创建一个新数组并复制给出O(n )的元素时间算法的。
根据就地需求,分而治之算法将算法提高为。
所以问题是:
是否有一个时间算法,它也就位?
(注意:您可以假设使用统一成本的WORD RAM模型,因此就地转换为空间限制)。
给你一个元素的数组
任务是使用就地算法对数组进行交织,以使生成的数组看起来像
如果就地需求不存在,我们可以轻松地创建一个新数组并复制给出O(n )的元素时间算法的。
根据就地需求,分而治之算法将算法提高为。
所以问题是:
是否有一个时间算法,它也就位?
(注意:您可以假设使用统一成本的WORD RAM模型,因此就地转换为空间限制)。
Answers:
这是乔(Joe)链接的论文中详细阐述算法的答案:http : //arxiv.org/abs/0805.1598
首先让我们考虑使用分而治之的算法。
1)分而治之
我们被给予
现在使用分治法,对于某些,我们尝试获得数组
然后递归
请注意,部分
按地方。
这是经典之举,可以通过三个逆转并在时间内就地完成。
因此,分而治之为您提供了算法,其递归类似于。
2)排列周期
现在,解决该问题的另一种方法是将置换视为一组不相交的循环。
排列方式为(假设从开始)
如果我们以一定的额外空间确切地知道了周期是什么,我们可以通过选择一个元素,确定该元素去向何处(使用上述公式),将该元素放在目标位置的临时空间中,元素移到该目标位置并继续执行该循环。一旦完成一个循环,我们便进入下一个循环的某个元素,并遵循该循环,依此类推。
这将给我们一个时间算法,但是它假设我们“以某种方式知道确切的周期是什么”,并试图在O(1 )内进行簿记。空间限制是使这个问题很难解决的原因。
这就是本文使用数论的地方。
可以示出的是,在情况下,当,在位置上的元件,是在不同的周期和每个循环包含在该位置处的元件。
这利用了是(Z / 3 k )∗的生成器的事实。
因此,当,遵循周期方法为我们提供了时间算法,对于每个周期,我们确切地知道从哪里开始:幂(包括)(可以在下式中计算)空间)。
3)最终算法
现在,我们结合以上两个:除法和征服+置换循环。
我们做了分而治之,但挑,使得是的功率和。
因此,取而代之的是对两个“一半”进行递归,而仅对一个递归进行额外的工作。
这给我们递归(对于某些),从而给我们一个时间,即空间算法!
我很确定我找到了一种不依赖于数论或循环论的算法。请注意,有一些细节需要解决(可能在明天),但是我很相信它们会解决。我应该在睡觉的时候挥手,不是因为我试图隐藏问题:)
为了简单起见A
,让第一个数组B
为第二个数组,|A| = |B| = N
并N=2^k
为某些数组假设k
。我们A[i..j]
是子阵列A
与指数i
通过对j
,包容性。数组基于0。让我们从右边算起,RightmostBitPos(i)
返回最右边的位(从0开始的位置),它是的'1' i
。该算法的工作原理如下。
GetIndex(i) {
int rightPos = RightmostBitPos(i) + 1;
return i >> rightPos;
}
Interleave(A, B, N) {
if (n == 1) {
swap(a[0], b[0]);
}
else {
for (i = 0; i < N; i++)
swap(A[i], B[GetIndex(i+1)]);
for (i = 1; i <= N/2; i*=2)
Interleave(B[0..i/2-1], B[i/2..i-1], i/2);
Interleave(B[0..N/2], B[N/2+1..N], n/2);
}
}
让我们用16个数字组成的数组,让我们开始使用交换对它们进行交织,看看会发生什么:
1 2 3 4 5 6 7 8 | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8 | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8 | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8 | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8 | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8 | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8 | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16
特别感兴趣的是第二个数组的第一部分:
|
| 1
| 2
| 2 3
| 4 3
| 4 3 5
| 4 6 5
| 4 6 5 7
| 8 6 5 7
模式应该很清楚:我们在末尾交替添加一个数字,然后用高数字替换最低数字。请注意,我们总是添加一个比已经拥有的最高数字高一个数字。如果我们能够以某种方式准确地找出在任何给定时间哪个数字最低,那么我们可以轻松地做到这一点。
现在,我们来看一些更大的示例,看看是否可以看到一个模式。请注意,我们无需固定数组的大小即可构建上述示例。在某个时候,我们得到了这种配置(第二行从每个数字中减去16):
16 24 20 28 18 22 26 30 17 19 21 23 25 27 29 31
0 8 4 12 2 6 10 14 1 3 5 7 9 11 13 15
现在,这清楚地显示了一个模式:“ 1 3 5 7 9 11 13 15”全部分开2,“ 2 6 10 14”全部分开4,“ 4 12”分开8。因此,我们可以设计一种算法,告诉我们下一个最小的数字是什么:该机制几乎完全是二进制数字的工作方式。您需要为数组的后半部分,第二个季度等,等等。
时间摊销。
。
现在的问题是:我们需要排序的零件中是否有某种样式?尝试使用32个数字可解决“ 16 12 10 14 9 11 13 15”问题。请注意,这里的模式完全相同!“ 9 11 13 15”,“ 10 14”和“ 12”以我们之前看到的相同方式分组在一起。
现在,诀窍是递归插入这些子部分。我们将“ 16”和“ 12”插入“ 12 16”。我们将“ 12 16”和“ 10 14”交织到“ 10 12 14 16”。我们将“ 10 12 14 16”和“ 9 11 13 15”交织到“ 9 10 11 12 13 14 15 16”。这是对第一部分的排序。
一个例子:
Interleave the first half:
1 2 3 4 5 6 7 8 | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8 | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8 | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8 | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8 | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8 | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8 | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16
Sort out the first part of the second array (recursion not explicit):
8 6 5 7 13 14 15 16
6 8 5 7 13 14 15 16
5 8 6 7 13 14 15 16
5 6 8 7 13 14 15 16
5 6 7 8 13 14 15 16
Interleave again:
5 6 7 8 | 13 14 15 16
13 6 7 8 | 5 14 15 16
13 5 7 8 | 6 14 15 16
13 5 14 8 | 6 7 15 16
13 5 14 6 | 8 7 15 16
Sort out the first part of the second array:
8 7 15 16
7 8 15 16
Interleave again:
7 8 | 15 16
15 8 | 7 16
15 7 | 8 16
Interleave again:
8 16
16 8
Merge all the above:
9 1 10 2 11 3 12 4 | 13 5 14 6 | 15 7 | 16 8
这是线性时间算法中的一种非递归就地插入阵列的两半,无需额外存储。
总体思路很简单:从左到右遍历数组的前半部分,将正确的值交换到适当的位置。随着您的前进,尚未使用的左侧值将交换到右侧值所腾出的空间中。唯一的技巧是弄清楚如何再次将它们拔出。
我们从大小为N的数组开始,将其分为2个几乎相等的一半。
[ left_items | right_items ]
当我们处理它时,它变成
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]
交换空间按以下方式增长:A)通过删除相邻的右侧项目并从左侧交换新项目来扩大空间;B)将最旧的项目与左侧的新项目交换。如果左侧的项目编号为1..N,则该模式如下
step swapspace index changed
1 A: 1 0
2 B: 2 0
3 A: 2 3 1
4 B: 4 3 0
5 A: 4 3 5 2
6 B: 4 6 5 1
7 A: 4 6 5 7 3
...
索引更改的顺序完全是OEIS A025480,可以通过简单的过程进行计算。这样,仅给定到目前为止添加的项目数,就可以找到交换位置,这也是放置的当前项目的索引。
这就是我们需要在线性时间内填充序列的前一半的所有信息。
当我们到达中点时,数组将分为三个部分:
[ placed_items | swapped_left_items | remaining_right_items]
如果我们可以对交换的项目进行加扰,则可以将问题减少到一半,并且可以重复。
为了解释交换空间,我们使用以下属性:通过N
交替执行append和swap_oldest操作构建的序列将包含N/2
年龄由给出的项目A025480(N/2)..A025480(N-1)
。 (整数除法,较小的值较旧)。
例如,如果左半部分最初保留值1..19,则交换空间将包含[16, 12, 10, 14, 18, 11, 13, 15, 17, 19]
。A025480(9..18)是[2, 5, 1, 6, 3, 7, 0, 8, 4, 9]
,这正是从最早到最新的项目的索引列表。
因此,我们可以通过前进并S[i]
与交换空间来对交换空间进行整理S[ A(N/2 + i)]
。这也是线性时间。
剩下的麻烦是,最终您将到达一个正确的值应位于较低索引处的位置,但该位置已被换出。很容易找到新位置:只需再次进行索引计算即可发现项目被交换到的位置。可能有必要按照以下步骤进行操作,直到找到未交换的位置。
至此,我们已经合并了一半的数组,并通过完全N/2 + N/4
交换保持了另一半中未合并部分的顺序。我们可以继续进行阵列的其余部分,以进行总N + N/4 + N/8 + ....
交换次数(严格小于) 3N/2
。
如何计算A025480:
OEIS中将a(2n) = n, a(2n+1) = a(n).
其定义为a(n) = isEven(n)? n/2 : a((n-1)/2)
。这导致使用位运算的简单算法:
index_t a025480(index_t n){
while (n&1) n=n>>1;
return n>>1;
}
这是对N的所有可能值的摊销O(1)运算(1/2需要1个移位,1/4需要2,1/8需要3,...)。还有一种更快的方法,它使用小的查找表来查找最低有效零位的位置。
鉴于此,这是C中的实现:
static inline index_t larger_half(index_t sz) {return sz - (sz / 2); }
static inline bool is_even(index_t i) { return ((i & 1) ^ 1); }
index_t unshuffle_item(index_t j, index_t sz)
{
index_t i = j;
do {
i = a025480(sz / 2 + i);
}
while (i < j);
return i;
}
void interleave(value_t a[], index_t n_items)
{
index_t i = 0;
index_t midpt = larger_half(n_items);
while (i < n_items - 1) {
//for out-shuffle, the left item is at an even index
if (is_even(i)) { i++; }
index_t base = i;
//emplace left half.
for (; i < midpt; i++) {
index_t j = a025480(i - base);
SWAP(a + i, a + midpt + j);
}
//unscramble swapped items
index_t swap_ct = larger_half(i - base);
for (index_t j = 0; j + 1 < swap_ct ; j++) {
index_t k = unshuffle_item(j, i - base);
if (j != k) {
SWAP(a + midpt + j, a + midpt + k);
}
}
midpt += swap_ct;
}
}
这应该是一种非常易于缓存的算法,因为3个数据位置中的2个是顺序访问的,并且正在处理的数据量正在严格减少。通过在循环开始时否定测试,可以将该方法从混洗变为混洗is_even
。