I created a simple bidirectional map class that works by internally storing two std::map
instances, with opposite key/value types, and providing an user-friendly interface:
template<class T1, class T2> class Bimap
{
std::map<T1, T2> map1;
std::map<T2, T1> map2;
// ...
};
Is there a more efficient method of implementing a bidirectional map that doesn't require twice the memory?
How is a bimap usually implemented?
EDIT:
Should bimap element be mutable or immutable? (Changing one element in
map1
should change the key inmap2
, but keys are const and that's impossible - what's the solution?)Ownership of elements is also another problem: when an user inserts a key-value pair in the bimap, the bimap should make a copy of that key-value pair and store it, then the internal second map (with inverted key/value) should not copy but point to the original pair. How can this be achieved?
EDIT 2:
I've posted a possible implementation I made on Code Review.
There is a certain problem with double-storing your data in all simple implementations of a bimap. If you can break it down to a bimap of pointers from outside, then you can readily ignore this and simply keep both maps of the form
std::map<A*,B*>
like Arkaitz Jimenez already suggested (though contrary to his answer you have to care about the storage from outside to avoid aA->A*
lookup). But if you have the pointers anyway, why not simply store astd::pair<A,B>
at the point where you would otherwise storeA
andB
separately?It would be nice to have
std::map<A,B*>
instead ofstd::map<A*,B*>
as this would allow for example the lookup of an element associated to an string by a newly created string with the same content instead of the pointer to the original string that created the pair. But it is customary to store a full copy of the key with every entry and only rely on the hash to find the right bucket. This way the returned item will be the correct one even in the case of a hash-collision...If you want to have it quick and dirty though, there is this
Using a
multimap
instead of amap
and verifying every element you get with a lookup in the respectivly other map (get candidateb
frommapA
, hashb
and look inmapB
if it matches the wanted key, iterate to the next candidate b otherwise) this is a valid implementation - but still hackish in my opinion...You can get a much nicer solution by using the copies of the elements that are used to compare the entries (see above) as only storage. It is a bit harder to get your head around that though. To elaborate:
This nicer solution is similar to the solution that boost uses - even though they use some annonymized pointers as second elements of the pairs and thus have to use
reinterpret_cast
s.Note that the
.second
part of the pairs need to be mutable (so I'm not surestd::pair
can be used), or you have to add another layer of abstraction (std::set<pair<B, A**>> mapA
) even for this simple insertion. In both solutions you need temporary elements to return non-const references to elements.How about this?
Here, we avoid double storage of one type (T1). The other type (T2) is still double stored.
One possible implementation that uses an intermediate data structure and an indirection is:
Insertion
Suppose you have to insert (X, Y) where X, Y are instances of A and B respectively, then:
mapA
--- O(lg sz)mapB
--- O(lg sz)push_back
(IterX, IterY) inregister
--- O(1). Here IterX and IterY are iterators to the corresponding element inmapA
andmapB
and are obtained from (1) and (2) respectively.Lookup
Lookup for the image of an element, X, of type A:
mapA
. --- O(lg n)register
and get corresponding IterY. --- O(1)IterY->first
. --- O(1)So both the operations are implemented in O(lg n).
Space: All the copies of the objects of A and B are required to be stored only once. There is, however, a lot of bookkeeping stuff. But when you have large objects, that would also be not much significant.
Note: This implementation relies on the fact that the iterators of a map are never invalidated. Hence, the contents of
register
are always valid.A more elaborate version of this implementation can be found here
It would be more efficient to store all elements in a vector and have 2 maps of
<T1*,T2*>
and<T2*,T1*>
that way you would not have everything copied twice.The way I see it you are trying to store 2 things, elements themselves and the relationship between them, if you are aiming to scalar types you could leave it as is 2 maps, but if you aim to treat complex types it makes more sense to separate the storage from the relationships, and handle relationships outside the storage.
Boost Bimap makes use of Boost Mutant Idiom.
From the linked wikipedia page:
The implementation in boost sources is of course fairly hairier.
If you create a set of pairs to your types
std::set<std::pair<X,Y>>
you pretty much have your functionallity implemented and rules about mutabillity and constness preset (OK maybe the settings aren't what you want but tweaks can be made). So here is the code :Example usage
NOTE: All this is a rough code sketching of what a full implementation would be and even after polishing and extending the code I'm not implying that this would be an alternative to
boost::bimap
but merely a homemade way of having an associative container searchable by both the value and the key.Live example