【问题标题】:Finding duplicates in O(n) time and O(1) space在 O(n) 时间和 O(1) 空间中查找重复项
【发布时间】:2011-08-09 23:43:50
【问题描述】:

输入:给定一个包含 n 个元素的数组,其中包含从 0 到 n-1 的元素,其中任何一个数字出现任意次数。

目标:在 O(n) 中找到这些重复的数字,并且只使用恒定的内存空间。

例如,设 n 为 7,数组为 {1, 2, 3, 1, 3, 0, 6},答案应为 1 & 3。 我在这里检查了类似的问题,但答案使用了一些数据结构,如HashSet 等。

任何有效的算法相同?

【问题讨论】:

    标签: c++ c algorithm


    【解决方案1】:

    我们可以做到 O(n) 时间和 O(1) 空间复杂度 -

    1. 取第 i 个数组元素。

    2. 如果它是负数,让它 +ve

    3. 最后,将 -1 乘以从数组索引(第 i 个元素)获得的数字。

    4. 如果数字为正,则返回索引。

       def findDuplicate(self, arr: List[int]) -> int:
           n=len(arr)
           for i in range(0,n):
      
               arr[(abs(arr[i]))-1]=arr[(abs(arr[i]))-1]*(-1)
               if arr[(abs(arr[i]))-1]>0:
                   return abs(arr[i])
      

    【讨论】:

      【解决方案2】:

      在这里查看解释https://youtu.be/qJ_Y7pKP0e4

      代码在这里https://github.com/TechieExpress/DataStructures/blob/main/findDuplicates

      代码sn-p:

      /**
      *
      * @author techieExpress
      *
      * You are given a list of n-1 integers and these integers are in the range * of 1 to n.
      * Input: Given an array of n elements which contains elements 
      * from 0 to n-1, with any of these numbers appearing any number of times.
      * 
      * Goal: To find these repeating numbers in O(n) and using only constant  * * memory space.
      **/
      
      public class findDuplicates {
          
          
          public static void main(String args[])
          {
              int arr[] = { 2,1,1,2 };
        
              for (int i = 0; i < arr.length; i++) {
                  arr[arr[i] % arr.length]
                      = arr[arr[i] % arr.length]
                        + arr.length;
              }
              System.out.println("The repeating elements are : ");
              for (int i = 0; i < arr.length; i++) {
                  
                  //System.out.print(numRay[i]);
                  if (arr[i] >= arr.length * 2) {
                      System.out.println(i + " ");
                      arr[i]=arr[i]%arr.length;
                  }
              }
          }
      
      }
      

      【讨论】:

      • 函数注释头和int arr[] = { 2,1,1,2 };矛盾
      • @shubham 对不起?没有得到你。
      • @shubham 感谢您指出,只有 cmets 不正确,解决方案是针对上面提出的问题,更正 cmets。
      【解决方案3】:
      private static void printRepeating(int arr[], int size) {
              int i = 0;
              int j = 1;
              while (i < (size - 1)) {
                  if (arr[i] == arr[j]) {
                      System.out.println(arr[i] + " repeated at index " + j);
                      j = size;
                  }
                  j++;
                  if (j >= (size - 1)) {
                      i++;
                      j = i + 1;
                  }
              }
      
          }
      

      【讨论】:

      • 上述方案在时间复杂度O(n)和常数空间上都会达到同样的效果。
      • 感谢您提供此代码 sn-p,它可能会提供一些有限的短期帮助。一个正确的解释would greatly improve 它的长期价值通过展示为什么这是一个很好的解决问题的方法,并将使它对未来有其他类似问题的读者更有用。请edit您的回答添加一些解释,包括您所做的假设。
      • 顺便说一句,这里的时间复杂度似乎是 O(n²) - 隐藏内部循环不会改变这一点。
      【解决方案4】:

      这是我想出来的,不需要额外的符号位:

      for i := 0 to n - 1
          while A[A[i]] != A[i] 
              swap(A[i], A[A[i]])
          end while
      end for
      
      for i := 0 to n - 1
          if A[i] != i then 
              print A[i]
          end if
      end for
      

      第一个循环对数组进行置换,以便如果元素 x 至少出现一次,则其中一个条目将位于位置 A[x]

      请注意,乍一看它可能看起来不像 O(n),但它确实是 - 尽管它有一个嵌套循环,但它仍然在 O(N) 时间运行。仅当存在i 使得A[i] != i 时才会发生交换,并且每个交换设置至少一个元素使得A[i] == i,以前不是这样。这意味着交换的总数(以及 while 循环体的执行总数)最多为 N-1

      第二个循环打印x 的值,其中A[x] 不等于x - 因为第一个循环保证如果x 在数组中至少存在一次,其中一个实例将是在A[x],这意味着它会打印数组中不存在的x 的值。

      (Ideone link so you can play with it)

      【讨论】:

      • @arasmussen:是的。不过,我首先想出了一个损坏的版本。问题的约束为解决方案提供了一点线索 - 每个有效数组值也是有效数组索引的事实暗示 a[a[i]],而 O(1) 空间约束暗示 swap() 操作是键。
      • @caf : 请使用数组作为 {3,4,5,3,4} 运行您的代码,它会失败。
      • @NirmalGeo:这不是一个有效的输入,因为5 不在0..N-1 范围内(在这种情况下N5)。
      • @caf {1,2,3,1,3,0,0,0,0,6} 的输出为 3 1 0 0 0 或在任何重复次数超过 2 的情况下. o/p 正确吗?
      • 这太棒了!我在这个问题上看到了许多变体,通常更受限制,这是我所见过的最通用的解决方法。我将简单地提一下,将print 语句更改为print i 会将其变成stackoverflow.com/questions/5249985/… 的解决方案,并且(假设“bag”是一个可修改的数组)stackoverflow.com/questions/3492302/… 的Qk。
      【解决方案5】:

      这是伪代码

      for i <- 0 to n-1:
         if (A[abs(A[i])]) >= 0 :
             (A[abs(A[i])]) = -(A[abs(A[i])])
         else
            print i
      end for
      

      Sample code in C++

      【讨论】:

      • 非常聪明 - 在索引条目的符号位中编码答案!
      • @sashan:不可能。查看问题规范。 "给定一个包含 n 个元素的数组,其中包含从 0 到 n-1 的元素"
      • 这不会检测到重复的 0,并且会多次发现相同的数字作为重复。
      • @Null Set:你可以将-替换为~来解决零问题。
      • 这可能是问题的答案,但从技术上讲,它使用 O(n) 隐藏空间 - n 符号位。如果数组被定义为每个元素只能保存0n-1 之间的值,那么它显然不起作用。
      【解决方案6】:

      假设我们将此数组表示为单向图数据结构 - 每个数字都是一个顶点,它在数组中的索引指向另一个顶点,形成图的一条边。

      为了更简单,我们有从 0 到 n-1 的索引和从 0..n-1 的数字范围。 例如

         0  1  2  3  4 
       a[3, 2, 4, 3, 1]
      

      0(3) --> 3(3) 是一个循环。

      Answer: 只需根据索引遍历数组即可。如果 a[x] = a[y] 那么它是一个循环,因此是重复的。跳到下一个索引并再次继续,依此类推,直到数组结束。 复杂度:O(n) 时间和 O(1) 空间。

      【讨论】:

      • 嗯。我看不到循环和重复之间的良好联系。考虑array = [1, 0]: element s 0 和 1 循环,但不重复。您可以推断,如果您使用这种遍历方法并到达一个循环,那么循环 before 的最后一个元素是重复的,例如:array = [1, 2, 3, 4, 2]。这会产生一些新问题。如何在不使用额外内存和时间的情况下检测循环。
      • 其次,即使您可以检测到您何时以恒定的时间和空间循环回来,像这样的数组呢:array = [1, 2, ...., n - 1, 0, 0]0 值的单个副本)。遍历每个元素的周期将花费 O(n) 时间,因此总共将是 O(n^2) 时间。
      • @Elliott 我相信这是“弗洛伊德循环检测算法”,已证明需要 O(n) 时间才能找到重复项。
      • @Zenquiorra,我认为我上面的例子足以证明这不起作用。另外,这里的伊万没有描述弗洛伊德的方法,它使用两种遍历速度。此外,弗洛伊德可以在这里调整以确定是否存在重复(在O(n)时间和O(1)空间),但这无助于提供解决方案。
      • @Elliott 他们提到时不是使用两种遍历速度吗? a[x] = a[y] 其中 x 和 y 是两个索引(两个不同的速度)?
      【解决方案7】:
      static void findrepeat()
      {
          int[] arr = new int[7] {0,2,1,0,0,4,4};
      
          for (int i = 0; i < arr.Length; i++)
          {
              if (i != arr[i])
              {
                  if (arr[i] == arr[arr[i]])
                  {
                      Console.WriteLine(arr[i] + "!!!");
                  }
      
                  int t = arr[i];
                  arr[i] = arr[arr[i]];
                  arr[t] = t;
              }
          }
      
          for (int j = 0; j < arr.Length; j++)
          {
              Console.Write(arr[j] + " ");
          }
          Console.WriteLine();
      
          for (int j = 0; j < arr.Length; j++)
          {
              if (j == arr[j])
              {
                  arr[j] = 1;
              }
              else
              {
                  arr[arr[j]]++;
                  arr[j] = 0;
              }
          }
      
          for (int j = 0; j < arr.Length; j++)
          {
              Console.Write(arr[j] + " ");
          }
          Console.WriteLine();
      }
      

      【讨论】:

      • 这基本上是 Caf 的答案,但有错误,没有解释。
      【解决方案8】:

      C 中的一个解决方案是:

      #include <stdio.h>
      
      int finddup(int *arr,int len)
      {
          int i;
          printf("Duplicate Elements ::");
          for(i = 0; i < len; i++)
          {
              if(arr[abs(arr[i])] > 0)
                arr[abs(arr[i])] = -arr[abs(arr[i])];
              else if(arr[abs(arr[i])] == 0)
              {
                   arr[abs(arr[i])] = - len ;
              }
              else
                printf("%d ", abs(arr[i]));
          }
      
      }
      int main()
      {   
          int arr1[]={0,1,1,2,2,0,2,0,0,5};
          finddup(arr1,sizeof(arr1)/sizeof(arr1[0]));
          return 0;
      }
      

      时间复杂度为 O(n),空间复杂度为 O(1)。

      【讨论】:

      • 它的空间复杂度是 O(N),因为它使用了 N 个额外的符号位。该算法应该在数组元素类型包含从 0 到 N-1 的数字的假设下工作。
      • 是的,但对于询问算法来说它是完美的,因为他们只想要数字 0 到 n-1 的算法,而且我检查了你的解决方案它高于 O(n) 所以我想到了这个跨度>
      【解决方案9】:

      如果数组不是太大,这个解决方案更简单, 它创建了另一个相同大小的数组以供滴答。

      1 创建一个与输入数组大小相同的位图/数组

       int check_list[SIZE_OF_INPUT];
       for(n elements in checklist)
           check_list[i]=0;    //initialize to zero
      

      2 扫描您的输入数组并增加其在上述数组中的计数

      for(i=0;i<n;i++) // every element in input array
      {
        check_list[a[i]]++; //increment its count  
      }  
      

      3 现在扫描 check_list 数组并打印重复的一次或多次重复

      for(i=0;i<n;i++)
      {
      
          if(check_list[i]>1) // appeared as duplicate
          {
              printf(" ",i);  
          }
      }
      

      当然它占用的空间是上面给出的解决方案的两倍,但时间效率是O(2n),基本上是O(n)。

      【讨论】:

      • 哎呀...!没注意到……我的错。
      • @nikhil O(1) 怎么样?我的数组 check_list 随着输入大小的增长而线性增长,那么 O(1) 如果是的话,你用什么启发式方法将其称为 O(1)。
      • 对于给定的输入,您需要恒定的空间,不是 O(1) 吗?我很可能是错的:)
      • 随着输入的增长,我的解决方案需要更多空间。算法的效率(空间/时间)未针对特定输入进行测量。(在这种情况下,每个搜索算法的时间效率将是恒定的,即在我们搜索的第一个索引中找到的元素)。它针对任何输入进行测量,那就是我们有最佳情况、最坏情况和平均情况的原因。
      【解决方案10】:

      在以下 C 函数中可以很容易地看到算法。检索原始数组虽然不是必需的,但可以将每个条目模n

      void print_repeats(unsigned a[], unsigned n)
      {
          unsigned i, _2n = 2*n;
          for(i = 0; i < n; ++i) if(a[a[i] % n] < _2n) a[a[i] % n] += n;
          for(i = 0; i < n; ++i) if(a[i] >= _2n) printf("%u ", i);
          putchar('\n');
      }
      

      Ideone Link for testing.

      【讨论】:

      • 我担心这在技术上是“作弊”,因为使用高达 2*n 的数字需要比存储原始数字所需的每个数组条目多 1 位的存储空间。实际上,您需要更接近 log2(3) = 1.58 每个条目的额外位,因为您存储的数字最多为 3*n-1。
      【解决方案11】:

      不是很漂亮,但至少很容易看到 O(N) 和 O(1) 属性。基本上我们扫描数组,并且对于每个数字,我们查看对应的位置是否已被标记为已经看过一次 (N) 或已经多次看过 (N+1)。如果它被标记为已经看过一次,我们打印它并标记它已经看过多次。如果没有标记,我们标记它已经见过一次,并将相应索引的原始值移动到当前位置(标记是破坏性操作)。

      for (i=0; i<a.length; i++) {
        value = a[i];
        if (value >= N)
          continue;
        if (a[value] == N)  {
          a[value] = N+1; 
          print value;
        } else if (a[value] < N) {
          if (value > i)
            a[i--] = a[value];
          a[value] = N;
        }
      }
      

      或者,更好(更快,尽管有双循环):

      for (i=0; i<a.length; i++) {
        value = a[i];
        while (value < N) {
          if (a[value] == N)  {
            a[value] = N+1; 
            print value;
            value = N;
          } else if (a[value] < N) {
            newvalue = value > i ? a[value] : N;
            a[value] = N;
            value = newvalue;
          }
        }
      }
      

      【讨论】:

      • +1,它工作得很好,但要弄清楚if (value &gt; i) a[i--] = a[value]; 工作的确切原因需要一些思考:如果value &lt;= i 那么我们已经处理了a[value] 的值并且可以覆盖它安全。我也不会说 O(N) 的性质是显而易见的!拼写出来:主循环运行N 次,加上a[i--] = a[value]; 行运行多次。该行仅在a[value] &lt; N 时才能运行,并且每次运行时,紧接着将尚未N 的数组值设置为N,因此它最多可以运行N 次,总共最多2N 循环迭代。
      【解决方案12】:

      caf's brilliant answer 打印在数组 k-1 中出现 k 次的每个数字。这是有用的行为,但可以说,这个问题要求每个副本只打印一次,他暗示这样做的可能性不会超出线性时间/恒定空间界限。这可以通过用以下伪代码替换他的第二个循环来完成:

      for (i = 0; i < N; ++i) {
          if (A[i] != i && A[A[i]] == A[i]) {
              print A[i];
              A[A[i]] = i;
          }
      }
      

      这利用了在第一个循环运行后,如果任何值m 多次出现,则其中一个出现在正确的位置,即A[m]。如果我们小心的话,我们可以使用该“家”位置来存储有关是否已打印任何副本的信息。

      在 caf 的版本中,当我们遍历数组时,A[i] != i 暗示 A[i] 是重复的。在我的版本中,我依赖于一个稍微不同的不变量:A[i] != i &amp;&amp; A[A[i]] == A[i] 意味着A[i] 是一个重复的我们以前没有见过。 (如果你去掉“我们以前没见过的”部分,其余部分可以看出是由 caf 不变量的真相所暗示的,并且保证所有重复项在一个主位置都有一些副本。)这个属性成立于一开始(在 caf 的第一个循环完成之后),我在下面显示它在每个步骤之后都得到维护。

      当我们遍历数组时,测试的A[i] != i 部分的成功意味着A[i] 可能是以前从未见过的重复。如果我们以前没有见过它,那么我们期望A[i] 的主位置指向它自己——这就是if 条件的后半部分所测试的。如果是这种情况,我们将其打印出来并更改主位置以指向第一个找到的副本,从而创建一个两步“循环”。

      要看到这个操作不会改变我们的不变量,假设m = A[i] 对特定位置i 满足A[i] != i &amp;&amp; A[A[i]] == A[i]。很明显,我们所做的更改 (A[A[i]] = i) 将通过导致if 条件的第二半失败,来防止m 的其他非家庭事件被输出为重复项,但是当@ 时它会起作用吗? 987654339@到家,m?是的,因为现在,即使在这个新的i 处,我们发现if 条件的前半部分A[i] != i 为真,后半部分测试它指向的位置是否是家庭位置,并且发现不是。在这种情况下,我们不再知道 mA[m] 是否是重复值,但我们知道无论哪种方式,已经报告过,因为保证不会出现这两个循环在 caf 的第一个循环的结果中。 (请注意,如果m != A[m],那么mA[m] 中恰好有一个出现了不止一次,而另一个根本不出现。)

      【讨论】:

      • 是的,这与我想出的非常相似。有趣的是,相同的第一个循环如何对几个不同的问题有用,只是使用不同的打印循环。
      【解决方案13】:

      对于相对较小的 N 我们可以使用 div/mod 操作

      n.times do |i|
        e = a[i]%n
        a[e] += n
      end
      
      n.times do |i| 
        count = a[i]/n
        puts i if count > 1
      end
      

      不是 C/C++,但无论如何

      http://ideone.com/GRZPI

      【讨论】:

      • +1 不错的解决方案。两次后停止向条目添加 n 将容纳更大的 n
      猜你喜欢
      • 2015-05-25
      • 2012-08-06
      • 1970-01-01
      • 1970-01-01
      • 2012-01-02
      • 2012-02-22
      • 2017-04-06
      • 1970-01-01
      • 2011-10-02
      相关资源
      最近更新 更多