【问题标题】:How to define a custom key equivalence predicate for an std::unordered_set?如何为 std::unordered_set 定义自定义键等价谓词?
【发布时间】:2020-03-15 14:11:00
【问题描述】:

我正在尝试为我正在使用的哈希集定义一个关键等价谓词,但编译器一直告诉我function template "UniqueTable::table_key_equiv_pred" is not a type name,我不明白为什么。

我已经将我的等价函数定义为:

template<class t>
bool UniqueTable::table_key_equiv_pred(t a, node b) const
{
    return a == b;
}

template<>
bool UniqueTable::table_key_equiv_pred<int>(int a, node b) const
{
    return b && a == b->level;
}

template<>
bool UniqueTable::table_key_equiv_pred<node>(node a, node b) const
{
    return b && a && a->level == b->level && a->one == b->one && a->zero == b->zero;
}

然后使用以下方法实例化一个集合:

auto hashtable = new std::unordered_set<node, std::hash<node>, table_key_equiv_pred>();

如何定义一个谓词作为std::unordered_set 的类型参数?

【问题讨论】:

  • 您是否尝试使用table_key_equiv_pred() 作为回调?
  • 我正在尝试通过两种不同的方法查找节点,使用intnode 作为键
  • 函数唯一的工作就是检查node == node是否为真。它由集合内部使用。此外,table_key_equiv_pred 是一个模板,而不是一个函数。 table_key_equiv_pred&lt;node&gt; 是一个函数。
  • 为什么要动态分配哈希集并将结果存储在原始指针中?
  • 旁白:看起来node 是指针类型的别名,为什么不让集合包含值呢?

标签: c++ c++11


【解决方案1】:

std::unordered_set 的第三个模板参数必须是比较对象的类型。 table_key_equiv_pred 不是类型。默认为std::equal_to,其定义类似于:

template <typename T>
struct equal_to {
  bool operator()(const T &lhs, const T &rhs) const {
    return lhs == rhs;
  }
};

您希望能够使用ints 或任何其他类型在集合中搜索nodes。这样做std::unordered_set 并非不可能,只是有点痛苦。问题是std::unordered_set 首先比较哈希值,然后再比较实际对象。如果您想使用int 搜索node,那么两者必须具有相同的哈希值并且比较相等。

从问题中的代码中,我收集到 node 不仅仅是 level,因此 4 的哈希值不会等于 node 的哈希值level == 4(除非您已将 std::hash&lt;node&gt; 定义为仅返回 level)。这意味着即使是第 4 级节点,在确定 4 不在集合中之前,甚至不会使用自定义比较对象。

能够搜索与任何东西进行比较的节点是很棘手的,std::unordered_set 因为您需要一个“透明”哈希函数。我建议您切换到二叉树集 (std::set)。此集合比较元素的顺序(使用小于比较)。这意味着您只需要定义一个小于比较函数。

实际创建自定义比较对象并执行以下操作是相当少见的:std::set&lt;node, MyCustomLessThan&gt;。通常所做的是为该类型定义一个自定义的小于运算符,然后让默认的比较对象(在本例中为 std::less)调用该运算符。现在你要少用“透明”了。

std::less&lt;int&gt; 比较两个整数。 std::less&lt;node&gt; 比较两个节点。 std::less&lt;&gt; 将任何事物与任何事物进行比较,称为不透明。 std::less&lt;&gt; 的定义类似于:

template <>
struct less<void> {
  template <typename Left, typename Right>
  bool operator()(const Left &lhs, const Right &rhs) const {
    return lhs < rhs;
  }
};

所以你想要的数据结构是std::set&lt;node, std::less&lt;&gt;&gt; 但是你仍然需要定义整数和节点之间的比较。您可以通过重载小于运算符来做到这一点。

// I'm not confident that these implementations are correct
// Nullable objects make this pretty tricky!
bool operator<(const node &lhs, const int rhs) {
  return !lhs || lhs->level < rhs;
}
bool operator<(const int lhs, const node &rhs) {
  return rhs && lhs < rhs->level;
}
bool operator<(const node &lhs, const node &rhs) {
  // I'll probably get this wrong so I'll leave this up to you!
}

这是一个完整的例子(注意这使用了 C++14 的特性):

#include <set>
#include <iostream>    

struct Node {
  int level;
  int thing;
};

bool operator<(const Node lhs, const int rhs) {
  return lhs.level < rhs;
}
bool operator<(const int lhs, const Node rhs) {
  return lhs < rhs.level;
}
bool operator<(const Node lhs, const Node rhs) {
  // A better way to do this is with std::tie
  // see https://stackoverflow.com/a/16090720/4093378
  if (lhs.level < rhs.level) return true;
  if (lhs.level > rhs.level) return false;
  if (lhs.thing < rhs.thing) return true;
  if (lhs.thing > rhs.thing) return false;
  return false;
}

int main() {
  std::set<Node, std::less<>> set;
  set.insert({1, 91});
  set.insert({1, 87});
  set.insert({2, 43});
  set.insert({2, 10});

  // find a level 2 node
  // there's more than 1 level 2 node so you'll get the first one
  // see https://en.cppreference.com/w/cpp/container/set/find
  if (auto iter = set.find(2); iter != set.end()) {
    // prints "2 - 10"
    std::cout << iter->level << " - " << iter->thing << '\n';
  }

  // get a range of all the level 1 nodes
  // see https://en.cppreference.com/w/cpp/container/set/equal_range
  auto range = set.equal_range(1);
  for (auto iter = range.first; iter != range.second; ++iter) {
    // prints "1 - 87" then "1 - 91"
    std::cout << iter->level << " - " << iter->thing << '\n';
  }

  // a neat thing about std::set is that all the elements are kept in order
  // that's how std::set differs from std::unordered_set!
  // this prints "1 - 87", "1 - 91", "2 - 10" and then "2 - 43"
  for (const Node node : set) {
    std::cout << node.level << " - " << node.thing << '\n';
  }
}

如果您更愿意使用std::unordered_set,则必须使用透明散列函数。 这是 C++20 的一项功能。请阅读我链接的文档以获取更多信息。

为了搜索具有给定级别的节点,级别和节点的哈希必须兼容。这意味着第 5 级的哈希必须与第 5 级节点的哈希相同。

这是一个完整的例子(注意这使用了 C++20 的特性):

#include <iostream>
#include <unordered_set>

struct Node {
  int level;
  int thing;
};

struct NodeEqual {
  struct is_transparent {};

  bool operator()(const Node lhs, const int rhs) const noexcept {
    return lhs.level == rhs;
  }
  bool operator()(const int lhs, const Node rhs) const noexcept {
    return lhs == rhs.level;
  }
  bool operator()(const Node lhs, const Node rhs) const noexcept {
    return lhs.level == rhs.level && lhs.thing == rhs.thing;
  }
};

struct NodeHash {
  using transparent_key_equal = NodeEqual;

  size_t operator()(const Node node) const noexcept {
    // note that the hash depends only on level
    // if lots of nodes have the same level,
    // then lots of nodes will have the same hash
    // this could lead to lots of equality comparisons
    return node.level;
  }
  size_t operator()(const int level) const noexcept {
    return level;
  }
};

int main() {
  std::unordered_set<Node, NodeHash> set;
  set.insert({1, 91});
  set.insert({1, 87});
  set.insert({2, 43});
  set.insert({2, 10});

  // find a level 2 node
  // see https://en.cppreference.com/w/cpp/container/unordered_set/find
  if (auto iter = set.find(2); iter != set.end()) {
    // prints either "2 - 43" or "2 - 10"
    std::cout << iter->level << " - " << iter->thing << '\n';
  }

  // getting a range of all the level 1 nodes isn't actually possible
  // there are a couple of tweaks required to make it possible
  // however, these tweaks will further reduce the performance of std::unordered_set
}

我实际上无法测试这个示例(因为它是 C++20),所以我不确定它是否正确。我只能希望我已经正确阅读了文档!

std::set 实际上可能比std::unordered_set 快,除非我对level 的独特性有更多了解。

【讨论】:

  • 这太棒了,我真的很感谢你的热情,但我将拥有数百万个节点,所以使用 O(log(n)) 不会减少它,我不过仍然接受答案,因为我现在意识到我必须使用 struct 来创建谓词对象?
  • @iggy12345 std::setstd::unordered_set 是否更快将取决于 level 的唯一性。每个级别只有节点吗?或者每个级别可能只有 2 或 3 个节点?还是第 5 层有 100 个节点?
  • @iggy12345 如果有一百万个节点,并且每个级别(平均)有超过 20 个节点(log_2(1000000) 大约是 20),那么 std::set 具有更好的时间复杂度。跨度>
  • 嗯,一般来说,我计划对指向节点本身的指针进行哈希处理,每个级别可以有任意数量的节点,除了级别 0,它只有 2 个。我不打算进行哈希处理使用级别,因为它不是唯一的,但是能够确定是否有任何节点处于给定级别会很好,这就是为什么我试图弄清楚如何通过级别查找节点
  • @iggy12345 std::set 查找节点和查找级别的时间复杂度相同。我认为std::set 没有你想象的那么慢。如果您想要对节点和级别进行恒定时间查找,那么您可能需要一个额外的容器。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多