用于交织数组的就地算法


62

给你一个元素的数组2ñ

一种1个一种2一种ñb1个b2bñ

任务是使用就地算法对数组进行交织,以使生成的数组看起来像

b1个一种1个b2一种2bñ一种ñ

如果就地需求不存在,我们可以轻松地创建一个新数组并复制给出On )的元素Øñ时间算法的。

根据就地需求,分而治之算法将算法提高为θñ日志ñ

所以问题是:

是否有一个时间算法,它也就位?Øñ

(注意:您可以假设使用统一成本的WORD RAM模型,因此就地转换为空间限制)。Ø1个


1
这是在stackoverflow上的, 但是他们没有提供优质的解决方案。最受好评的答案是:“这个问题并不像人们想象的那么简单。家庭作业?大声笑。arXiv 有一个解决方案 ”,但是arxiv解决方案需要一些数论+其他论文中的证明。在这里有一个简洁的解决方案将是很好的。
2012年


关于堆栈溢出的另一个线程:stackoverflow.com/questions/15996288/…–
Nayuki

Answers:


43

这是乔(Joe)链接的论文中详细阐述算法的答案:http : //arxiv.org/abs/0805.1598

首先让我们考虑使用分而治之的Θñ日志ñ算法。

1)分而治之

我们被给予

一种1个一种2b1个b2bñ

现在使用分治法,对于某些=Θñ,我们尝试获得数组

[一种1个一种2一种b1个b2b][一种+1个一种ñb+1个bñ]

然后递归

请注意,部分

b1个b2b一种+1个一种ñ
是的循环移位

一种+1个一种ñb1个b

地方。

这是经典之举,可以通过三个逆转并在Øñ时间内就地完成。

因此,分而治之为您提供了Θñ日志ñ算法,其递归类似于Ťñ=2Ťñ/2+Θñ

2)排列周期

现在,解决该问题的另一种方法是将置换视为一组不相交的循环。

排列方式为(假设从1个开始)

Ĵ2Ĵ2ñ+1个

如果我们以一定的额外空间确切地知道了周期是什么,我们可以通过选择一个元素一种,确定该元素去向何处(使用上述公式),将该元素放在目标位置的临时空间中,元素一种移到该目标位置并继续执行该循环。一旦完成一个循环,我们便进入下一个循环的某个元素,并遵循该循环,依此类推。

这将给我们一个Øñ时间算法,但是它假设我们“以某种方式知道确切的周期是什么”,并试图在O1 内进行簿记。Ø1个空间限制是使这个问题很难解决的原因。

这就是本文使用数论的地方。

可以示出的是,在情况下,当2ñ+1个=3ķ,在位置上的元件1个3323ķ-1个是在不同的周期和每个循环包含在该位置处的元件30

这利用了2Z / 3 k 的生成器的事实ž/3ķ

因此,当2ñ+1个=3ķ,遵循周期方法为我们提供了Øñ时间算法,对于每个周期,我们确切地知道从哪里开始:3幂(包括1个)(可以在下式中计算)Ø1个空间)。

3)最终算法

现在,我们结合以上两个:除法和征服+置换循环。

我们做了分而治之,但挑,使得2+1个是的功率3=Θñ

因此,取而代之的是对两个“一半”进行递归,而仅对一个递归进行Θñ额外的工作。

这给我们递归Ťñ=ŤCñ+Θñ(对于某些0<C<1个),从而给我们一个Øñ时间,即Ø1个空间算法!


4
那个好漂亮。
拉斐尔

1
非常好。通过排列示例,我现在大部分理解了。两个问题:1.如何实际找到值m?Paper声称需要O(log n),为什么呢?2.是否可以使用类似方法对阵列进行解交织?
num3ric

2
@ num3ric:1)你找到的最高功率这是< ñ。因此它将是O log n 。2)。是的,有可能,我相信我在某个地方对stackoverflow添加了一个答案。在这种情况下,我相信循环领袖出来是对的2 3 b(为2 + 1 =功率的3)。3<ñØ日志ñ2一种3b2+1个3
Aryabhata

@Aryabhata为什么我们只递归一个“一半”,而不是两个“一半”?
–sinoTrinity

1
@Aryabhata可以将此算法扩展为交错两个以上数组吗?例如,将变成c 1b 1a 1c 2b 2a 2一种1个一种2一种ñb1个b2bñC1个C2Cñ或类似的东西。C1个b1个一种1个C2b2一种2Cñbñ一种ñ
Doub '18年

18

我很确定我找到了一种不依赖于数论或循环论的算法。请注意,有一些细节需要解决(可能在明天),但是我很相信它们会解决。我应该在睡觉的时候挥手,不是因为我试图隐藏问题:)

为了简单起见A,让第一个数组B为第二个数组,|A| = |B| = NN=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。因此,我们可以设计一种算法,告诉我们下一个最小的数字是什么:该机制几乎完全是二进制数字的工作方式。您需要为数组的后半部分,第二个季度等,等等。

日志ñ日志ñØ1个时间摊销。

ØñØñ

ØñØ日志ñØ1个

现在的问题是:我们需要排序的零件中是否有某种样式?尝试使用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

有趣。您愿意尝试写一份正式证明吗?我确实知道,还有另一种处理位的算法(在Joe找到的论文中提到)。也许您已经重新发现了它!
Aryabhata 2012年

1

这是线性时间算法中的一种非递归就地插入阵列的两半,无需额外存储。

总体思路很简单:从左到右遍历数组的前半部分,将正确的值交换到适当的位置。随着您的前进,尚未使用的左侧值将交换到右侧值所腾出的空间中。唯一的技巧是弄清楚如何再次将它们拔出。

我们从大小为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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.