【问题标题】:How to master in-place array modification algorithms?如何掌握原地数组修改算法?
【发布时间】:2010-02-28 20:29:38
【问题描述】:

我正在准备软件工作面试,但在就地数组修改方面遇到了麻烦。

例如,在 out-shuffle 问题中,您将数组的两半交错,这样 1 2 3 4 5 6 7 8 将变为 1 5 2 6 3 7 4 8This question 要求提供恒定内存解决方案(和线性时间,尽管我不确定这是否可能)。

起初我认为线性算法是微不足道的,但后来我无法解决。然后我确实找到了一个简单的O(n^2) 算法,但我花了很长时间。而且我仍然没有找到更快的解决方案。

我记得在解决 Bentley 的 Programming Pearls 第 2 列中的类似问题时也遇到了麻烦:

将数组向左旋转 i 位置(例如 abcde 旋转 2 变为 cdeab),及时 O(n) 并且只有几个字节的额外空间。

有没有人可以帮助我解决这些问题?

【问题讨论】:

  • 这是一个软件工程职位。
  • @Pavel:你应该是人力资源部门的,你应该有能力通过一篇帖子来估计编程技能
  • @Pavel,我认为他的问题与对工程充满热情的人不相上下;他在问他关心的事情。我无法接近回答他的问题,但享受作为 Web 开发人员的成功职业,解释一下。 +1 询问。
  • 这个问题比人们想象的要难(O(n) 时间和 O(1) 空间)。我建议你们所有人在建议 OP 采取其他措施之前,针对各种 n 值运行解决方案。
  • @Pavel Shved :很抱歉,但您的评论很荒谬。实际上,这不仅对提出此问题的人而且对堆栈溢出的一般用户而言都太令人沮丧了。请不要再发布此类 cmets 。这是一个真诚的要求....呵呵!!!!

标签: algorithm arrays in-place


【解决方案1】:

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


在 O(n) 时间和 O(1) 空间内进行洗牌是可能的,但 很难。不知道为什么人们认为这很容易并建议您尝试其他方法。

下面的论文有一个 O(n) 时间和 O(1) 空间的解决方案(虽然它是用于 in-shuffle,但进行 in-shuffle 会使 out-shuffle 变得微不足道):

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


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


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

考虑一对:

  • 在线性时间内就地洗牌。使用数论。
  • 就地合并排序已开放几年。一个算法来了,但太复杂了,不实用。使用非常复杂的簿记。

抱歉,如果这听起来令人沮丧,但没有灵丹妙药可以为您解决所有就地算法问题。你需要解决这个问题,找出它的属性,并尝试利用它们(就像大多数算法一样)。

也就是说,对于结果是原始数组的排列的数组修改,您可以尝试遵循排列的循环的方法。基本上,任何排列都可以写成一组不相交的循环(也参见约翰的回答)。比如排列:

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。

要遵循这个漫长的循环,您可以只使用一个temp 变量。将 3 存储在其中。将 2 放在 3 所在的位置。现在将 3 放入 5 并将 5 存储在 temp 中,依此类推。由于您只使用恒定的额外 temp 空间来遵循特定循环,因此您正在为该循环对数组进行就地修改。

现在,如果我给你一个计算元素去向的公式,你现在需要的只是每个循环的起始元素集合。

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

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

例如:如果你知道无符号整数数组只包含正整数。

您现在可以跟踪循环,但将其中的数字否定为“已访问”元素的指标。现在您可以遍历数组并选择遇到的第一个正数并遵循循环,使循环的元素为负并继续找到未触及的元素。最后,您只需再次使所有元素为正,即可得到结果排列。

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

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

  • The in-shuffle (or out-shuffle) problem:当2n+13 的幂时,可以证明(使用数论)1,3,3^2, 等处于不同的循环中,并且使用它们覆盖所有循环。将这一点与 in-shuffle 容易分而治之的事实相结合,您将获得 O(n) 时间、O(1) 空间算法(公式为 i -> 2*i modulo 2n+1)。有关详细信息,请参阅上述论文。

  • The cyclic shift an array problem: 将大小为n 的数组循环移位k 也给出了结果数组的排列(由公式ii+k modulo n 给出),也可以线性求解时间和就地使用下面的循环方法。事实上,就元素交换的数量而言,以下循环方法优于 3 reverses 算法。当然,由于访问模式的原因,遵循循环方法可以杀死缓存,并且在实践中,3 反向算法实际上可能会更好。


对于面试,如果面试官是一个通情达理的人,他们会看你是如何思考的,而不是看你是否真的解决了问题。所以即使你没有解决一个问题,我想你也不应该气馁。

【讨论】:

  • 我在 20 分钟内没有到达任何地方后,最终在谷歌上搜索了这个问题 :)。确实很难。像我建议的那样以正确的顺序打印数字是微不足道的(顺便说一下,O(n),而不是 O(nlogn)),但不可否认的是作弊。
  • @IVlad:是的,但分而治之确实给出了 O(nlogn) 时间,O(1) 空间算法用于洗牌(没有作弊!)。我建议你尝试一下,这很有趣:-)
  • 找到一种在 O(n^2) 时间和 O(1) 空间内求解的方法是合理的,考虑到问题的难度。即使是 O(nlogn) 时间和 O(1) 空间也足够困难(因为它使用循环移位作为子方法)。我能想到的唯一原因是,如果你认为这个问题可以轻松解决,建议他们采取其他措施。对不起,如果这不是你的意思,但肯定是这样的(至少对我来说)。
  • @Aryabhatta,我们可以使用位向量并在循环中出现该索引时设置相应的索引,而不是否定数字来标记已使用的数字。我们正常继续数组迭代,当我们看到已经在位向量中设置的新索引时,我们只需继续,否则从该索引开始一个新的循环。
  • @Cupidvogel 相当于使用O(n) 空间。据我了解,OP 正在寻找恒定空间解决方案。
【解决方案2】:

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

例如,您的随机播放。如果 A 和 B 是卡片,N 是字符数。上半场的规则和下半场的规则不同

 // 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 中的卡片。将 A 放在槽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 值,这只是面试官正在寻找的分析的一个示例。

【讨论】:

    【解决方案3】:

    弗兰克,

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

    【讨论】:

      【解决方案4】:

      补充Aryabhatta's answer

      即使不知道每个循环的起始位置或使用内存来知道访问过的循环,也有一种通用方法可以“跟踪循环”。如果您需要 O(1) 内存,这将特别有用。

      对于数组中的每个位置 i,在不移动任何数据的情况下遵循循环,直到到达...

      • 起始位置i:循环结束。这是一个新的循环:这次再次跟随它移动数据。
      • 低于 i 的位置:此循环已被访问过,与此无关。

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

      【讨论】:

        【解决方案5】:

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

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

        x1 = first[1]x2 = second[1]

        现在,您必须打印前半部分的一个、后半部分的一个、前半部分的一个、后半部分的一个...

        含义first[1], second[1], first[2], second[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
        
        reverse [1, i] =>
        i, i - 1, ..., 4, 3, 2, 1, i + 1, i + 2, ..., N
        
        reverse [i + 1, N] =>
        i, i - 1, ..., 4, 3, 2, 1, N, ..., i + 1
        
        reverse [1, N] =>
        i + 1, ..., N, 1, 2, 3, 4, ..., i - 1, i
        

        这就是你想要的。使用 O(1) 内存编写反向函数应该很简单。

        【讨论】:

        • 感谢您的解释。至于第一个,以不同的顺序打印出来很容易,但实际上就地修改数组本身以使其元素被打乱更难。
        • 哦,我错过了。如果我弄明白了,我会考虑并编辑我的帖子。
        【解决方案6】:

        一般来说,思路是遍历数组一次,while

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

        【讨论】:

          【解决方案7】:

          一般方法如下:

          1. 构造一个positions数组 int[] pos,使得pos[i]引用的位置(索引) a[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;
            }
            

          例如,对于 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;
          }
          

          【讨论】:

            猜你喜欢
            • 2015-10-27
            • 2011-05-29
            • 1970-01-01
            • 2011-05-27
            • 2011-03-21
            • 2012-12-29
            • 1970-01-01
            • 2014-11-01
            • 1970-01-01
            相关资源
            最近更新 更多