【问题标题】:Fast ways to remove duplicates from a list of integer vectors从整数向量列表中删除重复项的快速方法
【发布时间】:2013-03-31 09:53:33
【问题描述】:

假设我们有一个函数,它返回一百万个长度为 30 的整数向量,每个向量都有较小的条目(例如,介于 -100 和 100 之间)。进一步假设输出只有大约 30000 个唯一向量,其余的都是重复的。检索唯一输出向量列表的良好数据结构和算法是什么?当 3% 的唯一向量的比例大致恒定时,该解决方案最好能很好地扩展。

这个问题主要是关于数据结构的,但我计划使用 STL 在 C++ 中实现它,所以也欢迎任何关于实现的提示。

  • 朴素算法是存储已知向量的列表(可能按字典顺序排序)。当一个新向量到达时,我们可以使用循环检查它是否已经在列表中(或在排序列表中搜索)。
  • 散列:假设向量存储在 C 数组中。什么是整数向量的好散列函数?我看到的一个缺点是每个向量的每个组件都至少被触摸一次。这似乎已经太多了。
  • 任何树形数据结构都可以吗?例如,我们可以将所有可见向量的第一个分量中的值存储为根,然后将第二个分量中的值存储为它们的子项,...

我没有计算机科学背景。我也很乐意提供一些文献资料,让我可以学习如何处理这些问题。

【问题讨论】:

  • 这些完全重复是否意味着即使重复向量中的顺序也是相同的?这些向量的长度都是30吗?
  • @AlexeyFrunze 我不完全理解;向量的顺序是什么?
  • @G.Bach 不属于,属于。向量中的数字顺序被视为重复。
  • @AlexeyFrunze 是的,在您的术语中它们是“完全重复的”,并且它们的长度都相同。例如 (-1,2,3) 不是 (-1,3,2) 的重复项,两者都必须保留。
  • 散列让您决定内存与速度的权衡,这很好。我认为您无法避免“至少触摸每个元素一次”。

标签: c++ algorithm sorting unique


【解决方案1】:

您提出的建议有时被称为后备表;一种 用于各种查找目的的辅助表。在你的情况下, 你有许多不同的可能方式来组织这个 桌子。最明显的是不组织它,使用线性 搜索下一个元素是否已知。由于 表最终将包含大约 30000 个元素,即 可能不是一个好主意。来自标准库(至少 在 C++11) 中,有两种可能性:std::setstd::unordered_setstd::set 使用某种形式的平衡 树,因此对每个树最多进行 lg n 次比较 查找(对于 30000 个元素,大约 15 个); std::unordered_set 是一个 哈希表,并具有良好的哈希函数,将需要尽可能小 恒定数量的比较:你应该能够得到它 平均下降到 2 以下(但可能要付出更多的代价 内存——负载因子越低,概率越小 碰撞)。正如你提到的,你确实有额外的费用 计算哈希函数,正如你所指出的,这 确实涉及访问向量中的每个元素;在二进制 树,每次比较所需的只是足够 元素被比较以确定顺序——在许多情况下, 那可能只是一两个。 (但如果你说有一个 很多重复...在您检测到重复之前,您无法检测到重复 访问了所有 30 个条目,因为任何一个都可能不同。)唯一的方法 要知道哪种解决方案实际上更快是衡量 两者都使用典型数据;对于您描述的数据集 (很多重复),我怀疑哈希表会赢,但它是 远未确定。

最后,您可以使用某种非二叉树。如果你可以的话 真正将值限制在特定范围内(例如 -100..100), 您可以使用带有指针的普通向量或数组 子节点,直接用元素值索引,转置 有必要的。然后,您只需沿着树走,直到找到 一个空指针,或者你到达终点。最大深度 树将是 30,实际上,每个元素将是 30 深,但是 通常,您会在获取之前发现该元素是唯一的 这么深。我怀疑(但同样,你需要测量) 在你的情况下,有很多重复,这实际上是 明显慢于前两个建议。 (而且它 你会做更多的工作,因为我不知道 任何现有的实现。)

至于散列,几乎是任何形式的线性全等散列 应该足够了:例如 FNV。大部分的 此类哈希的文档涉及字符串(数组 char),但它们往往与任何积分一起工作得一样好 类型。我通常使用类似的东西:

template <typename ForwardIterator>
size_t
hash( ForwardIterator begin, ForwardIterator end )
{
    size_t results = 2166136261U 
    for ( ForwardIterator current = begin; current != end; ++ current ) {
        results = 127 * results + static_cast<size_t>( *current );
    }
    return results;
}

我选择127 作为乘数很大程度上是基于速度 旧系统:乘以 127 比大多数系统快得多 其他可以产生良好结果的值。 (我不知道 这是否仍然是真的。但是乘法仍然是 在许多机器上运行相对较慢,并且编译器 如果那样的话,会将127 * x 转换为x &lt;&lt; 7 - x 更快。)使用上述算法的分布约为 和 FNV 一样好,至少在我拥有的数据集上 经过测试。

【讨论】:

  • 感谢您的回答。我玩了一下,哈希表比我预期的要慢得多。哈希函数的计算量大约是 30 次比较的 3 倍,后者是 trie 中最坏情况的查找。在存储桶中搜索时,std::unordered_map 还会进行几次 30 循环比较。我想第二个问题可以通过优化桶数等来解决。到目前为止,trie 速度要快得多。
  • 好的,我现在可以使用 stl::unordered_set 快速运行它。我犯了一些实现错误。再次感谢。
【解决方案2】:

基数映射是理想的,但您需要实现它,因为标准库中没有实现。

【讨论】:

  • 理想吗?在他的例子中,这意味着深度为 30,而对于 30000 个不同的值,平衡的 B 树的深度约为 15。(当然,比较更便宜:保证你只看一个元素每个级别,就像在 B 树中一样,可以肯定的是,有时您必须查看多个。不过,您必须查看的元素数量可能并不那么重要,因为在第一个之后,所有其余的都在缓存中。)
【解决方案3】:

计算第一个向量中值的 CRC 表示。您现在有一个代表您的 30 个值的数字。相对于其余向量,该数字可能是唯一的,但不能保证。

将 CRC 值作为键,将一个指向实际向量的指针插入到多映射 {CRC, VectorPointer} 中。

现在为每个剩余的向量计算 CRC,并在多重映射中查找。

如果找不到,请插入 {CRC, VectorPointer}。如果确实找到了,请遍历匹配项并比较数据元素以确定它是否相同。如果是丢弃新向量。如果不是,则插入 {CRC, VectorPointer}。

冲洗并重复,直到处理完所有 30,000 个向量。

您在多重地图中拥有可迭代的唯一集合。

【讨论】:

  • 这基本上是用树散列。您可以通过使用在每个存储桶中存储完整哈希值并在比较键之前比较完整哈希值的哈希容器获得类似的结果。
  • @brianbeuning 计算 CRC 和计算哈希码之间有一个区别——计算 CRC 的成本要高得多。
【解决方案4】:

假设你有 N 个长度为 K 的向量,其中只有 M 个是唯一的。

  • 哈希 + 哈希图

您可以在 O(K) 时间内计算每个向量的哈希,检查您的哈希图中是否已经有这样的向量,并在 O(1) 时间内插入新向量。对于散列函数,您可以简单地使用不带模数的多项式散列,只需将散列存储为 64 位类型并忽略溢出。实现非常简单,它将在需要 O(M*K) 内存的 O(N*K) 时间内工作。如果需要先对元素进行排序,时间会是O(N*K*log(K))

  • 基数树

我认为你不应该在这里使用基数树,因为你仍然需要查看每个向量的每个元素。之所以如此,是因为如果您在树中没有这样的向量,则需要插入其所有元素,如果您有这样的向量,则需要下到树的叶子才能看到你以前真的插入过这样的向量。所以渐近线保持不变,但你需要自己实现树,这不是一个好主意:)


看起来很容易表明您至少需要阅读向量的所有元素。之所以如此,是因为在每一刻你都有两种可能性——你之前已经找到了当前向量,你需要读取它的所有元素到最后来识别它,或者你之前没有找到当前向量,你需要读取它的所有元素排序并保存它们。然而,如果向量已经排序,您将只需要读取第一个不匹配的元素。但是让我们假设前 30000 个向量是唯一的,那么无论您将使用什么算法或数据结构,您都需要读取所有其他向量到最后以确定它们不是唯一的。最后我们知道你需要阅读几乎所有的向量到最后:)

如果您的值确实在范围内 (-100, 100) 并且向量中只有 30 个值,您会注意到此类向量可以保存为四个 64 位整数,因为您只有 8*30 = 240 位数据在里面。但这只是另一个想法,我认为使用它的任何实现都不会比 hashing + hashmap 更快。

【讨论】:

  • 我同意大多数时候我必须进行 30 次比较才能发现向量已经存在。但是,计算哈希函数需要比 30 次比较长得多的时间。在我的实验中,trie 是迄今为止最快的。
  • 哇。散列需要每个符号一次加法和一次乘法。使用 trie 时需要使用指针进行一些工作,除非您使用数组实现 trie。所以你说一加一乘比从内存中取东西快?
【解决方案5】:

哈希:...我看到的一个缺点是每个向量的每个组件都至少被触摸一次。这似乎已经太多了。

在最坏的情况下,你怎么能比较两个向量而不看至少一次呢?不,真的,如果你有 1,1,1 和 2,2,2 比较/匹配立即结束。但是如果你有 1,2,3 和 1,2,3 呢?

无论如何,这是解决问题的一种方法。实施肯定可以改进。

#include <iostream>
#include <map>
#include <vector>
#include <list>
#include <cstdint>
#include <cstdlib>
#include <ctime>

using namespace std;

const int TotalVectorCount = 1000000;
const int UniqueVectorCount = 30000;
const int VectorLength = 30;

typedef vector<int> Vector;

typedef unsigned long long uint64;

void GenerateRandomVector(Vector& v)
{
  v.reserve(VectorLength);
  // generate 30 values from -100 to +100
  for (int i = 0; i < VectorLength; i++)
    v.push_back(rand() % 201 - 100);
}

bool IdenticalVectors(const Vector& v1, const Vector& v2)
{
  for (int i = 0; i < VectorLength; i++)
    if (v1[i] != v2[i])
      return false;

  return true;
}

// this lets us do "cout << Vector"
ostream& operator<<(ostream& os, const Vector& v)
{
  for (int i = 0; i < VectorLength; i++)
    os << v[i] << ' ';

  return os;
}

uint64 HashVector(const Vector& v)
{
  // this is probably a bad hash function,
  // but it seems to work nonetheless
  uint64 h = 0x7FFFFFFFFFFFFFE7;
  for (int i = 0; i < VectorLength; i++)
    h = h * 0xFFFFFFFFFFFFFFC5 + v[i];
  return h & 0xFFFFFFFFFFFFFFFF;
}

Vector UniqueTestVectors[UniqueVectorCount];

void GenerateUniqueTestVectors()
{
  map<uint64,char> m;
  for (int i = 0; i < UniqueVectorCount; i++)
  {
    for (;;)
    {
      GenerateRandomVector(UniqueTestVectors[i]);
      uint64 h = HashVector(UniqueTestVectors[i]);

      map<uint64,char>::iterator it = m.find(h);

      if (it == m.end())
      {
        m[h] = 0;
        break;
      }
    }
  }
}

bool GetNextVector(Vector& v)
{
  static int count = 0;
  v = UniqueTestVectors[count % UniqueVectorCount];
  return ++count <= TotalVectorCount;
}

int main()
{
  srand(time(0));

  cout << "Generating " << UniqueVectorCount << " unique random vectors..."
       << endl;
  GenerateUniqueTestVectors();

#if 0
  for (int i = 0; i < UniqueVectorCount; i++)
    cout << UniqueTestVectors[i] << endl;
#endif

  cout << "Generating " << TotalVectorCount << " random vectors with only "
       << UniqueVectorCount << " unique..." << endl;

  map<uint64,list<Vector>> TheBigHashTable;

  int uniqCnt = 0;
  int totCnt = 0;

  Vector v;
  while (GetNextVector(v))
  {
    totCnt++;

    uint64 h = HashVector(v);

    map<uint64,list<Vector>>::iterator it = TheBigHashTable.find(h);

    if (it == TheBigHashTable.end())
    {
      // seeing vector with this hash (h) for the first time,
      // insert it into the hash table
      list<Vector> l;
      l.push_back(v);

      TheBigHashTable[h] = l;
      uniqCnt++;
    }
    else
    {
      // we've seen vectors with this hash (h) before,
      // let's see if we've already hashed this vector
      list<Vector>::iterator it;
      bool exists = false;

      for (it = TheBigHashTable[h].begin();
           it != TheBigHashTable[h].end();
           it++)
      {
        if (IdenticalVectors(*it, v))
        {
          // we've hashed this vector before
          exists = true;
          break;
        }
      }

      if (!exists)
      {
        // we haven't hashed this vector before,
        // let's do it now
        TheBigHashTable[h].push_back(v);
        uniqCnt++;
      }
    }
  }

#if 0
  cout << "Unique vectors found:" << endl;
  map<uint64,list<Vector>>::iterator it;
  for (it = TheBigHashTable.begin();
       it != TheBigHashTable.end();
       it++)
  {
    list<Vector>::iterator it2;
    for (it2 = it->second.begin();
         it2 != it->second.end();
         it2++)
      cout << *it2 << endl;
  }
#endif

  cout << "Hashed " << uniqCnt << " unique vectors out of " << totCnt << " total" << endl;

  return 0;
}

使用 12848 kB RAM 在 1.12 秒内输出 (ideone):

Generating 30000 unique random vectors...
Generating 1000000 random vectors with only 30000 unique...
Hashed 30000 unique vectors out of 1000000 total

现在,唯一向量更少更短也是如此,因此它们可以在控制台中打印:

使用 3040 kB 的 RAM 在 0.14 秒内输出 (ideone):

Generating 10 unique random vectors...
-45 75 1 -71 -83 97 10 -18 89 -10 
-11 60 18 -54 -90 77 19 -90 -7 -31 
-15 -65 -47 88 25 -56 4 39 -20 39 
-64 -14 -37 -13 15 -70 -66 -75 12 73 
-35 -99 32 83 98 -8 59 16 2 -98 
86 37 -63 -62 24 62 -68 78 -50 -38 
17 -64 48 80 -26 -87 61 8 -62 -28 
-70 -47 -27 62 86 -29 -97 44 37 -45 
-4 -28 92 -17 -40 -35 -56 -58 -57 -55 
5 10 -19 -48 -61 5 -35 100 -88 -47 
Generating 1000000 random vectors with only 10 unique...
Unique vectors found:
86 37 -63 -62 24 62 -68 78 -50 -38 
17 -64 48 80 -26 -87 61 8 -62 -28 
5 10 -19 -48 -61 5 -35 100 -88 -47 
-4 -28 92 -17 -40 -35 -56 -58 -57 -55 
-11 60 18 -54 -90 77 19 -90 -7 -31 
-15 -65 -47 88 25 -56 4 39 -20 39 
-35 -99 32 83 98 -8 59 16 2 -98 
-45 75 1 -71 -83 97 10 -18 89 -10 
-64 -14 -37 -13 15 -70 -66 -75 12 73 
-70 -47 -27 62 86 -29 -97 44 37 -45 
Hashed 10 unique vectors out of 1000000 total

【讨论】:

  • 我同意大多数时候我必须进行 30 次比较才能发现向量已经存在。但是,计算散列函数需要比 30 次比较长得多的时间。此外,您的实现将哈希值存储在映射中,该映射返回列表长度中的对数搜索时间...
  • Maps-sorted-trees 可以在几秒钟内更改为 maps-hash-tables,只需将 map 重命名为 unordered_map。运行时间在视觉上得到改善。
猜你喜欢
  • 2019-07-17
  • 2010-12-13
  • 2011-08-10
  • 2019-05-25
  • 1970-01-01
  • 2017-07-11
  • 2017-05-01
  • 2020-04-27
  • 1970-01-01
相关资源
最近更新 更多