【问题标题】:find the duplicate number in an array which has no duplicates except for one number在除一个数字外没有重复的数组中找到重复的数字
【发布时间】:2014-11-07 10:55:04
【问题描述】:

假设有一个元素数组,除了 1 个数字之外没有重复,

ex. 1,2,13,4,7,11,2,6

如何高效查找重复号码? 我们可以在 O(n) 时间内使用哈希表 (HT) 并使用 O(n) 空间来完成,如下所示。

if(HT.Contains(item)) -> this is the duplicate
else
ht.add(item)

在空间和时间复杂度方面有没有更好的方法?

注意:这个问题不是以下两个问题的重复,它们是不同的。

如果整数是连续的,则可以使用此链接中的解决方案how-to-find-a-duplicate-element-in-an-array-of-shuffled-consecutive-integers

如果n个元素的数组包含从0到n-1的元素,只有这个链接有解决方案Finding duplicates in O(n) time and O(1) space

【问题讨论】:

  • 这些数字足够小(并且全部 >= 0),可以作为单个短整数中的位。那么,不是哈希,而是 set —— 而且,稍微设置一下,您可以有效地测试每个新项目。
  • 数组有多大,数组中项的范围是多少?它是一个任意大的整数数组吗?或者它是一个小值的小数组?
  • @JimMischel 这是任意大的数组,我们无法对数组进行排序。
  • 如果不能对数组进行排序,那么哈希表似乎是唯一合理的解决方案。
  • @Falmarri:链接中的问题仅将输入限制为 0 到 n-1,这使问题变得更容易。这里问的不一样

标签: c arrays algorithm data-structures


【解决方案1】:

我不认为你可以做得比 O(n) 时间复杂度更好 - 在最坏的情况下,你将不得不触摸数据集中的每个元素才能找到重复项

改善空间消耗的一种方法(以需要一些位旋转以及两次遍历数据集为代价)是使用Bloom Filter。这个想法是对数据集进行第一次传递:如果您发现可能的重复项,则将其从数据集中删除并将其添加到哈希表中(如果布隆过滤器功能正确,那么只有大约 1% 的元素会被标记尽可能重复)。然后对过滤后的数据集进行第二次遍历,根据可能重复的小哈希表测试元素。

我的代码将使用 Java,因为它是我最熟悉的语言。

Class DupFinder {
  BloomFilter filter = new BloomFilter();
  HashTable hashTable = new HashTable();
  int start = 0;

  int run(int[] dataset) {
    // first pass
    for(int i = 0; i < dataset.length; i++) {
      if(filter.contains(dataset[i]) {
        // check if element is in hashTable, else add it
        if(hashTable.contains(dataset[i]) {
          return dataset[i]; // duplicate found
        } else {
          hashTable.add(dataset[i]);
        }

        // remove element from dataset
        int temp = dataset[start];
        dataset[start] = dataset[i];
        dataset[i] = temp;
        start++;
      } else filter.add(dataset[i]);
    } 

    // second pass
    for(int i = start; i < dataset.length; i++) {
      if(hashTable.contains(dataset[i]) {
        return dataset[i]; // duplicate found
      }
    }
    return NULL; // no duplicate found
  }
}

哈希表的替代方法是使用Radix Sort,这是一种线性时间排序算法。基数排序将具有更好的最坏情况性能(基数排序为 O(n),与哈希表的 O(n^2) 相比,在您遇到荒谬数量的冲突的不太可能的情况下)但平均情况性能较差(哈希表实现通常会在只扫描一半数据集后找到重复项,而基数排序总是需要对数据集进行多次遍历)。如果您为存储桶使用空间高效的数据结构,则基数排序在空间消耗方面也将稍微更有效,例如一个分块列表。

您可以并行化哈希表实现,而不会产生过多的同步开销。使用 t 个线程,每个线程将处理数据集的 n/t 个元素(例如,如果数据集中有 32 个元素和 2 个线程,则 thread0 处理元素 0-15 thread1 处理元素 16-31),将每个元素放入索引为 absoluteValue(x modulo t) 的桶中。在此之后,每个线程将负责处理具有给定存储桶索引的所有元素,例如thread0 将处理索引为 0 的所有存储桶。我使用 BlockingQueue 进行同步 - 这允许线程在队列上调用 take(),从而导致线程删除第一个元素排队或阻塞,直到元素可用;所有其他数据结构都是线程本地的。您需要初始化 dupFinders 变量,以便 DupFinder 实例出现在每个 DupFinder 的 dupFinders 变量的相同索引中(例如 thread0 始终出现在第 0 个索引中,从而确保其 BlockingQueue 中的所有元素都具有 absoluteValue(x modulo t) == 0).

Class DupFinder implements Callable<Integer> {
  private Class Chunk {
    int size = 0;
    int chunk = new int[64];

    boolean add(int x) {
      if(size < 64) {
        chunk[size] = x;
        size++;
        return true;
      } else return false;
    }
  }

  int t = ??? // number of threads
  private BlockingQueue<Stack<Chunk>> queue = new LinkedBlockingQueue()
  private DupFinder[] dupFinders = new DupFinder[t];
  private Stack<Chunk>[] stacks = new Stack<Chunk>[t];

  void add(Stack<Chunk> stack) {
    queue.add(stack);
  }

  // the thread only receives n/t elements of the dataset
  int call(int[] partialDataset) {
    // partition dataset elements by their modulus(t)
    for(int i = 0; i < partialDataset.length; i++) {
      tempStack = stacks[Math.absoluteValue(partialDataset[i] modulo t)];
      if(!tempStack.peek().add(partialDataset[i])) {
        Chunk chunk = new Chunk();
        chunk.add(partialDataset[i]);
        tempStack.push(chunk);
      } 
    }

    // distribute chunk stacks to the appropriate threads
    for(int i = 0; i < t; i++) {
      dupFinders[i].add(stacks[i]);
    }

    HashTable hashTable = new HashTable();
    for(int i = 0; i < t; i++) {
      // wait for a chunk stack to become available
      Stack<Chunk> tempStack = queue.take();
      while(!tempStack.isEmpty) {
        tempChunk = tempStack.pop();
        for(int i = 0; i < tempChunk.size; i++) {
          if(hashTable.contains(tempChunk.chunk[i]) {
            return tempChunk.chunk[i]; // duplicate found
          } else {
            hashTable.add(tempChunk.chunk[i]);
          }
        }
      }
    }
    return NULL; // no duplicate found
  }
}

【讨论】:

    【解决方案2】:

    单个位的操作非常耗时(例如:获取字、获取/设置 1 位、设置字),与字操作(获取/设置字)相比。

    如果您知道 MIN_VALUE >=0,还知道 MAX_VALUE 并且它足够小,您可以执行 Jongware 建议的类似操作 - 哈希表,但不能基于位:哈希值就是那个值。

    #include <stdio.h>
    #include <string.h>
    
    #define MAX_VALUE 13 +1 // +1 so we don't have do -1 in for loop
    
    main()
    {
        int i;
        int array[] = { 1,2,13,4,7,11,2,6 };
        int array_size = sizeof(array) / sizeof(array[0]);
    
        short flags[MAX_VALUE] = { 0 };
        for (i = 0; i < array_size; ++i) {
              if (++flags[ array[i] ] != 1) {
                  printf ("duplicated %d on %d\th position", array[i], i);
              }
        }
    }
    

    而且它也不需要为每个元素计算哈希。

    【讨论】:

    • max_value 将始终更大/近似。等于元素的数量。
    猜你喜欢
    • 2019-08-10
    • 2021-06-24
    • 1970-01-01
    • 1970-01-01
    • 2023-03-10
    • 1970-01-01
    • 2016-01-17
    • 2015-06-14
    • 1970-01-01
    相关资源
    最近更新 更多