KeyEqual 在 std::unordered_set/std::unordered_map 中的有用性

Usefulness of KeyEqual in std::unordered_set/std::unordered_map

本文关键字:unordered std 有用性 map KeyEqual set      更新时间:2023-10-16

我知道这可能是一个模糊的问题,但我想知道当自定义比较器对 std 中的哈希容器有用时,现实世界的情况是什么。 我知道它在有序容器中很有用,但对于哈希容器来说,这似乎有点奇怪。

这样做的原因是,根据比较器相等的元素的哈希值需要相同,我相信在大多数情况下,这实际上意味着将查找/插入元素转换为一些常见的表示形式(它更快更容易实现)。

例如:

  • 一组不区分大小写的字符串:如果要正确散列,无论如何都需要将整个字符串大写/小写。
  • 分数集(其中 2/3 == 42/63):您需要将 42/63 转换为 2/3,然后对其进行哈希处理...

所以我想知道是否有人可以提供一些真实世界的例子来说明自定义std::unordered_模板参数的有用性,以便我可以在我编写的未来代码中识别这些模式。

注1:"对称参数"(std::map允许定制比较器,因此std::unordred_也应该可定制)是我考虑过的东西,我认为它没有说服力。

注2:为了简洁起见,我在帖子中混合了2种比较器(<==),我知道std::map使用<std::unordered_map使用==

根据 https://en.cppreference.com/w/cpp/container/unordered_set

在内部,元素不按任何特定顺序排序,但 组织到存储桶中。元素放入哪个存储桶取决于 完全基于其值的哈希值。这允许快速访问 单个元素,因为一旦计算了哈希,它就引用了 将元素放入的确切存储桶。

因此,哈希函数定义了您的元素最终将进入的存储桶,但是一旦确定了存储桶,为了找到该元素,将使用operator ==

基本上operator ==用于解决哈希冲突,因此,您需要哈希函数和operator ==保持一致。此外,如果您的运算符operator ==说两个元素相等,则该集合将不允许重复。

对于有关自定义的内容,我认为对strings 进行不区分大小写的set的想法很好:给定两个字符串,您需要提供一个不区分大小写的哈希函数,以允许set确定它必须存储字符串的存储桶。然后,您需要提供一个自定义KeyEqual,以允许集合实际检索元素。

过去,我必须处理的一个案例是一种允许用户插入字符串的方法,跟踪他们的插入顺序,但避免重复。因此,给定一个结构,例如:

struct keyword{
std::string value;
int sequenceCounter;
};

您希望仅根据value检测重复项。我想出的解决方案之一是具有自定义比较器/哈希函数的unordered_set,该函数仅使用value。这允许我在允许插入之前检查密钥是否存在。

一个有趣的用法是为一组给定的对象定义内存效率索引(术语的数据库意义)。

假设我们有一个程序,其中包含此类N对象的集合:

struct Person {
// each object has a unique firstName/lastName pair
std::string firstName;
std::string lastName;
// each object has a unique ssn value
std::string socialSecurityNumber;
// each object has a unique email value
std::string email;
}

我们需要通过任何唯一属性的值有效地检索对象。

实现比较

假设字符串比较是常量时间(字符串长度有限),则给出时间复杂度。

1) 单unordered_map

使用单个键索引的单个map(例如:email):

std::unordered_map<std::string,Person> indexedByEmail;
  • 时间复杂度:通过除email以外的任何唯一属性进行查找都需要遍历映射:平均 O(N)。
  • 内存使用情况:email值重复。这可以通过使用带有自定义哈希和比较的单个set来避免(见3)。

2)多重unordered_map,没有自定义哈希和比较

使用每个唯一属性的映射,默认哈希和比较:

std::unordered_map<std::pair<std::string,std::string>, Person> byName;
std::unordered_map<std::string, const Person*> byEmail;
std::unordered_map<std::string, const Person*> bySSN;
  • 时间复杂度:通过使用适当的映射,任何唯一属性的查找都是平均值 O(1)。
  • 内存使用:效率低下,因为所有string重复。

3)多重unordered_set,自定义哈希和比较:

通过自定义哈希和比较,我们定义了不同的unordered_set,这些将仅哈希和比较对象的特定字段。这些集可用于执行查找,就像项目存储在 2 中的map中一样,但不重复任何字段。

using StrHash = std::hash<std::string>;
// --------------------
struct PersonNameHash {
std::size_t operator()(const Person& p) const {
// not the best hashing function in the world, but good enough for demo purposes.
return StrHash()(p.firstName) + StrHash()(p.lastName);
}
};
struct PersonNameEqual {
bool operator()(const Person& p1, const Person& p2) const {
return (p1.firstName == p2.firstName) && (p1.lastName == p2.lastName);
}
};
std::unordered_set<Person, PersonNameHash, PersonNameEqual> byName;
// --------------------
struct PersonSsnHash {
std::size_t operator()(const Person* p) const {
return StrHash()(p->socialSecurityNumber);
}
};
struct PersonSsnEqual {
bool operator()(const Person* p1, const Person* p2) const {
return p1->socialSecurityNumber == p2->socialSecurityNumber;
}
};
std::unordered_set<const Person*, PersonSsnHash, PersonSsnEqual> bySSN;
// --------------------
struct PersonEmailHash {
std::size_t operator()(const Person* p) const {
return StrHash()(p->email);
}
};
struct PersonEmailEqual {
bool operator()(const Person* p1, const Person* p2) const {
return p1->email == p2->email;
}
};
std::unordered_set<const Person*,PersonEmailHash,PersonEmailEqual> byEmail;
  • 时间复杂度:任何唯一属性的查找仍然是 O(1) 平均值。
  • 内存使用情况:比 2 好得多):没有string重复。

现场演示

哈希函数本身以某种方式提取特征,并且 比较器的工作是区分特征是否相同
使用数据的"外壳",您可能不需要修改比较器
简而言之:在数据上放置一个特征外壳。特征负责进行比较

事实上,我不太明白你的问题描述。我的演讲在逻辑上不可避免地是混乱的。请理解。 :)