从整数向量列表中删除重复项的快速方法

时间:2013-03-31 09:53:34

标签: c++ algorithm sorting unique

假设我们有一个函数返回一百万个长度为30的整数向量,每个向量都带有小条目(比如说在-100和100之间)。进一步假设输出只有大约30000个唯一向量,其余的是重复的。什么是良好的数据结构和算法来检索唯一输出向量列表?优选地,当3%独特载体的比例大致恒定时,溶液应该很好地扩展。

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

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

我没有计算机科学背景。我也很擅长指向文学,我可以学习如何处理这些问题。

5 个答案:

答案 0 :(得分:3)

你提出的建议有时被称为后备表;一个 用于各种查找目的的辅助表。在你的情况下, 你有许多不同的组织方式 表。最明显的是不组织它,并使用线性 搜索以查看下一个元素是否已知。自从 表最终将包含大约30000个元素,即 可能不是一个好主意。从标准库(至少 在C ++ 11)中,有两种可能性:std::setstd::unordered_setstd::set使用某种形式的平衡 树,因此每个最多进行lg n 比较 查找(大约15个,30000个元素); 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 << 7 - x的内容 更快。)使用上述算法的分布是关于 和FNV一样好,至少我的数据集 测试

答案 1 :(得分:1)

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

答案 2 :(得分:1)

计算第一个向量中值的CRC表示。您现在有一个代表30个值的数字。对于其他载体而言,这个数字可能是唯一的,但它并不是保证。

将CRC值作为键,并指向实际向量并将其插入多图{CRC,VectorPointer}。

现在,对于每个剩余的向量计算CRC,并在多图中查找。

如果找不到,请插入{CRC,VectorPointer}。如果找到它,则迭代匹配并比较数据元素以确定它是否相同。如果丢弃新的矢量。如果不是,则插入{CRC,VectorPointer}。

冲洗并重复,直到所有30,000个载体都已处理完毕。

您在multimap中拥有可以迭代的唯一设置。

答案 3 :(得分:1)

假设您有N个长度为K的向量,并且只有M个唯一的向量。

  • 哈希+ hashmap

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

  • 基数树

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


看起来很容易证明你至少需要阅读载体的所有元素。这是因为在每个时刻你都有两种可能性 - 你之前已经找到了当前的向量,你需要将所有元素读到最后才能识别它,或者你之前没有找到当前向量,你需要阅读它的所有元素排序和保存它们。然而,如果向量已经排序,则需要仅在第一个不匹配时读取元素。但是让我们想象一下,前30000个向量是唯一的,然后你需要读取所有其他向量到最后确定它们不是唯一的,无论你使用什么算法或数据结构。最后我们得到你需要阅读几乎所有的向量到最后:)

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

答案 4 :(得分:0)

  

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

在最坏的情况下,你怎么能比较两个向量而不至少看一次?不,真的,如果你有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;
}

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

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

现在,使用更少和更短的唯一向量相同,因此可以在控制台中打印它们:

输出(ideone),0.14秒,使用3040 kB RAM:

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