我创建了一个简单的 双向映射 类,该类通过在内部存储两个具有相反键/值类型的 std::map
实例并提供用户友好的界面来工作:
template<class T1, class T2> class Bimap
{
std::map<T1, T2> map1;
std::map<T2, T1> map2;
// ...
};
是否有更有效的方法来实现不需要两倍内存的双向映射?
bimap 通常是如何实现的?
编辑:
bimap 元素应该是可变的还是不可变的? (更改 map1 中的一个元素应该更改 map2 中的键,但键是 const 并且这是不可能的 - 解决方案是什么?)
元素的所有权也是另一个问题:当用户在 bimap 中插入键值对时,bimap 应该复制该键值对并存储它,然后内部的第二个映射(具有反转的键/值)应该不是复制而是指向原始对。如何做到这一点?
编辑2:
I've posted a possible implementation I made on Code Review.
boost::bimap
的源代码,但我没有发现它特别有用,因为它是大量模板化的,而且关于 bimap 实现的有趣细节并不是真的裸露。它当然不容易阅读,也没有使用 C++11 特性(这可能有助于从头开始实现 bimap)。也许我应该在第一篇文章中提到它。另外,我确实在谷歌上查找了“双向地图实现”、“双向地图教程”等,但找不到好的文章。
std::map<T2, const T1*>
作为第二张地图进行测试后,mutability/immutability 问题浮出水面。问题是,如果我使用应该是用户可变的 T2
,则在第一个和第二个映射中修改该 T2
实例变得有些困难。可能需要从第二个映射中删除键/值对并使用修改后的键插入一个新的键/值对。然后我意识到可能 bimap 的 internal 表示应该取决于键/值对是否应该是可变的?也许我应该检查类型是否为 POD?
sizeof
是否很大,如果是,则使用“const 指针”第二个映射实现。我之所以打开这个问题,是因为在网上查看并尝试了各种事情之后,我仍然有很多疑问 - 我相信一定有某种数据结构允许双向键/值交互,同时在一定程度上保持正常的效率地图。
在 bimap 的所有简单实现中双重存储数据存在一定的问题。如果您可以将其分解为来自外部的指针双映射,那么您可以轻松忽略这一点,并像 Arkaitz Jimenez 已经建议的那样简单地保留 std::map<A*,B*>
形式的两个映射(尽管与他的回答相反,您必须关心从外部以避免 A->A*
查找)。但是,如果您仍然有指针,为什么不简单地将 std::pair<A,B>
存储在您将分别存储 A
和 B
的位置?
最好有 std::map<A,B*>
而不是 std::map<A*,B*>
因为这将允许例如通过具有相同内容的新创建的字符串而不是指向创建该对的原始字符串的指针来查找与字符串关联的元素.但习惯上在每个条目中存储密钥的完整副本,并且仅依靠哈希来找到正确的存储桶。这样即使在哈希冲突的情况下,返回的项目也将是正确的......
如果你想快速又脏,这里有
hackish 解决方案:创建两个地图 std::map
使用 multimap
而不是 map
并通过分别在另一个映射中查找来验证您获得的每个元素(从 mapA
获取候选 b
,散列 b
并查看 mapB
,如果它匹配想要密钥,否则迭代到下一个候选者 b)这是一个有效的实现 - 但在我看来仍然是 hackish ......
通过使用用于比较条目(见上文)的元素副本作为唯一存储,您可以获得更好的解决方案。不过,要弄清楚这一点有点困难。详细说明:
一个更好的解决方案:创建两组对作为 std::set
std::set<pair<B, A*>> mapA;
std::set<pair<A, B*>> mapB;
void insert(const A &a, const B &b) {
auto aitr = mapA.insert({b, nullptr}).first; // creates first pair
B *bp = &(aitr->first); // get pointer of our stored copy of b
auto bitr = mapB.insert({a, bp}).first;
// insert second pair {a, pointer_to_b}
A *ap = &(bitr->first); // update pointer in mapA to point to a
aitr->second = ap;
}
现在可以通过简单的 std::set 查找和指针取消引用来简单地完成查找。
这个更好的解决方案类似于 boost 使用的解决方案——尽管它们使用一些匿名指针作为对的第二个元素,因此必须使用 reinterpret_cast
。
请注意,对的 .second
部分需要是可变的(所以我不确定是否可以使用 std::pair
),或者即使对于这个简单的插入,您也必须添加另一个抽象层(std::set<pair<B, A**>> mapA
)。在这两种解决方案中,您都需要临时元素来返回对元素的非常量引用。
将所有元素存储在一个向量中并拥有 <T1*,T2*>
和 <T2*,T1*>
的 2 个映射会更有效,这样您就不会将所有内容复制两次。
在我看来,你试图存储 2 个东西,元素本身以及它们之间的关系,如果你的目标是标量类型,你可以将它保留为 2 个地图,但如果你的目标是处理复杂类型,那么更有意义将存储与关系分离,处理存储之外的关系。
std::vector<std::unique_ptr<T>>
,因为指针不能失效并且某些类可能是不可复制的。我想访问成员的效率会降低(但内存使用量会减半)。
Boost Bimap 使用 Boost Mutant Idiom。
从链接的维基百科页面:
Boost 突变惯用语使用 reinterpret_cast 并且很大程度上依赖于假设具有相同数据成员(类型和顺序)的两个不同结构的内存布局是可互换的。尽管 C++ 标准不保证这个属性,但几乎所有的编译器都满足它。
template <class Pair>
struct Reverse
{
typedef typename Pair::first_type second_type;
typedef typename Pair::second_type first_type;
second_type second;
first_type first;
};
template <class Pair>
Reverse<Pair> & mutate(Pair & p)
{
return reinterpret_cast<Reverse<Pair> &>(p);
}
int main(void)
{
std::pair<double, int> p(1.34, 5);
std::cout << "p.first = " << p.first << ", p.second = " << p.second << std::endl;
std::cout << "mutate(p).first = " << mutate(p).first << ", mutate(p).second = " << mutate(p).second << std::endl;
}
在 boost 源中的实现当然是相当复杂的。
std::map<Pair>
和 std::map<ReversePair>
存储在我的 bimap 类中?我了解 reinterpret_cast
发生了什么,但我不明白如何将其用于双向地图。
std::set<T1*>
和 std::set<T2*>
,我可以从 T1
检索 T2
个项目,但我找不到将突变用于从 T2
检索 T1
个项目。这是我的进度:pastie.org/8752050我应该如何在这里使用反向对成语?
如果您为您的类型创建一组对std::set<std::pair<X,Y>>
,您几乎已经实现了您的功能和关于可变性和常量性预设的规则(好吧,也许这些设置不是您想要的,但可以进行调整)。所以这里是代码:
#ifndef MYBIMAP_HPP
#define MYBIMAP_HPP
#include <set>
#include <utility>
#include <algorithm>
using std::make_pair;
template<typename X, typename Y, typename Xless = std::less<X>,
typename Yless = std::less<Y>>
class bimap
{
typedef std::pair<X, Y> key_type;
typedef std::pair<X, Y> value_type;
typedef typename std::set<key_type>::iterator iterator;
typedef typename std::set<key_type>::const_iterator const_iterator;
struct Xcomp
{
bool operator()(X const &x1, X const &x2)
{
return !Xless()(x1, x2) && !Xless()(x2, x1);
}
};
struct Ycomp
{
bool operator()(Y const &y1, Y const &y2)
{
return !Yless()(y1, y2) && !Yless()(y2, y1);
}
};
struct Fless
{ // prevents lexicographical comparison for std::pair, so that
// every .first value is unique as if it was in its own map
bool operator()(key_type const &lhs, key_type const &rhs)
{
return Xless()(lhs.first, rhs.first);
}
};
/// key and value type are interchangeable
std::set<std::pair<X, Y>, Fless> _data;
public:
std::pair<iterator, bool> insert(X const &x, Y const &y)
{
auto it = find_right(y);
if (it == end()) { // every .second value is unique
return _data.insert(make_pair(x, y));
}
return make_pair(it, false);
}
iterator find_left(X const &val)
{
return _data.find(make_pair(val,Y()));
}
iterator find_right(Y const &val)
{
return std::find_if(_data.begin(), _data.end(),
[&val](key_type const &kt)
{
return Ycomp()(kt.second, val);
});
}
iterator end() { return _data.end(); }
iterator begin() { return _data.begin(); }
};
#endif
示例用法
template<typename X, typename Y, typename In>
void PrintBimapInsertion(X const &x, Y const &y, In const &in)
{
if (in.second) {
std::cout << "Inserted element ("
<< in.first->first << ", " << in.first->second << ")\n";
}
else {
std::cout << "Could not insert (" << x << ", " << y
<< ") because (" << in.first->first << ", "
<< in.first->second << ") already exists\n";
}
}
int _tmain(int argc, _TCHAR* argv[])
{
bimap<std::string, int> mb;
PrintBimapInsertion("A", 1, mb.insert("A", 1) );
PrintBimapInsertion("A", 2, mb.insert("A", 2) );
PrintBimapInsertion("b", 2, mb.insert("b", 2));
PrintBimapInsertion("z", 2, mb.insert("z", 2));
auto it1 = mb.find_left("A");
if (it1 != mb.end()) {
std::cout << std::endl << it1->first << ", "
<< it1->second << std::endl;
}
auto it2 = mb.find_right(2);
if (it2 != mb.end()) {
std::cout << std::endl << it2->first << ", "
<< it2->second << std::endl;
}
return 0;
}
注意:所有这些都是对完整实现的粗略代码草图,即使在完善和扩展代码之后,我也不是暗示这将是 boost::bimap
的替代品,而只是 < strong>自制 方法,通过值和键搜索关联容器。
std::set
的迭代速度很慢,并且在最坏的情况下插入/查找需要 O(n) - 使用映射的好处是即使有很多元素也可以快速查找/插入
unordered
容器(哈希表)混淆了。正如 Josuttis 所说:您可以将集合视为特殊类型的映射,其中值与键相同。事实上,所有这些关联容器类型通常都是通过使用二叉树的相同基本实现来实现的
std::set
的复杂性与 std::map
相似这一事实,这似乎是一个不错的解决方案。有什么明显的缺点吗? (例如,与我的幼稚实现相比,查找中的任何性能损失?)
X
的比较运算符构造的,所以可能对 Y
的查找不是最佳的,但我不知道内部结构足以猜测(也许那里没有后顾之忧)。无论如何,我希望通过只提供一个 _data
来弥补这一点(更新、插入、删除、检查等),而不是两个会强制执行更复杂的代码(更复杂的错误)、更多内存的 _data
使用和重复操作
find
的实现似乎效率低下,因为 std::find_if
具有线性复杂性 (en.cppreference.com/w/cpp/algorithm/find),因此在此处使用 std::set
是毫无意义的(快速插入除外)。
使用中间数据结构和间接的一种可能实现是:
int sz; // total elements in the bimap
std::map<A, int> mapA;
std::map<B, int> mapB;
typedef typename std::map<A, int>::iterator iterA;
typedef typename std::map<B, int>::iterator iterB;
std::vector<pair<iterA, iterB>> register;
// All the operations on bimap are indirected through it.
插入
假设您必须插入 (X, Y) 其中 X, Y 分别是 A 和 B 的实例,那么:
在 mapA 中插入 (X, sz) --- O(lg sz) 在 mapB 中插入 (Y, sz) --- O(lg sz) 然后在寄存器中 push_back (IterX, IterY) --- O(1)。这里 IterX 和 IterY 是 mapA 和 mapB 中对应元素的迭代器,分别从 (1) 和 (2) 获得。
抬头
查找类型 A 的元素 X 的图像:
获取映射到 mapA 中 X 的 int。 --- O(lg n) 使用 int 索引到寄存器并得到对应的 IterY。 --- O(1) 一旦有了IterY,就可以通过IterY->first 得到Y。 --- O(1)
所以这两个操作都是在 O(lg n) 中实现的。
空间:A和B对象的所有副本只需要存储一次。然而,有很多簿记的东西。但是当你有大型物体时,这也没有多大意义。
注意:此实现依赖于 map 的迭代器永远不会失效的事实。因此,register
的内容始终有效。
可以在 here 中找到此实现的更详细版本
这个怎么样?
在这里,我们避免了一种类型(T1)的双重存储。另一种类型(T2)仍然是双重存储的。
// Assume T1 is relatively heavier (e.g. string) than T2 (e.g. int family).
// If not, client should instantiate this the other way.
template <typename T1, typename T2>
class UnorderedBimap {
typedef std::unordered_map<T1, T2> Map1;
Map1 map1_;
std::unordered_map<T2, Map1::iterator> map2_;
};
不定期副业成功案例分享