【问题标题】:How to efficiently compare vectors with C++?如何有效地将向量与 C++ 进行比较?
【发布时间】:2013-06-30 19:38:49
【问题描述】:

我需要针对向量比较函数在 C++ 中进行微优化的建议, 它比较两个向量是否相等,元素的顺序无关紧要。

template <class T>
static bool compareVectors(const vector<T> &a, const vector<T> &b)
{
  int n = a.size();
  std::vector<bool> free(n, true);
  for (int i = 0; i < n; i++) {
    bool matchFound = false;
    for (int j = 0; j < n; j++) {
      if (free[j] && a[i] == b[j]) {
        matchFound = true;
        free[j] = false;
        break;
      }
    }
    if (!matchFound) return false;
  }
  return true;
}

这个功能被大量使用,我正在考虑优化它的可能方法。 你能给我一些建议吗?顺便说一句,我使用 C++11。

谢谢

【问题讨论】:

  • 如果输入向量的大小是编译时间常数,则潜力巨大。然后,使用 bitset 而不是 std::vector&lt;bool&gt; 会更好。如果没有输入的编译时间常数大小,这甚至可以更有效。其他想法:i &lt;= j 还不够,您可以从 i 开始在 j 上循环吗?
  • 我完全不明白你的“免费”矢量的目的是什么。一个有点不相关的想法是,如果在输入此函数之前可以确保数组的顺序,您可能会提高一些效率。
  • @crowder 如果b 中有多个比较相等的元素,您必须为a 中的每个元素找到一个对应的free 向量存储b 中的一个元素是否已经与a 中的一个元素相关联。
  • @user2381422 我知道你说元素的顺序无关紧要,但是可以对元素进行排序/排序吗? (即它们是否具有可比性?)如果是这样,您可以轻松地从 O(n²) 转到 O(n logn)
  • 不管你如何优化它,如果向量的大小不匹配是常见的,在开头添加if (a.size() != b.size()) return false;可能会有所帮助。

标签: c++ c++11


【解决方案1】:

它刚刚意识到这段代码只进行了一种“设置等效性”检查(现在我看到你确实这么说,我真是个糟糕的读者!)。这可以更简单地实现

template <class T>
static bool compareVectors(vector<T> a, vector<T> b)
{
    std::sort(a.begin(), a.end());
    std::sort(b.begin(), b.end());
    return (a == b);
}

您需要包含标题algorithm

如果您的向量始终具有相同的大小,您可能需要在方法的开头添加一个断言:

assert(a.size() == b.size());

如果您曾经错误地执行了不等长的此操作,这将有助于调试您的程序。

否则,如果向量长度不等,则向量不能相同,所以只需添加

if ( a.size() != b.size() )
{
   return false;
}

排序指令之前。这将为您节省大量时间。

这在技术上的复杂性是O(n*log(n)),因为它主要取决于(通常)具有这种复杂性的排序。这比您的 O(n^2) 方法要好,但由于需要副本,可能会更糟。如果您的原始向量可以排序,这无关紧要。


如果您想坚持自己的方法,但要对其进行调整,以下是我对此的看法:

您可以为此使用std::find

template <class T>
static bool compareVectors(const vector<T> &a, const vector<T> &b)
{
  const size_t n = a.size(); // make it const and unsigned!
  std::vector<bool> free(n, true);
  for ( size_t i = 0; i < n; ++i )
  {
      bool matchFound = false;
      auto start = b.cbegin();
      while ( true )
      {
          const auto position = std::find(start, b.cend(), a[i]);
          if ( position == b.cend() )
          {
              break; // nothing found
          }
          const auto index = position - b.cbegin();
          if ( free[index] )
          {
             // free pair found
             free[index] = false;
             matchFound = true;
             break;
          }
          else
          {
             start = position + 1; // search in the rest
          }
      }
      if ( !matchFound )
      {
         return false;
      }
   }
   return true;
}

另一种可能性是替换结构以存储空闲位置。您可以尝试 std::bitset 或将使用的索引存储在向量中并检查匹配是否不在该索引向量中。如果这个函数的结果经常是相同的(所以要么大部分是真的,要么大部分是假的),你可以优化你的数据结构来反映这一点。例如。如果结果通常是错误的,我会使用已使用索引的列表,因为可能只需要存储少数索引。

此方法与您的方法具有相同的复杂性。使用 std::find 搜索有时比手动搜索要好。 (例如,如果数据已排序并且编译器知道它,这可以是二进制搜索)。

【讨论】:

  • 我不是反对票,但我可以推测反对票的原因;我认为没有必要保留“我刚刚意识到这段代码......”之前的部分。如果您已经意识到您的答案的第一部分是错误的或其他不受欢迎的,为什么要保留它并浪费未来读者的时间?
  • 请记住&lt;T&gt; 可能无法排序,并且您不知道向量使用了多少内存 - 复制它们可能是个坏主意。 (OP 在他的代码中专门通过引用传递它们)
  • 为了比较两个完整的容器,== 优于 std::equal
  • 我认为== 优于std::equal 的原因是== 可以针对特定容器进行优化(标准库实现很可能不包含指向容器的指针在迭代器中)。
  • 对第一个解决方案的微优化是为第二个vector 实现自己的排序算法(例如快速排序),您可以在其中使用来自第一个已排序向量的枢轴。如果两者相等,它们应该是最佳枢轴,如果它们不是最佳的(除以 50/50),vectors 是不同的(停止排序)。
【解决方案2】:

您可以概率地在 O(n) 中比较两个未排序的向量 (u,v):

计算:

U= xor(h(u[0]), h(u[1]), ..., h(u[n-1]))
V= xor(h(v[0]), h(v[1]), ..., h(v[n-1]))

如果 U==V,那么向量可能相等。

h(x) 是任何 non-cryptographic hash function - 例如 MurmurHash。 (加密函数也可以,但通常会更慢)。

(即使没有散列也可以工作,但是当值的范围相对较小时,它的健壮性会低得多)。

128 位散列函数对于许多实际应用来说已经足够了。

【讨论】:

  • 聪明,让大多数不等式更快,但代价是让等式和少数不等式变慢。
【解决方案3】:

我注意到大多数提议的解决方案都涉及对输入向量进行排序。我认为对数组进行排序计算的计算量比评估两个向量的相等性所必需的更多(如果输入向量是恒定的,则复制一份需要制作)。 另一种方法是构建一个关联容器来计算每个向量中的元素......也可以在并行中减少两个向量。在非常大的向量的情况下可以提供很好的加速。

template <typename T>  bool compareVector(const std::vector<T> &  vec1, const std::vector<T> & vec2) {
    if (vec1.size() != vec2.size())
        return false ;

    //Here we assuame that T is hashable ...
    auto count_set =  std::unordered_map<T,int>();

    //We count the element in each vector...
    for (unsigned int count = 0 ; count <  vec1.size();++count)
    {
        count_set[vec1[count]]++;
        count_set[vec2[count]]--;
    } ;

    // If everything balance out we should have zero everywhere
    return std::all_of(count_set.begin(),count_set.end(),[](const std::pair<T,int> p) { return p.second == 0 ;});

}

这种方式取决于您的哈希函数的性能,我们可能会在展位向量的长度上得到线性复杂度(与排序的 n*logn 相比)。 注意代码可能有一些错误,确实有时间检查它......

我在 ubuntu 13.10,vmware core i7 gen 3 上对这种比较两个向量的方式进行基准测试以进行基于排序的比较:

通过计数比较 500 个元素的 200 个向量需要 0.184113 秒

通过排序比较 500 个元素的 200 个向量需要 0.276409 秒

通过计数比较 1000 个元素的 200 个向量需要 0.359848 秒

通过排序比较 1000 个元素的 200 个向量需要 0.559436 秒

通过计数比较 5000 个元素的 200 个向量需要 1.78584 秒

通过排序比较 5000 个元素的 200 个向量需要 2.97983 秒

【讨论】:

  • const std::vector&lt;T&gt;&amp; smallest_vec = (a.size()&lt;b.size()?a:b);
  • @MooingDuck 即使使用vec1vec2 也是没有意义的,因为这个函数首先要做的是if (vec1.size() != vec2.size()) return false;。另外,我认为您仍然无法使用它来初始化 hash_map(键值对)。
  • @user2381422 最明确的是,这个答案还没有得到足够的支持。这是一个非常好的方法!
  • 这个解决方案需要 O(n) 额外的空间和一个散列函数;为了高效,哈希函数必须比多次比较更有效。通过提供存储桶计数,您可能会获得额外的速度。我认为如果for 循环被分成两个(每个向量上一个),可能会获得更好的缓存一致性(不确定那个)。对于某些情况,这是一种有效的解决方案。 +1
  • @DyP 非常正确。我做了一些实验,要比较的向量越长,这种方法执行的性能就越差。调整桶的数量(我采取减少哈希冲突的数量)确实可以解决问题。拆分循环还可以通过更好的缓存一致性提高性能,但也因为它会降低数据依赖性并因此增加 ILP。(如果向量足够长,两个循环甚至可能位于不同的线程中)。最后,我认为我可以使用 set 或 map 之类的东西,并且仍然可以获得比排序更好的性能
【解决方案4】:

正如其他人所建议的,事先对向量进行排序会提高性能。

作为一项额外的优化,您可以将向量堆出来进行比较(复杂度为 O(n),而不是使用 O(n*log(n) 进行排序)。

之后,您可以从两个堆中弹出元素(复杂度 O(log(n))),直到出现不匹配。

这样做的好处是,如果向量不相等,您只需堆化而不是排序。

以下是代码示例。要知道什么是真正最快的,您必须针对您的用例使用一些示例数据进行测量。

#include <algorithm>

typedef std::vector<int> myvector;

bool compare(myvector& l, myvector& r)
{
   bool possibly_equal=l.size()==r.size();
   if(possibly_equal)
     {
       std::make_heap(l.begin(),l.end());
       std::make_heap(r.begin(),r.end());
       for(int i=l.size();i!=0;--i)
         {
           possibly_equal=l.front()==r.front();
           if(!possibly_equal)
             break;
           std::pop_heap(l.begin(),l.begin()+i);
           std::pop_heap(r.begin(),r.begin()+i);
         }
     }
  return possibly_equal;
}

【讨论】:

    【解决方案5】:

    如果您在相同的向量上经常使用此函数,最好保留已排序的副本以进行比较。

    理论上,如果每个向量只比较一次,那么对向量进行排序并比较排序的向量可能会更好(排序是 O(n*log(n)),比较排序的向量 O(n),而你的函数是 O(n^2)。 但我认为,如果您不经常比较相同的向量,那么为排序向量分配内存所花费的时间将使任何理论上的收益相形见绌。

    与所有优化一样,分析是确保的唯一方法,我会尝试一些 std::sort / std::equal 组合。

    【讨论】:

    • 内存分配开销可以通过在调用之间重用排序向量来克服。如果涉及多个线程,这显然需要小心。
    • 如何比较 O(log(n)) 中的两个排序向量?
    • @LiorKogan:这可能是一个错字,但这并不重要,不是吗?无论成本是O(log N) 还是O(N)(实际上是这样),算法仍然以向量的排序为主导O(N log N)
    • sry 伙计们,我在编写代码时有点着急。一切都在 c++11 编译器上编译和运行 :)
    【解决方案6】:

    就像 stefan 说的那样,您需要进行排序以获得更好的复杂性。 然后你可以使用 == 运算符(用于在 cmets 中进行校正的 tnx - ste equal 也可以,但更适合比较范围而不是整个容器)

    如果这还不够快,那就麻烦微优化。

    向量是否也保证大小相同? 如果不把那个检查放在开头。

    【讨论】:

    【解决方案7】:

    另一种可能的解决方案(只有在所有元素都是唯一的情况下才可行)应该会在一定程度上改进@stefan 的解决方案(尽管复杂性将保持在 O(NlogN) 中)是这样的:

    template <class T>
    static bool compareVectors(vector<T> a, const vector<T> & b)
    {
        // You should probably check this outside as it can 
        // avoid you the copy of a
        if (a.size() != b.size()) return false;
    
        std::sort(a.begin(), a.end());
        for (const auto & v : b)
            if ( !std::binary_search(a.begin(), a.end(), v) ) return false;
        return true;
    }
    

    这应该会更快,因为它直接将搜索作为O(NlogN) 操作执行,而不是对b (O(NlogN)) 进行排序,然后搜索两个向量(O(N))。

    【讨论】:

    • 这是完全错误的。它打破了[1 2][1 1][1 2][1]
    • 没错,我假设所有不同的元素和大小相等的向量。我已经确定了我的答案以强调这一点。
    • 如果所有元素都是唯一的,那么使用vector 而不是set 就没有意义了。
    猜你喜欢
    • 2021-12-29
    • 1970-01-01
    • 2018-10-31
    • 1970-01-01
    • 1970-01-01
    • 2015-01-22
    • 1970-01-01
    • 2017-08-05
    • 1970-01-01
    相关资源
    最近更新 更多