【问题标题】:Hash value for a std::unordered_mapstd::unordered_map 的哈希值
【发布时间】:2015-09-14 21:37:46
【问题描述】:

根据标准,std::hash 类中不支持容器(更不用说无序容器了)。所以我想知道如何实现它。我有的是:

std::unordered_map<std::wstring, std::wstring> _properties;
std::wstring _class;

我考虑过迭代条目,计算键和值的单独哈希(通过std::hash&lt;std::wstring&gt;)并以某种方式连接结果。

有什么好的方法可以做到这一点,如果未定义地图中的顺序是否重要?

注意:我不想使用 boost。

建议了一个简单的异或,所以应该是这样的:

size_t MyClass::GetHashCode()
{
  std::hash<std::wstring> stringHash;
  size_t mapHash = 0;
  for (auto property : _properties)
    mapHash ^= stringHash(property.first) ^ stringHash(property.second);

    return ((_class.empty() ? 0 : stringHash(_class)) * 397) ^ mapHash;
}

?

我真的不确定这个简单的异或是否足够。

【问题讨论】:

  • s/concatenate/XOR 你应该很高兴。那么散列函数必须能够做的事情就是为两个语义等价的值生成相同的散列,并将其输出合理地均匀分布在所有可能的散列值集合中。
  • @dyp OP 想要对容器本身进行哈希处理。
  • 基本上,您的问题是如何获取(无序)值范围的哈希值,实际上并不特定于std::unordered_map
  • “够了”是什么意思?你如何定义“足够”?完全没有碰撞?
  • 嗯,“足够”在这里意味着它满足与为 std::hash 定义的哈希函数相同的条件:en.cppreference.com/w/cpp/utility/hash

标签: c++ c++11 hash unordered-map


【解决方案1】:

响应

如果足够,您的意思是您的函数是否是单射的,答案是否定的。原因是您的函数可以输出的所有哈希值的集合具有基数 2^64,而您的输入空间是 很多更大。但是,这并不是很重要,因为考虑到输入的性质,您不能使用单射散列函数。一个好的散列函数具有以下品质:

  • 它不容易逆转。给定输出 k,在宇宙的生命周期内找到 m 使得 h(m) = k 在计算上是不可行的。
  • 范围均匀分布在输出空间中。
  • 很难找到两个输入 m 和 m' 使得 h(m) = h(m')

当然,这些范围实际上取决于您是否想要加密安全的东西,或者您想要获取一些任意数据块并只是发送一些任意 64 位整数。如果您想要加密安全的东西,那么自己编写并不是一个好主意。在这种情况下,您还需要保证该函数对输入中的微小变化敏感。 std::hash 函数对象不需要是加密安全的。它存在于与哈希表同构的用例。 CPP Rerefence 说:

对于两个不同的参数k1k2不相等,std::hash&lt;Key&gt;()(k1) == std::hash&lt;Key&gt;()(k2)的概率应该很小,接近1.0/std::numeric_limits&lt;size_t&gt;::max()

我将在下面展示您当前的解决方案如何不能真正保证这一点。

碰撞

我将就您的解决方案的变体向您提供一些观察(我不知道您的_class 成员是什么)。

std::size_t hash_code(const std::unordered_map<std::string, std::string>& m) {
    std::hash<std::string> h;
    std::size_t result = 0;
    for (auto&& p : m) {
        result ^= h(p.first) ^ h(p.second);
    }
    return result;
}

很容易产生冲突。考虑以下地图:

std::unordered_map<std::string, std::string> container0;
std::unordered_map<std::string, std::string> container1;
container0["123"] = "456";
container1["456"] = "123";
std::cout << hash_code(container0) << '\n';
std::cout << hash_code(container1) << '\n';

在我的机器上,使用 g++ 4.9.1 编译,输出如下:

1225586629984767119
1225586629984767119

关于这是否重要的​​问题出现了。相关的是您将多久拥有一次键和值颠倒的映射。这些冲突将发生在键和值集相同的任何两个映射之间。

迭代顺序

具有完全相同键值对的两个unordered_map 实例不一定具有相同的迭代顺序。 CPP Rerefence 说:

对于两个相等的参数k1k2std::hash&lt;Key&gt;()(k1) == std::hash&lt;Key&gt;()(k2)

这是散列函数的一个微不足道的要求。您的解决方案避免了这种情况,因为迭代顺序无关紧要,因为 XOR 是可交换的。

可能的解决方案

如果您不需要加密安全的东西,您可以稍微修改您的解决方案以消除对称性。这种方法在实践中对于哈希表等是可以的。此解决方案也与unordered_map 中的订单未定义这一事实无关。它使用与您的解决方案相同的属性(XOR 的交换性)。

std::size_t hash_code(const std::unordered_map<std::string, std::string>& m) {
    const std::size_t prime = 19937;
    std::hash<std::string> h;
    std::size_t result = 0;
    for (auto&& p : m) {
        result ^= prime*h(p.first) + h(p.second);
    }
    return result;
}

在这种情况下,哈希函数所需要的只是一种将键值对映射到任意好的哈希值的方法,以及一种使用交换操作组合键值对的哈希值的方法。这样,顺序就无关紧要了。在我写的示例hash_code 中,键值对哈希值只是键的哈希值和值的哈希值的线性组合。您可以构建一些更复杂的东西,但没有必要这样做。

【讨论】:

  • 啊哈,这与我的预期很接近。 “基数”可能是一个素数并且是任意的,对吧?当然,这不适用于任何类型的加密支持。我认为这会从 std::hash 的使用中隐含地清除。
  • 是的,我选择了 19937,因为 2^19937 - 1 是我最喜欢的梅森素数。
  • 我可能会感到困惑,但是如果两个相等的映射没有以相同的顺序迭代,这不能为您提供两个不同的哈希值吗? (即这个哈希顺序不是依赖的吗?)
  • @Hasturkun 好吧,我刚刚修好了!
  • 不错且全面的答案,但我认为第一部分有点误导。据我所知,C++ 标准从未声称 std::hash 应该是加密散列函数,因此如果您基于 std::hash 编写自己的容器散列,您也不会期望它是加密安全的。对于它作为哈希表的密钥生成器的预期用途,这种安全性也不是必需的,也不会保证额外的成本。但是,您的最后一个要点与击败 DOS 攻击有关。
猜你喜欢
  • 2013-06-12
  • 2022-11-02
  • 1970-01-01
  • 2016-05-26
  • 1970-01-01
  • 1970-01-01
  • 2011-04-17
  • 2011-11-05
  • 1970-01-01
相关资源
最近更新 更多