【问题标题】:Permute elements of array according to another one, without making a copy根据另一个排列数组的元素,而不复制
【发布时间】:2023-03-20 22:46:01
【问题描述】:

考虑一个数组。根据另一个提供元素新位置的数组来排列其元素的好方法是什么(不首先制作数组的副本)?

例如

int a[]={37,43,5,10}; //array to permute
int b[]={3,1,4,2};  //new position of each element

所以应该变成

{43,10,37,5}

我很自然地想到制作一个 a 的副本,然后将它的元素重新分配到新的位置。但是有没有办法在不复制数组 a 的情况下做到这一点(即更简单的方法)?

注意:如果可能的话,不应该使用特定的 C++ 头文件,而应该只使用 <iostream>

【问题讨论】:

  • 这似乎倒退了一个..
  • @RickAstley 你的想法正好相反——5 是第一个数组的第三个元素,然后是 37 是第一个,然后是 10 是第四个,43 是第二个。你基本上想要a[b - 1],如果在 C++ 中允许这样的符号。
  • @Holt 啊,我明白了,搞混了。

标签: c++ arrays algorithm permutation


【解决方案1】:

当您想避免复制数组时,通常意味着将自己限制为交换。

如果我们使用交换对b[] 进行排序,并对a[] 使用相同的交换,那么a[] 最终将根据b[] 的值进行排列。

我将逐步介绍下面的算法。为简单起见,我从 1 开始数组计数,尽管在 C 数组中从 0 开始计数。您必须在代码中进行调整。

a[]={37, 43, 5, 10} //array to permute
b[]={3, 1, 4, 2}  //new position of each element

i = 1; b[i] = 3
swap(a[1], a[3]); a[] = {5, 43, 37, 10}
swap(b[1], b[3]); b[] = {4, 1, 3, 2}

i = 1; b[i] = 4
swap(a[1], a[4]); a[] = {10, 43, 37, 5}
swap(b[1], b[4]); b[] = {2, 1, 3, 4}

i = 1; b[i] = 2
swap(a[1], a[2]); a[] = {43, 10, 37, 5}
swap(b[1], b[2]); b[] = {1, 2, 3, 4}

i = 1; b[i] = 1
++i

i = 2; b[i] = 2
++i

i = 3; b[i] = 3
++i

i = 4; b[i] = 4
++i

i = 5; i > 4
DONE

请注意我们如何在最后一步通过b[]。考虑b[]={2, 1, 4, 3}

a[] = {37, 43, 5, 10}
b[] = {2, 1, 4, 3}

i = 1; b[i] = 2
swap(a[1], a[2]); a[] = {43, 37, 5, 10}
swap(b[1], b[2]); b[] = {1, 2, 4, 3}

i = 1; b[i] = 1
++i

i = 2; b[i] = 2
++i

i = 3; b[i] = 4
swap(a[3], a[4]); a[] = {43, 37, 10, 5}
swap(b[3], b[4]); b[] = {1, 2, 3, 4}

i = 3; b[i] = 3
++i

i = 4; b[i] = 4
++i

i = 5; i > 4
DONE

每次交换时,数组中的一个元素都会出现在正确的位置,这意味着我们最多执行 N 次交换。

【讨论】:

    【解决方案2】:

    它可以在 O(n) 时间内完成,需要 O(1) 额外内存,通过一次处理一个置换数组的周期。

    注意:这种方法比此特定设置所需的要复杂得多(ab 都是 int 数组),但它有一些好处:

    • 它可以处理任意数据类型(例如置换数组a可以是字符串数组)。

    • 它可以保留排列数组b中的原始值。


    考虑最初的例子:

    int a[] = {37, 43, 5, 10};  // array to permute
    int b[] = { 3,  1, 4,  2};  // new position of each element
    

    数组b表示我们要进行如下的赋值链:

    a[1] <-- a[3] <-- a[4] <-- a[2] <-- a[1].

    问题是,在最后一次分配中,我们无法再访问a[1](它已经被替换为a[3])。

    但是,起始元素的原始值可以保存在一个辅助变量中,以便我们在关闭循环时使用它(保证当我们关闭循环时,我们会准确到达我们已经开始的元素from - 否则某些元素可以通过多种方式访问​​,ie 我们将有 b[i] = b[j]),对于某些 i != j)。


    一般来说,排列可能包含多个循环。处理完一个循环后,我们需要从一个尚未更新的元素开始(到目前为止它还不是处理过的循环的一部分)。

    因此,我们需要知道到目前为止哪些元素没有被处理。

    一种可能的方法是临时修改置换向量b 以跟踪更新了哪些元素,例如在更新元素时​​否定b 中相应位置的值在a。这样做的好处是,最后我们可以遍历b 的所有元素并恢复初始值(通过再次否定所有元素)。


    下面是之前想法的实现。

    int main() {
        int a[] = {11, 22, 33, 44};
        int b[] = { 2,  1,  4,  3};
        int aux, crtIdx, nxtIdx;
    
        int n = sizeof(a) / sizeof(a[0]);
    
        for (int i = 0; i < n; i++) {
            // check whether the i'th element
            // was already processed
            if (b[i] < 0) {
                continue;
            }
    
            // start processing of a new cycle;
            // backup the first value to aux
            aux = a[i];
            crtIdx = i;
            nxtIdx = b[i] - 1;
    
            // advance along the cycle until we reach 
            // again the first element 
            while (nxtIdx != i) {
                a[crtIdx] = a[nxtIdx];
    
                // use the b array to mark that the 
                // element at crtIdx was updated
                b[crtIdx] = -b[crtIdx];
    
                crtIdx = nxtIdx;
                nxtIdx = b[nxtIdx] - 1;
            }
    
            // finalize the cycle using the aux variable
            a[crtIdx] = aux;
            b[crtIdx] = -b[crtIdx];
        }
    
        // restore the original values of b[i]
        for (int i = 0; i < n; i++) {
            b[i] = -b[i];
        }
    }
    

    注意:虽然代码包含两个嵌套循环,但时间复杂度为 O(n)。这可以通过考虑每个元素仅更新一次这一事实来看出(如果我们到达一个已经处理过的元素,则外部循环将立即继续)。


    我将在这里展示算法执行的主要步骤,使用这个例子:

    a = {11, 22, 33, 44}
    b = { 2,  1,  4,  3}
    

    第 1 步。 我们查看第一个元素(请参见代码中的 i 的外部 for 循环)。第一个元素不是已处理循环的一部分,因此我们开始处理新循环。为此,我们将这个元素的初始值存储在 aux 中。

    a = {11, 22, 33, 44}
    b = { 2,  1,  4,  3}
    aux = 11
    

    第 2 步。 我们沿着这个循环,更新元素,将它们标记为已更新(通过否定 b 数组中的相应元素),直到我们再次到达第一个元素。

    a = {22, 22, 33, 44}
    b = {-2,  1,  4,  3}
    aux = 11
    

    第 3 步。 我们再次到达循环的第一个元素,并且需要它的初始值来更新循环的最后一个元素。这是我们使用辅助变量的地方。这样第一个循环就处理完了。

    a = {22, 11, 33, 44}
    b = {-2, -1,  4,  3}
    aux = 11
    

    第 4 步。 我们继续外循环(for 超过i)。我们看到第二个元素已经被处理(因为b[1] 是负数),因此我们不会在这里开始一个新的循环。我们继续,并在第三个元素(尚未处理)处开始一个新的循环。

    现在我们可以重用相同的aux 变量来备份这个循环的第一个元素(我们不再需要保存第一个循环的值,因为那个循环已经完全解决了)。

    a = {22, 11, 33, 44}
    b = {-2, -1,  4,  3}
    aux = 33
    

    第 5 步。 第二个循环的处理按照前面步骤中描述的类似方式进行,结果是这样的:

    a = {22, 11, 44, 33}
    b = {-2, -1, -4, -3}
    aux = 33
    

    第 6 步。 i 上的循环继续进行,没有找到未处理的元素。 现在,我们知道所有元素都已处理完毕,我们可以对 b 中的每个元素取反,以恢复原始值。

    a = {22, 11, 44, 33}
    b = { 2,  1,  4,  3}
    

    【讨论】:

    • 您确定这适用于所有情况吗?如果他的b数组是[3,4,1,2],那你就不用暂存两个数了吗?
    • @Jimmischel 确实,该许可有两个循环,但是一次处理一个循环(并且在完成一个循环的处理后,不再需要存储的值并且可以使用相同的辅助变量被重用),因此仍然需要一个辅助变量。
    • 我想我得画一幅画了。或者拿出我的扑克牌并以此为模型。我没看到。
    • @JimMischel :-) 我将添加一个 C/C++ 实现(但很可能是明天)——我会在完成之后再发送一个回复。感谢您对这种方法的兴趣!
    • @qwertyman 很棒的答案。我特别喜欢关于需要存储i'th 值的aux 变量的解释,当我们找到循环回i 的最后一个元素时需要替换它(因为值i 会有被其排列指向的第一个元素替换)。
    【解决方案3】:

    最简单的答案是将a复制到b,边走边销毁b:

    for (i = 0; i < B_SIZE; ++i):
        b[i] = a[b[i] - 1];
    

    然后,如果必须,只需将b 复制回a

    for (i = 0; i < B_SIZE; ++i):
        a[i] = b[i];
    

    由于 ab 都是 int 数组,因此您不会在这样做时耗尽任何多余的内存。您以a 中的正确值结束,而不会使用比提供给您的内存更多的内存。这不是最有效的(尽管它是 O(n)),但它最容易理解。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-08-11
      • 2018-02-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多