在 O(n) 时间内找到数组中的重复元素

Posted

技术标签:

【中文标题】在 O(n) 时间内找到数组中的重复元素【英文标题】:Find duplicate element in array in time O(n) 【发布时间】:2013-02-03 08:45:44 【问题描述】:

我在一次求职面试中被问到这个问题,我一直想知道正确的答案。

您有一个从 0 到 n-1 的数字数组,其中一个数字被删除,并替换为数组中已有的数字,该数字与该数字重复。我们如何才能及时检测到这个重复O(n)

例如,4,1,2,3 的数组将变为 4,1,2,2

时间O(n2)的简单解决方法是使用嵌套循环来查找每个元素的重复项。

【问题讨论】:

不必排序 忘记了一个元素被替换了...用我的方法以[替换的数字] xor [重复的数字]结束... 大声笑,所有的解决方案都有或多或少相同的方法...... 是否有社区可以讨论此类问题? 【参考方案1】:

这个视频If Programming Was An Anime太有趣了,不能分享。同样的问题,视频有答案:

    排序 创建哈希图/字典。 创建一个数组。 (虽然这部分被跳过了。) 使用龟兔算法。

注意:这个问题更像是一个琐事问题,而不是现实世界。哈希图之外的任何解决方案都是过早的优化,除非在罕见的有限内存情况下,如嵌入式编程。

此外,您最后一次在现实世界中看到一个数组,其中数组中的所有变量都适合数组的大小是什么时候?例如,如果数组中的数据是字节(0-255),那么您是否有一个包含 256 个元素或更大元素且其中没有 null 或 inf 的数组,并且您需要找到一个重复的数字?这种情况非常罕见,你可能在整个职业生涯中都不会使用这个技巧。

因为这是一个琐事问题,而不是现实世界的问题,所以我会谨慎地接受一家提出此类琐事问题的公司的报价,因为人们会通过纯粹的运气而不是技巧来通过面试。这意味着那里的开发人员不能保证熟练,除非你可以教你的老年人技能,否则你可能会过得很糟糕。

【讨论】:

【参考方案2】:
int[] a = 5, 6, 8, 9, 3, 4, 2, 9 ;
int[] b = 5, 6, 8, 9, 3, 6, 1, 9 ;

 for (int i = 0; i < a.Length; i++)
  
     if (a[i] != b[i])
      
       Console.Write("Original Array manipulated at position 0  + "\t\n"  
                             + "and the element is 1 replaced by 2 ", i, 
                             a[i],b[i] + "\t\n" );
       break;               
            
  
   Console.Read();

   ///use break if want to check only one manipulation in original array.
   ///If want to check more then one manipulation in original array, remove break

【讨论】:

请解释您如何假设有两个数组ab【参考方案3】:

这是在 O(n) 时间内使用 hashmap 的简单解决方案。

#include<iostream>
#include<map>
using namespace std;

int main()

    int a[]=1,3,2,7,5,1,8,3,6,10;
    map<int,int> mp;
    for(int i=0;i<10;i++)

        if(mp.find(a[i]) == mp.end())
            mp.insert(a[i],1);
        else
            mp[a[i]]++;
    

    for(auto i=mp.begin();i!=mp.end();++i)
        if(i->second > 1)
            cout<<i->first<<" ";
    


【讨论】:

【参考方案4】:

这可以在 O(n) 时间和 O(1) 空间内完成。 不修改输入数组

    这个想法类似于在链表中查找循环的起始节点。 维护两个指针:快和慢
slow = a[0]
fast = a[a[0]]
    循环直到慢!=快 一旦我们找到循环(慢 == 快) 将慢速重置为零
slow = 0
    找到起始节点
while(slow != fast)
    slow = a[slow];
    fast = a[fast];

    slow 是您的重复号码。

这是一个 Java 实现:

class Solution 
    public int findDuplicate(int[] nums) 
        if(nums.length <= 1) return -1;
        int slow = nums[0], fast = nums[nums[0]]; //slow = head.next, fast = head.next.next
        while(slow != fast)            //check for loop
            slow = nums[slow];
            fast = nums[nums[fast]];
        
        if(slow != fast) return -1;
        slow = 0; //reset one pointer
        while(slow != fast) //find starting point of loop
            slow = nums[slow];
            fast = nums[fast];
        
        return slow;
    

【讨论】:

我认为if(slow != fast) return -1; 是多余的。它永远不会是真的,循环将无限运行。【参考方案5】:

如上所述,

你有一个从 0 到 n-1 的数字数组,其中一个数字是 删除,并替换为数组中已有的数字,这使得 该号码的副本。

我假设数组中的元素除了重复项之外都已排序。如果是这种情况,我们可以轻松实现以下目标:

        public static void main(String[] args) 
    //int arr[] =  0, 1, 2, 2, 3 ;
    int arr[] =  1, 2, 3, 4, 3, 6 ;
    int len = arr.length;
    int iMax = arr[0];
    for (int i = 1; i < len; i++) 
        iMax = Math.max(iMax, arr[i]);
        if (arr[i] < iMax) 
            System.out.println(arr[i]);
            break;
        else if(arr[i+1] <= iMax) 
            System.out.println(arr[i+1]);
            break;
        
    

O(n) 时间和 O(1) 空间;请分享您的想法。

【讨论】:

【参考方案6】:

这是O(n) 时间和O(1) 空间的替代解决方案。它类似于rici's。我发现它更容易理解,但在实践中,它会溢出更快。

X 为缺失的数字,R 为重复的数字。

    我们可以假设这些数字来自[1..n],即不出现零。实际上,在遍历数组时,我们可以测试是否找到了零,如果没有找到则立即返回。

    现在考虑:

    sum(A) = n (n + 1) / 2 - X + R
    
    product(A) = n! R / X
    

其中product(A)A 中所有元素跳过零的乘积。我们有两个未知数的两个方程,XR 可以代数推导出来。

编辑:根据大众的需求,这里有一个成功的例子:

让我们设置:

S = sum(A) - n (n + 1) / 2
P = n! / product(A)

那么我们的方程变成:

R - X = S
X = R P

可以解决:

R = S / (1 - P)
X = P R = P S / (1 - P)

例子:

A = [0 1 2 2 4]

n = A.length - 1 = 4
S = (1 + 2 + 2 + 4) - 4 * 5 / 2 = -1
P = 4! / (1 * 2 * 2 * 4) = 3 / 2

R = -1 / (1 - 3/2) = -1 / -1/2 = 2
X = 3/2 * 2 = 3

【讨论】:

【参考方案7】:

@rici 关于时间和空间的使用是正确的:“这可以在 O(n) 时间和 O(1) 空间内完成。”

但是,这个问题可以扩展到更广泛的要求:没有必要只有一个重复的数字,并且数字可能不连续。

OJ 是这样说的 here: (注3显然可以缩小)

给定一个包含 n + 1 个整数的数组 nums,其中每个整数都介于 1 和 n(含)之间,证明至少存在一个重复数。假设只有一个重复号码,找到重复号码。

注意:

不得修改数组(假设数组是只读的)。 您只能使用常量,O(1) 额外空间。 您的运行时复杂度应小于 O(n2)。 数组中只有一个重复的数字,但可以重复多次。

这个问题非常由 Keith Schwarz 用Floyd's cycle-finding 算法很好地解释和回答了here:

我们需要用来解决这个问题的主要技巧是注意,因为我们有一个包含从 0 到 n - 2 的 n 个元素的数组,我们可以将数组视为从集合 0 , 1, ..., n - 1 到自身上。该函数由 f(i) = A[i] 定义。鉴于此设置,重复值对应于一对索引 i != j 使得 f(i) = f(j)。因此,我们的挑战是找到这对 (i, j)。一旦我们有了它,我们只需选择 f(i) = A[i] 即可轻松找到重复值。

但是我们如何找到这个重复值呢?事实证明,这是计算机科学中一个经过充分研究的问题,称为循环检测。问题的一般形式如下。我们得到一个函数 f。将序列 x_i 定义为

    x_0     = k       (for some k)
    x_1     = f(x_0)
    x_2     = f(f(x_0))
    ...
    x_n+1 = f(x_n)

假设 f 从域映射到自身,则此函数将具有三种形式之一。首先,如果域是无限的,那么序列可以是无限长且不重复的。例如,整数上的函数 f(n) = n + 1 具有此属性 - 没有数字是重复的。其次,该序列可能是一个闭环,这意味着存在一些 i 使得 x_0 = x_i。在这种情况下,序列无限期地循环通过一些固定的值集。最后,序列可以是“rho 形”。在这种情况下,序列看起来像这样:

 x_0 -> x_1 -> ... x_k -> x_k+1 ... -> x_k+j
                    ^                       |
                    |                       |
                    +-----------------------+

也就是说,序列从一个元素链开始,进入一个循环,然后无限循环。我们将在序列中到达的循环的第一个元素表示为循环的“入口”。

也可以找到一个python实现here:

def findDuplicate(self, nums):
    # The "tortoise and hare" step.  We start at the end of the array and try
    # to find an intersection point in the cycle.
    slow = 0
    fast = 0

    # Keep advancing 'slow' by one step and 'fast' by two steps until they
    # meet inside the loop.
    while True:
        slow = nums[slow]
        fast = nums[nums[fast]]

        if slow == fast:
            break

    # Start up another pointer from the end of the array and march it forward
    # until it hits the pointer inside the array.
    finder = 0
    while True:
        slow   = nums[slow]
        finder = nums[finder]

        # If the two hit, the intersection index is the duplicate element.
        if slow == finder:
            return slow

【讨论】:

请注意,这实际上并不是 100% 有效,特别是如果 nums[0] = 0 会损坏它。要修复它,您必须首先找到第一个不会自行循环的索引。【参考方案8】:

遍历数组,检查array[abs(array[i])]的符号,如果为正则为负,如果为负则打印,如下:

import static java.lang.Math.abs;

public class FindRepeatedNumber 

    private static void findRepeatedNumber(int arr[]) 
        int i;
        for (i = 0; i < arr.length; i++) 
            if (arr[abs(arr[i])] > 0)
                arr[abs(arr[i])] = -arr[abs(arr[i])];
            else 
                System.out.print(abs(arr[i]) + ",");
            
        
    

    public static void main(String[] args) 
        int arr[] =  4, 2, 4, 5, 2, 3, 1 ;
        findRepeatedNumber(arr);
    

参考:http://www.geeksforgeeks.org/find-duplicates-in-on-time-and-constant-extra-space/

【讨论】:

上述解决方案仅适用于非负整数,int arr[] = 4, 2, 4, 5, -2, 3, 1 ; 将失败并打印 4,4,2,这是错误的,它应该只打印 4。 int numRay[] = 0, 800, 300, 2, 7, 800, 2, 300, 1; 当我尝试这个也不起作用。【参考方案9】:
    对数组排序 O(n ln n)

    使用滑动窗口技巧遍历数组O(n)

    空间是 O(1)

    Arrays.sort(input);
    for(int i = 0, j = 1; j < input.length ; j++, i++)
        if( input[i] == input[j])
            System.out.println(input[i]);
            while(j < input.length && input[i] == input[j]) j++;
            i = j - 1;
        
    
    

测试用例 int[] 1, 2, 3, 7, 7, 8, 3, 5, 7, 1, 2, 7

输出 1、2、3、7

【讨论】:

n log n 不是n ln nln 是自然对数(以 e 为底的对数),而 CS 文献中的 log 是以 2 为底的对数,除非指定了底数。【参考方案10】:

这个程序是基于c#的,如果你想用另一种编程语言来做这个程序,你必须首先按升序更改数组并将第一个元素与第二个元素进行比较。如果相等,则找到重复的数字。程序是

int[] array=new int[]1,2,3,4,5,6,7,8,9,4;
Array.Sort(array);
for(int a=0;a<array.Length-1;a++)

  if(array[a]==array[a+1]
  
     Console.WriteLine("This 0 element is repeated",array[a]);
   

Console.WriteLine("Not repeated number in array");

【讨论】:

【参考方案11】:

我们可以有效地使用 hashMap:

Integer[] a = 1,2,3,4,0,1,5,2,1,1,1,;
HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
for(int x : a)

    if (map.containsKey(x))  map.put(x,map.get(x)+1);
    else map.put(x,1);


Integer [] keys = map.keySet().toArray(new Integer[map.size()]);
for(int x : keys)

    if(map.get(x)!=1)
    
        System.out.println(x+" repeats : "+map.get(x));
    

【讨论】:

【参考方案12】:

//这类似于HashSet的方法,但只使用一种数据结构:

    int[] a =  1, 4, 6, 7, 4, 6, 5, 22, 33, 44, 11, 5 ;

    LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>();

    for (int i : a) 
        map.put(i, map.containsKey(i) ? (map.get(i)) + 1 : 1);
    

    Set<Entry<Integer, Integer>> es = map.entrySet();
    Iterator<Entry<Integer, Integer>> it = es.iterator();

    while (it.hasNext()) 
        Entry<Integer, Integer> e = it.next();
        if (e.getValue() > 1) 
            System.out.println("Dupe " + e.getKey());
        
    

【讨论】:

【参考方案13】:
  public void duplicateNumberInArray 
    int a[] = new int[10];
    Scanner inp = new Scanner(System.in);
    for(int i=1;i<=5;i++)  
        System.out.println("enter no. ");
        a[i] = inp.nextInt();
    
    Set<Integer> st = new HashSet<Integer>();
    Set<Integer> s = new HashSet<Integer>();
    for(int i=1;i<=5;i++)          
        if(!st.add(a[i]))
            s.add(a[i]);
        
    

    Iterator<Integer> itr = s.iterator();
                System.out.println("Duplicate numbers are");
    while(itr.hasNext())
        System.out.println(itr.next());
    

首先使用 Scanner 类创建一个整数数组。然后遍历数字循环并检查是否可以将数字添加到集合中(仅当该特定数字不应该在集合中时才可以将数字添加到集合中,这意味着集合不允许重复的数字添加并返回布尔值vale FALSE 添加重复值)。如果没有。不能添加意味着它是重复的,所以将该重复的数字添加到另一个集合中,以便我们稍后打印。请注意我们正在将重复数字添加到集合中,因为重复数字可能会重复多次,因此只添加一次。最后我们使用迭代器打印集合。

【讨论】:

【参考方案14】:

这可以在O(n) 时间和O(1) 空间内完成。

(该算法之所以有效,是因为数字是已知范围内的连续整数):

在一次遍历向量中,计算所有数字的总和,以及所有数字的平方和。

N(N-1)/2 中减去所有数字的总和。打电话给A

N(N-1)(2N-1)/6 中减去平方和。除以A。调用结果B

被删除的号码是(B + A)/2,被替换的号码是(B - A)/2

示例:

向量是[0, 1, 1, 2, 3, 5]:

N = 6

向量之和为 0 + 1 + 1 + 2 + 3 + 5 = 12。N(N-1)/2 为 15。A = 3。

平方和是 0 + 1 + 1 + 4 + 9 + 25 = 40。N(N-1)(2N-1)/6 是 55。B = (55 - 40)/A = 5。

删除的数字是 (5 + 3) / 2 = 4。

它被替换的数字是 (5 - 3) / 2 = 1。

为什么有效:

原向量[0, ..., N-1]之和为N(N-1)/2。假设值a 被删除并替换为b。现在修改后的向量的总和将是N(N-1)/2 + b - a。如果我们从N(N-1)/2 中减去修改后的向量的总和,我们得到a - b。所以A = a - b

同样,原始向量的平方和为N(N-1)(2N-1)/6。修改后的向量的平方和为N(N-1)(2N-1)/6 + b<sup>2</sup> - a<sup>2</sup>。从原始和中减去修改向量的平方和得到a<sup>2</sup> - b<sup>2</sup>,与(a+b)(a-b) 相同。所以如果我们将它除以a - b(即A),我们得到B = a + b

现在B + A = a + b + a - b = 2aB - A = a + b - (a - b) = 2b

【讨论】:

太棒了!在您的假设中,N = 1 + 数组中的最高键。如果 N = 最高键,则方程为 N(N+1)/2 和 N(N+1)(2N+1)/6。 如果数字是连续整数,只检查前后的数字即可。如果您正在查看的内容有问题,那么您有答案。如果它是有序的但等于它的继任者的前任,你有你的副本。为什么需要所有花哨的数学? @imray:假设数组没有排序。 @rici 好的,如果你在你的例子中打乱了数组,那会更清楚 为什么内存不是O(n)?存储数字总和所需的内存随着 n 的增长而增长......【参考方案15】:
public class FindDuplicate 
    public static void main(String[] args) 
        // assume the array is sorted, otherwise first we have to sort it.
        // time efficiency is o(n)
        int elementData[] = new int[]  1, 2, 3, 3, 4, 5, 6, 8, 8 ;
        int count = 1;
        int element1;
        int element2;

        for (int i = 0; i < elementData.length - 1; i++) 
            element1 = elementData[i];
            element2 = elementData[count];
            count++;
            if (element1 == element2) 
                System.out.println(element2);
            
        
    

【讨论】:

【参考方案16】:

您可以在 O(N) 时间内完成,无需任何额外空间。以下是算法的工作原理:

按以下方式遍历数组:

    对于遇到的每个元素,将其对应的索引值设置为负数。 例如:如果你发现 a[0] = 2。得到 a[2] 并否定该值。

    通过这样做,您可以将其标记为遇到。既然你知道你不能有负数,你也知道你是否定它的人。

    检查与该值对应的索引是否已标记为负,如果是,则您得到重复的元素。例如:如果 a[0]=2 ,转到 a[2] 并检查它是否为负数。

假设你有以下数组:

int a[]  = 2,1,2,3,4;

在第一个元素之后,您的数组将是:

int a[] = 2,1,-2,3,4;

在第二个元素之后,您的数组将是:

int a[] = 2,-1,-2,3,4;

当您到达第三个元素时,您转到 a[2] 并看到它已经为负数。你得到了副本。

【讨论】:

我认为如果我们在一个数组中得到两个 0,那么逻辑就不会起作用。例如,数组是 0,1,2,3,4,5,其中 3 替换为 0。 如果其中一个元素是 10,000 而不是 4 怎么办?它将超出数组的范围。 上述解决方案即使对 0 也有效,除了将元素一一设置为负值外,您还必须跟踪 0。算法时间为 O(n),空间为 O(1)。如果数组是无符号整数,解决方案可能不起作用。 @apadana 这个答案只在input array is 0 -&gt; n - 1 where integers between 0 and n时有效 仍然不清楚空间是否为 O(1),因为您从数组中借用了 O(N) 空间。【参考方案17】:

我们有原始数组int A[N]; 也创建第二个数组bool B[N],类型为bool=false。迭代第一个数组并设置B[A[i]]=true if 为 false,否则 bing!

【讨论】:

好的,我明白了,但它只适用于数字的整数值。 @parsifal 是的,你是对的,我知道这一点,但在我写的那一刻忘记了。 我同意如果数字是整数。但是,如果 A[] 中的数字不是整数怎么办?我认为哈希表在这种一般情况下效果很好。 @bitfox 即使 A[] 是整数,如果 A[] 中的任何整数大于 N,该算法也不起作用。 @qchen 如原始问题所述:“您有一个从 0 到 N-1 的数字数组”。在 A 数组中可以找到的最大值是 N。因此,使用 a[A[i]] 时不会出现数组索引错误。【参考方案18】:
int a[] = 2,1,2,3,4;

int b[] = 0;

for(int i = 0; i < a.size; i++)


    if(a[i] == a[i+1])
    
         //duplicate found
         //copy it to second array
        b[i] = a[i];
    

【讨论】:

这错了!如果你想这样做,你需要再做一次 for 循环,那就是 o(N^2)【参考方案19】:

您可以按照以下方式进行:

    使用线性时间排序算法(例如计数排序)对数组进行排序 - O(N) 扫描已排序的数组并在两个连续元素相等时立即停止 - O(N)

【讨论】:

【参考方案20】:

一个可行的解决方案:

假设数字是整数

创建一个 [0 .. N] 的数组

int[] counter = new int[N];

然后迭代读取并递增计数器:

 if (counter[val] >0) 
   // duplicate
  else 
   counter[val]++;
 

【讨论】:

好的。你有我的+1。从 Q 开始,我们需要 TIME O(N),而不需要内存。布尔值可以是 1 个字节 ***.com/a/383597/1458030。并且 BitSet() 需要一些额外的技巧(直接或间接)来设置正确的位..【参考方案21】:

我建议使用 BitSet。我们知道 N 对于数组索引来说足够小,所以 BitSet 的大小是合理的。

对于数组的每个元素,检查与其值对应的位。如果已经设置,那就是副本。如果没有,请设置该位。

【讨论】:

@qPCR4vir 不一样,它只使用解决方案内存的 1/8(带布尔值)【参考方案22】:

扫描数组 3 次:

    对所有数组元素进行异或运算 -> A。对从 0 到 N-1 的所有数字进行异或运算 -> B。现在是A XOR B = X XOR D,其中 X 是删除的元素,D 是重复的元素。 选择A XOR B 中的任何非零位。 对所有设置了该位的数组元素进行异或运算 -> A1。将设置此位的 0 到 N-1 的所有数字进行异或运算 -> B1。现在要么A1 XOR B1 = X要么A1 XOR B1 = D。 再次扫描阵列并尝试找到A1 XOR B1。如果找到,这是重复元素。如果不是,则重复元素为A XOR B XOR A1 XOR B1

【讨论】:

扫描数组 3 次需要 O(3n) 时间,而不是 OP 提到的 O(n)。 @prashant O(3n) 与 O(n) 相同——大 O 表示法中的系数被忽略。【参考方案23】:

使用HashSet 来保存所有已经看到的数字。它在(摊销)O(1) 时间运行,因此总数为O(N)

【讨论】:

那不是真的,插入HashSet不是O(1),如果数字范围e远大于hashTable的大小 @AlexWien - 你看到我说的“摊销”了吗? rehash 不依赖于数组中元素的数量,因此仍被视为O(1)。另外,您可以预先设置集合的大小,这样您就根本不需要重新散列。当然,如果您有Long.MAX_VALUE 项目,由于表上的物理限制,您最终会在添加/检索时得到O(N) 语义,但大多数人不考虑这一点(就像他们不考虑快速排序一样在最坏的情况下具有O(N^2) 行为)。 或者,引用Cormen et al(强调添加):“在合理假设下,在哈希表中搜索元素的平均时间为 O(1)。 " 在这种情况下,数字范围相对较小,因此不会比表格大小高多少。预计不会重新散列。 谁说数字范围很小?在这种情况下,不要考虑并使用数组变体(bool array int[])如果 N 很大,则哈希集将比数组更早耗尽内存【参考方案24】:

使用哈希表。在哈希表中包含一个元素是 O(1)。

【讨论】:

如果桶分布良好,平均值为 O(1)。我们不需要处理表扩展,因为所需的最大大小在 HashSet 构建时是已知的。但是,我怀疑问题是要达到 O(N) 最坏情况,而不是 O(N) 平均值。 在哈希表中使用整数作为它们自己的哈希意味着哈希永远不会发生冲突。但是哈希表在这个问题中基本上只是一个复杂的位数组。

以上是关于在 O(n) 时间内找到数组中的重复元素的主要内容,如果未能解决你的问题,请参考以下文章

在 O(1) 空间中的数组中查找重复元素(数字不在任何范围内)

剑指offer_查找数组中的任一重复元素

在线性时间内创建图案化阵列 [重复]

在小于线性的时间内,在排序数组中找到重复项

如何在 C 或 C++ 中以 O(n) 删除数组中的重复元素?

设计一个 O(n) 算法来找到一个不在 [0,n-1] 范围内的数字 [重复]