如何为键为std::pair的std::unordered_map编写顺序无关哈希函数
问题描述
我正在尝试创建一个以std::pair为键、size_t为值的std::unordered_map,核心需求是自定义哈希函数,使其忽略键std::pair两个成员的顺序,即满足如下逻辑:
std::pair<int,int> p1 = std::make_pair(3,4); std::pair<int,int> p2 = std::make_pair(4,3); std::unordered_map<std::pair<int,int>, int> m; m[p1] = 3; // m[p2] 此时也应该返回3!
以下是我从实际程序中截取的相关代码片段,并非完整的最小可运行示例:
#include <vector> #include <string> #include <iostream> #include <algorithm> #include <memory> #include <unordered_map> #include <functional> class Point { public: static size_t id_counter; size_t id; Point()=default; ~Point()=default; bool operator==(const Point& rhs) { return id == rhs.id; } friend std::ostream& operator<<(std::ostream& os, Point& P); }; size_t Point::id_counter = 0; class Hasch_point_pair { public: size_t operator()(const std::pair<Point*, Point*>* p) const { // XOR哈希,不在乎碰撞,随便写的 auto h1 = std::hash<size_t>()(p->first->id); auto h2 = std::hash<size_t>()(p->second->id); return h1^h2; } }; int main(int argc, char const *argv[]) { auto p1 = std::make_unique<Point>(); auto p2 = std::make_unique<Point>(); auto p3 = std::make_unique<Point>(); auto p4 = std::make_unique<Point>(); std::unordered_map<std::pair<Point*, Point*>*, size_t*, Hasch_point_pair> m; auto p = std::make_unique<std::pair<Point*, Point*>>(p1.get(),p2.get()); auto p_hmm = std::make_unique<std::pair<Point*, Point*>>(p2.get(),p1.get()); size_t value = 3; m[p.get()] = &value; std::cout << "m[p] = " << m.at(p.get()) << std::endl; std::cout << "m[p_hmm] = " << m.at(p_hmm.get()) << std::endl; }
我曾想到一个实现思路:先比较pair中两个Point对象的id,始终将id更大的Point作为哈希计算的第一个输入,但目前没能成功实现,想请问这个思路是否合理,应当如何正确实现该需求?
我尝试修改的哈希函数代码如下:
class Hasch_point_pair { public: size_t operator()(const std::pair<Point*, Point*>* p) const { if (p->first->id > p->second->id) { auto h1 = std::hash<size_t>()(p->first->id); auto h2 = std::hash<size_t>()(p->second->id); return h1^h2; } else { // 注意这里交换了h1和h2的顺序 auto h2 = std::hash<size_t>()(p->first->id); auto h1 = std::hash<size_t>()(p->second->id); return h1^h2; } } };
解答
你提出的「先排序pair内两个元素,固定顺序后再计算哈希」的思路完全合理,但现有代码无法生效,核心问题有两个,和哈希计算的核心逻辑无关:
- 你只自定义了哈希函数,没有自定义键的相等判断逻辑
unordered_map判定两个键为同一个,除了校验哈希值相等,还会调用键的==运算符做二次确认。你当前使用的键类型是std::pair<Point*, Point*>*即指针类型,默认的指针相等判断只比较地址是否相同——哪怕两个pair指向的内容完全一致,只要是不同的pair对象、地址不一样,就会被判定为不相等,自然查不到对应的值。 - 你写的交换顺序异或逻辑本质和直接
h1^h2没有区别,因为异或运算满足交换律,a^b和b^a的计算结果完全一致。而且纯异或哈希的碰撞概率极高,比如(a,a)这类两个id相同的pair,哈希值永远为0,很容易触发哈希冲突。
正确实现步骤
- 不要把pair指针当键,直接用
std::pair<Point*, Point*>作为键类型,省去指针地址比较的额外问题。 - 自定义哈希函数时,先把pair里的两个指针按id大小排序,固定顺序后再做哈希组合,不要用纯异或,改用移位混合的方式降低碰撞概率。
- 为该pair类型自定义对应的相等判断逻辑,只要两个pair里存储的Point指针集合一致(不考虑顺序),就判定为键相等。
修正后的可运行核心代码如下:
#include <iostream> #include <memory> #include <unordered_map> #include <functional> class Point { public: static inline size_t id_counter = 0; size_t id; Point() : id(id_counter++) {} ~Point()=default; bool operator==(const Point& rhs) const { return id == rhs.id; } }; // 无序pair的哈希函数 struct HashUnorderedPointPair { size_t operator()(const std::pair<Point*, Point*>& p) const { size_t h1, h2; // 固定计算顺序:id小的放前面,id大的放后面 if (p.first->id < p.second->id) { h1 = std::hash<size_t>()(p.first->id); h2 = std::hash<size_t>()(p.second->id); } else { h1 = std::hash<size_t>()(p.second->id); h2 = std::hash<size_t>()(p.first->id); } // 哈希组合,碰撞概率远低于纯异或 return h1 ^ (h2 << 1); } }; // 无序pair的相等判断 struct EqUnorderedPointPair { bool operator()(const std::pair<Point*, Point*>& a, const std::pair<Point*, Point*>& b) const { // 不考虑顺序,只要两个pair包含的两个Point id完全一致就算相等 return (a.first->id == b.first->id && a.second->id == b.second->id) || (a.first->id == b.second->id && a.second->id == b.first->id); } }; int main() { auto p1 = std::make_unique<Point>(); // id=0 auto p2 = std::make_unique<Point>(); // id=1 std::unordered_map<std::pair<Point*, Point*>, size_t, HashUnorderedPointPair, EqUnorderedPointPair> m; std::pair<Point*, Point*> key1 = {p1.get(), p2.get()}; std::pair<Point*, Point*> key2 = {p2.get(), p1.get()}; m[key1] = 3; std::cout << "m[key1] = " << m[key1] << std::endl; // 输出3 std::cout << "m[key2] = " << m[key2] << std::endl; // 同样输出3,符合预期 return 0; }
补充说明:如果后续需要支持空指针场景,只需要在排序和相等判断逻辑里给空指针指定固定排序优先级(比如空指针永远排在最前面)即可,核心逻辑不变。
内容的提问来源于stack exchange,提问作者Johan
相关产品推荐
相关产品推荐

