如何掌握就地数组修改算法?

时间:2010-02-28 20:29:39

标签: algorithm arrays in-place

我正准备进行软件工作面试,而我在就地修改阵列时遇到了麻烦。例如,在out-shuffle问题中,你将数组的两半交错,这样1 2 3 4 5 6 7 8将成为1 5 2 6 3 7 4 8. This question要求一个恒定内存解决方案(和线性时间,虽然我不确定这是否可能)。

首先,我认为线性算法是微不足道的,但后来我无法解决这个问题。然后我找到了一个简单的O(n^2)算法,但它花了我很长时间。我仍然没有找到更快的解决方案。

我还记得在Bentley编程珍珠中遇到类似问题时遇到了麻烦,第2栏:旋转i个位置左侧的数组(例如,旋转2的abcde变为cdeab),时间O(n)并且只有几个字节额外空间。

有没有人有提示帮助我解决这些问题?是否有针对此类问题的特定教程?谢谢!

7 个答案:

答案 0 :(得分:15)

关于O(n)时间,O(1)空间算法用于out-shuffle


可以在O(n)时间和O(1)空间中进行彻底改变,但它 艰难 。不确定为什么人们认为这很容易,并建议你尝试别的东西。

下面的论文有一个O(n)时间和O(1)空间解决方案(虽然它是为了洗牌,但是做洗不足使得混乱变得微不足道):

http://arxiv.org/PS_cache/arxiv/pdf/0805/0805.1598v1.pdf


关于解决就地数组修改算法的方法


就地修改算法可能会非常难以来处理。

考虑一对夫妇:

  • 在线性时间内进行外部随机播放。使用数论。
  • 就地合并排序,开放了几年。算法来了,但实际上太复杂了。使用非常复杂的簿记。

很抱歉,如果这听起来令人沮丧,但没有魔法灵药可以解决所有就地算法问题。您需要处理问题,弄清楚它的属性并尝试利用它们(就像大多数算法一样)。

也就是说,对于导致原始数组的置换的数组修改,您可以尝试following the cycles of the permutation的方法。基本上任何排列都可以写成一组不相交的循环(参见John的答案)。例如排列:

1 4 2 5 3 6
1 2 3 4 5 6

可以写为

1 -> 1
2 -> 3 -> 5 -> 4 -> 2
6 -> 6.

您可以将箭头视为“转到”。

因此,要对数组1 2 3 4 5 6进行置换,请遵循以下三个周期:

1转为1.

6到6。

2变为3,3变为5,5变为4,4变为2。

要遵循这个长周期,您只能使用一个临时变量。将3存储在其中。把2放在3的位置。现在把3分放在5中,然后将5分存储在temp中,依此类推。由于您只使用常量额外临时空间来跟踪特定周期,因此您正在对该周期进行数组的就地修改。

现在,如果我给你一个计算元素所在位置的公式,你现在需要的只是每个周期的起始元素集。

明智地选择循环的起点可以使算法变得容易。如果你想出O(1)空间中的起点,你现在有了一个完整的就地算法。这是您可能必须熟悉问题并利用其属性的地方。

即使您不知道如何计算循环的起点,但有一个计算下一个元素的公式,您也可以使用此方法在某些特殊情况下获得O(n)时间就地算法案例。

例如:如果你知道有符号整数数组只保留正整数。

您现在可以按照周期进行操作,但是否定其中的数字作为“已访问”元素的指示。现在,您可以遍历数组并选择您遇到的第一个正数,并按照周期进行操作,使循环元素为负数并继续查找未触及的元素。最后,你只需要使所有元素再次为正,以得到最终的排列。

你得到一个O(n)时间和O(1)空间算法!当然,我们通过使用数组整数的符号位作为我们的个人“访问”位图来“欺骗”。

即使数组不一定是整数,这个方法(遵循循环,而不是符号位的黑客:-))实际上可以用来解决你所说的两个问题:

  • The inshuffle (or out-shuffle) problem:当2n + 1为3的幂时,可以显示(使用数论)1,3,3 ^ 2等处于不同的周期并且所有周期都被覆盖使用那些。结合这种事实,即洗牌很容易分裂和征服,你得到一个O(n)时间,O(1)空间算法(公式是i - > 2 * i模2n + 1)。有关详细信息,请参阅上述文章。

  • The cyclic shift an array problem:循环移位大小为n乘k的数组也给出了结果数组的排列(由公式i给出i + k模n),也可以求解线性时间和就地使用以下循环方法。实际上,就元素交换的数量而言,该后续循环方法比3个反转算法更好。当然,由于访问模式,循环方法可以杀死缓存,实际上3反转算法实际上可能更好。


至于面试,如果面试官是一个合理的人,他们会看着你如何思考并解决问题,而不是你是否真正解决了问题。所以,即使你没有解决问题,我认为你不应该气馁。

答案 1 :(得分:4)

使用就地算法的基本策略是找出将条目从插槽N移动到插槽M的规则。

所以,你的洗牌,例如。如果A和B是卡,N是chards的数量。甲板上半部分的规则与甲板下半部分的规则不同

 // A is the current location, B is the new location.
 // this math assumes that the first card is card 0
 if (A < N/2)
    B = A * 2;
 else
    B = (A - N/2) * 2 + 1;

现在我们知道规则,我们只需要移动每张卡,每次移动卡片,我们计算新位置,然后删除当前位于B的卡片。插槽B,然后让B为A,然后循环回到算法的顶部。移动的每张卡取代新卡,成为下一张要移动的卡。

如果我们基于0而不是基于1,我认为分析更容易,所以

 0 1 2 3 4 5 6 7  // before
 0 4 1 5 2 6 3 7  // after

所以我们想要移动1-> 2 2-> 4 4-> 1并完成一个循环 然后移动3-> 6 6-> 5 5-> 3并完成一个循环 我们已经完成了。

现在我们知道卡0和卡N-1不移动,所以我们可以忽略它们, 所以我们知道我们只需要交换N-2卡。唯一的粘性位 是有2个循环,1,2,4,1和3,6,5,3。当我们到达卡1时 第二次,我们需要继续使用卡片3.

 int A = 1;
 int N = 8;
 card ary[N]; // Our array of cards
 card a = ary[A];

 for (int i = 0; i < N/2; ++i)
 {
     if (A < N/2)
        B = A * 2;
     else
        B = (A - N/2) * 2 + 1;

     card b = ary[B];
     ary[B] = a;
     a = b;
     A = B;

     if (A == 1)
     {
        A = 3;
        a = ary[A];
     }
 }   

现在这段代码仅适用于8卡示例,因为当我们完成第一个周期时,if测试将我们从1移动到3。我们真正需要的是一个通用规则来识别循环的结束,以及从哪里开始下一个循环。

如果您可以想到某种方式,那么该规则可能是数学的,或者您可以在单独的数组中跟踪您访问过的地点,当A返回到访问过的地方时,您可以在数组中向前扫描寻找第一个未访问过的地方。

对于您的就地算法为0(n),解决方案需要是数学的。

我希望这个思维过程的细分对你有所帮助。如果我正在采访你,我希望在白板上看到类似的东西。

注意:正如Moron指出的那样,这对N的所有值都不起作用,这只是面试官正在寻找的那种分析的一个例子。

答案 2 :(得分:1)

对于第一个,让我们假设n是偶数。你有:

上半场:1 2 3 4
第二:5 6 7 8

设x1 =第一[1],x2 =第二[1]。

现在,您必须从上半部分打印一个,从第二个打印一个,从第一个打印一个,从第二个打印一个......

含义首先[1],第二[1],第一[2],第二[2],...... 显然你不会在内存中保留两半,因为那将是O(n)内存。你保持指向两半的指针。你知道你是怎么做到的吗?

第二个有点困难。考虑一下:
12345
abcde
..cde
.....ab
..cdeab
cdeab

你注意到了吗?您应该注意到这个问题基本上要求您将第一个i字符移动到字符串的末尾,而不提供将最后一个n - i复制到缓冲区然后附加第一个i然后返回缓冲区的奢侈。你需要处理O(1)内存。

要想明白如何做到这一点,基本上需要对这些问题进行大量练习,就像其他任何事情一样。实践基本完善。如果你以前从未做过这类问题,你就不太可能弄明白了。如果你有,那么你必须考虑如何操纵子串和/或索引,以便在给定的约束条件下解决问题。一般规则是尽可能多地工作并尽可能多地学习,以便在看到这些问题时能够非常快速地找出这些问题的解决方案,但解决方案在问题上有很大不同。我担心没有明确的成功秘诀。在继续之前,请仔细阅读并了解您阅读的内容。

第二个问题的逻辑是:如果我们反转子串[1,2],子串[3,5]然后连接它们并反转它会发生什么?我们一般都有:

1, 2, 3, 4, ..., i, i + 1, i + 2, ..., N
反向[1,i] =&gt;
i, i - 1, ..., 4, 3, 2, 1, i + 1, i + 2, ..., N反向[i + 1,N] =&gt;
i, i - 1, ..., 4, 3, 2, 1, N, ..., i + 1反向[1, N] =&gt;
i + 1, ..., N, 1, 2, 3, 4, ..., i - 1, i
,这是你想要的。使用O(1)存储器写入反向功能应该是微不足道的。

答案 3 :(得分:1)

弗兰克,

对于使用循环和数组进行编程,没有什么能胜过David Gries的教科书The Science of Programming。我在20多年前研究过它,并且每天都有一些我仍在使用的想法。这是非常数学的,需要真正的努力才能掌握,但这种努力会在你的整个职业生涯中多次回报你。

答案 4 :(得分:1)

补充Aryabhatta's answer

即使不知道每个循环的起始位置或使用内存来了解访问周期,也有一种“遵循循环”的一般方法。如果您需要O(1)内存,这将特别有用。

对于阵列中的每个位置i,按照循环而不移动任何数据,直到达到...

  • 起始位置i:cyle的结尾。这是一个新的循环:这次再次跟踪它移动数据。
  • 低于我的位置:这个周期已经被访问过,与它无关。

当然这有时间开销(O(n ^ 2),我相信)并且具有一般“跟随周期”方法的缓存问题。

答案 5 :(得分:0)

一般来说,这个想法是循环遍历数组,而

  • 将值存储在临时变量
  • 找到该位置的正确值并将其写入
  • 要么继续下一个值,要么在继续之前弄清楚如何处理临时值。

答案 6 :(得分:0)

一般方法如下:

  1. 构造一个位置数组 int [] pos ,这样 pos [i] 指的是的位置(索引)洗牌阵列中的[i]
  2. 重新排列原始数组 int [] a ,根据此位置数组 pos

    /** Shuffle the array a. */    
    void shuffle(int[] a) {
        // Step 1
        int [] pos = contructRearrangementArray(a)
        // Step 2
        rearrange(a, pos);
    }
    
    /**
     * Rearrange the given array a according to the positions array pos.
     */
    private static void rearrange(int[] a, int[] pos)
    {
        //  By definition 'pos' should not contain any duplicates, otherwise rearrange() can run forever.
       // Do the above sanity check.
        for (int i = 0; i < pos.length; i++) {
            while (i != pos[i]) {
                // This while loop completes one cycle in the array
                swap(a, i, pos[i]);
                swap(pos, i, pos[i]);
            }
        }
    }
    
    /** Swap ith element in a with jth element. */
    public static void swap(int[] a, int i, int j) 
    {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
    
  3. 例如,对于 outShuffle 的情况,以下是contructRearrangementArray()的实现。

    /**
     * array     : 1 2 3 4 5 6 7 8
     * pos       : 0 2 4 6 1 3 5 7
     * outshuffle: 1 5 2 6 3 7 4 8 (outer boundaries remain same)
     */
    public int[] contructRearrangementArray(int[] a)
    {
        if (a.length % 2 != 0) {
            throw new IllegalArgumentException("Cannot outshuffle odd sized array");
        }
        int[] pos = new int[a.length];
        for (int i = 0; i < pos.length; i++) {
            pos[i] = i * 2 % (pos.length - 1);
        }
        pos[a.length - 1] = a.length - 1;
        return pos;
    }