J'ai créé une classe de carte bidirectionnelle simple qui fonctionne en stockant en interne deux std::map
instances, avec des types de clé/valeur opposés, et fournissant une interface conviviale:
template<class T1, class T2> class Bimap
{
std::map<T1, T2> map1;
std::map<T2, T1> map2;
// ...
};
Existe-t-il une méthode plus efficace pour implémenter une carte bidirectionnelle qui ne nécessite pas deux fois la mémoire?
Comment un bimap est-il généralement implémenté?
MODIFIER:
L'élément bimap doit-il être modifiable ou immuable? (Modification d'un élément dans map1
devrait changer la clé dans map2
, mais les clés sont const et c'est impossible - quelle est la solution?)
La propriété des éléments est également un autre problème: lorsqu'un utilisateur insère une paire clé-valeur dans la bimap, la bimap doit faire une copie de cette paire clé-valeur et la stocker, puis la deuxième carte interne (avec clé/valeur inversée) doit pas copier mais pointer vers la paire d'origine. Comment cela peut il etre accompli?
EDIT 2:
J'ai posté une implémentation possible que j'ai faite sur Code Review.
Il y a un certain problème avec le double stockage de vos données dans toutes les implémentations simples d'une bimap. Si vous pouvez le décomposer en une bimap de pointeurs de l'extérieur, vous pouvez facilement l'ignorer et conserver simplement les deux cartes de la forme std::map<A*,B*>
comme Arkaitz Jimenez l'a déjà suggéré (bien que contrairement à sa réponse, vous devez vous soucier du stockage de l'extérieur pour éviter un A->A*
Chercher). Mais si vous avez quand même les pointeurs, pourquoi ne pas simplement stocker un std::pair<A,B>
au point où vous stockeriez autrement A
et B
séparément?
Ce serait bien d'avoir std::map<A,B*>
au lieu de std::map<A*,B*>
car cela permettrait par exemple la recherche d'un élément associé à une chaîne par une chaîne nouvellement créée avec le même contenu au lieu du pointeur sur la chaîne d'origine qui a créé la paire. Mais il est habituel de stocker une copie complète de la clé avec chaque entrée et de ne compter que sur le hachage pour trouver le bon compartiment. De cette façon, l'article retourné sera le bon, même en cas de collision de hachage ...
Si vous voulez que ce soit rapide et sale, il y a ceci
solution hackish:
Créez deux cartes
std::map<size_t, A> mapA
etstd::map<size_t, B> mapB
. Lors de l'insertion, hachez les deux éléments à insérer pour obtenir les clés des cartes respectives.void insert(const A &a, const B &b) { size_t hashA = std::hash<A>(a); size_t hashB = std::hash<B>(b); mapA.insert({hashB, a}); mapB.insert({hashA, b}); }
La recherche est implémentée de manière analogue.
Utiliser un multimap
au lieu d'un map
et vérifier chaque élément obtenu avec une recherche dans l'autre carte respectivement (obtenir le candidat b
de mapA
, hachage b
et regardez dans mapB
si elle correspond à la clé voulue, répétez le prochain candidat b sinon) c'est une implémentation valide - mais toujours hackish à mon avis ...
Vous pouvez obtenir une solution beaucoup plus agréable en utilisant les copies des éléments utilisés pour comparer les entrées (voir ci-dessus) comme seul stockage. Il est cependant un peu plus difficile de comprendre cela. Élaborer:
ne meilleure solution:
Créez deux ensembles de paires comme
std::set<pair<A, B*>>
etstd::set<pair<B, A*>>
et surcharger leoperator<
etoperator==
pour ne prendre en compte que le premier élément des paires (ou fournir une classe de comparaison correspondante). Il est nécessaire de créer des ensembles de paires au lieu de cartes (qui se ressemblent en interne) car nous avons besoin d'une garantie queA
etB
seront toujours aux mêmes positions en mémoire. Lors de l'insertion d'unpair<A,B>
nous l'avons divisé en deux éléments qui correspondent aux ensembles ci-dessus.
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;
}
La recherche peut maintenant être effectuée simplement par un simple
std::set
recherche et déréférencement de pointeur.
Cette solution plus agréable est similaire à la solution qui stimule les utilisations - même si elles utilisent des pointeurs annonymisés comme seconds éléments des paires et doivent donc utiliser reinterpret_cast
s.
Notez que le .second
une partie des paires doit être modifiable (donc je ne suis pas sûr std::pair
peut être utilisé), ou vous devez ajouter une autre couche d'abstraction (std::set<pair<B, A**>> mapA
) même pour cette simple insertion. Dans les deux solutions, vous avez besoin d'éléments temporaires pour renvoyer des références non const à des éléments.
Il serait plus efficace de stocker tous les éléments dans un vecteur et d'avoir 2 cartes de <T1*,T2*>
et <T2*,T1*>
de cette façon, tout ne serait pas copié deux fois.
La façon dont je le vois, vous essayez de stocker 2 choses, les éléments eux-mêmes et la relation entre eux, si vous visez des types scalaires, vous pouvez le laisser tel quel, mais si vous visez à traiter des types complexes, il est plus logique de séparez le stockage des relations et gérez les relations en dehors du stockage.
Boost Bimap utilise Boost Mutant Idiom .
De la page wikipedia liée:
L'idiome mutant Boost utilise reinterpret_cast et dépend fortement de l'hypothèse que les dispositions de mémoire de deux structures différentes avec des membres de données identiques (types et ordre) sont interchangeables. Bien que la norme C++ ne garantisse pas cette propriété, pratiquement tous les compilateurs la satisfont.
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;
}
L'implémentation dans les sources boost est bien sûr assez plus épaisse.
Si vous créez un ensemble de paires pour vos types std::set<std::pair<X,Y>>
vous avez à peu près votre fonctionnalité implémentée et des règles sur la mutabillité et la constance prédéfinies (OK peut-être que les paramètres ne sont pas ce que vous voulez mais des ajustements peuvent être faits ). Voici donc le code:
#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
Exemple d'utilisation
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;
}
[~ # ~] note [~ # ~] : Tout ceci est un croquis de code approximatif de ce que serait une implémentation complète et même après polissage et extension le code que je n'implique pas que ce serait une alternative à boost::bimap
mais simplement une façon artisanale d'avoir un conteneur associatif consultable à la fois par la valeur et la clé.
Une implémentation possible qui utilise une structure de données intermédiaire et une indirection est:
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.
Insertion
Supposons que vous devez insérer (X, Y) où X, Y sont des instances de A et B respectivement, puis:
mapA
--- O (lg sz)mapB
--- O (lg sz)Push_back
(IterX, IterY) dans register
--- O (1). Ici, IterX et IterY sont des itérateurs de l'élément correspondant dans mapA
et mapB
et sont obtenus à partir de (1) et (2) respectivement.Recherche
Recherchez l'image d'un élément, X, de type A:
mapA
. --- O (lg n)register
et obtenir IterY correspondant. --- O (1)IterY->first
. --- O (1)Les deux opérations sont donc implémentées en O (lg n).
Espace : Toutes les copies des objets de A et B doivent être stockées une seule fois. Il y a cependant beaucoup de choses sur la comptabilité. Mais lorsque vous avez de gros objets, cela ne serait pas non plus très important.
Remarque : Cette implémentation repose sur le fait que les itérateurs d'une carte ne sont jamais invalidés. Par conséquent, le contenu de register
est toujours valide.
Une version plus élaborée de cette implémentation peut être trouvée ici
Que dis-tu de ça?
Ici, on évite le double stockage d'un type (T1). L'autre type (T2) est toujours stocké en double.
// 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_;
};