web-dev-qa-db-fra.com

Existe-t-il une implémentation plus efficace pour une carte bidirectionnelle?

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.

50
Vittorio Romeo

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 et std::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*>> et std::set<pair<B, A*>> et surcharger le operator< et operator== 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 que A et B seront toujours aux mêmes positions en mémoire. Lors de l'insertion d'un pair<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_casts.

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.

20
example

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.

17
Arkaitz Jimenez

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.

14
jrok

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é.

exemple en direct

6
Nikos Athanasiou

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:

  1. Insérer (X, sz) dans mapA --- O (lg sz)
  2. Insérer (Y, sz) dans mapB --- O (lg sz)
  3. Ensuite 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:

  1. Obtenez l'int entier mappé sur X dans mapA. --- O (lg n)
  2. Utilisez l'int pour indexer dans register et obtenir IterY correspondant. --- O (1)
  3. Une fois que vous avez IterY, vous pouvez obtenir Y par 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

4
divkakwani

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_;
};
1
Arun