C ++ unordered_map usando un tipo di classe personalizzato come chiave


286

Sto cercando di utilizzare una classe personalizzata come chiave per un unordered_map, come il seguente:

#include <iostream>
#include <algorithm>
#include <unordered_map>

using namespace std;

class node;
class Solution;

class Node {
public:
    int a;
    int b; 
    int c;
    Node(){}
    Node(vector<int> v) {
        sort(v.begin(), v.end());
        a = v[0];       
        b = v[1];       
        c = v[2];       
    }

    bool operator==(Node i) {
        if ( i.a==this->a && i.b==this->b &&i.c==this->c ) {
            return true;
        } else {
            return false;
        }
    }
};

int main() {
    unordered_map<Node, int> m;    

    vector<int> v;
    v.push_back(3);
    v.push_back(8);
    v.push_back(9);
    Node n(v);

    m[n] = 0;

    return 0;
}

Tuttavia, g ++ mi dà il seguente errore:

In file included from /usr/include/c++/4.6/string:50:0,
                 from /usr/include/c++/4.6/bits/locale_classes.h:42,
                 from /usr/include/c++/4.6/bits/ios_base.h:43,
                 from /usr/include/c++/4.6/ios:43,
                 from /usr/include/c++/4.6/ostream:40,
                 from /usr/include/c++/4.6/iostream:40,
                 from 3sum.cpp:4:
/usr/include/c++/4.6/bits/stl_function.h: In member function bool std::equal_to<_Tp>::operator()(const _Tp&, const _Tp&) const [with _Tp = Node]’:
/usr/include/c++/4.6/bits/hashtable_policy.h:768:48:   instantiated from bool std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Equal, _H1, _H2, std::__detail::_Default_ranged_hash, false>::_M_compare(const _Key&, std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Equal, _H1, _H2, std::__detail::_Default_ranged_hash, false>::_Hash_code_type, std::__detail::_Hash_node<_Value, false>*) const [with _Key = Node, _Value = std::pair<const Node, int>, _ExtractKey = std::_Select1st<std::pair<const Node, int> >, _Equal = std::equal_to<Node>, _H1 = std::hash<Node>, _H2 = std::__detail::_Mod_range_hashing, std::__detail::_Hash_code_base<_Key, _Value, _ExtractKey, _Equal, _H1, _H2, std::__detail::_Default_ranged_hash, false>::_Hash_code_type = long unsigned int]’
/usr/include/c++/4.6/bits/hashtable.h:897:2:   instantiated from std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Node* std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_M_find_node(std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Node*, const key_type&, typename std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Hash_code_type) const [with _Key = Node, _Value = std::pair<const Node, int>, _Allocator = std::allocator<std::pair<const Node, int> >, _ExtractKey = std::_Select1st<std::pair<const Node, int> >, _Equal = std::equal_to<Node>, _H1 = std::hash<Node>, _H2 = std::__detail::_Mod_range_hashing, _Hash = std::__detail::_Default_ranged_hash, _RehashPolicy = std::__detail::_Prime_rehash_policy, bool __cache_hash_code = false, bool __constant_iterators = false, bool __unique_keys = true, std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Node = std::__detail::_Hash_node<std::pair<const Node, int>, false>, std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::key_type = Node, typename std::_Hashtable<_Key, _Value, _Allocator, _ExtractKey, _Equal, _H1, _H2, _Hash, _RehashPolicy, __cache_hash_code, __constant_iterators, __unique_keys>::_Hash_code_type = long unsigned int]’
/usr/include/c++/4.6/bits/hashtable_policy.h:546:53:   instantiated from std::__detail::_Map_base<_Key, _Pair, std::_Select1st<_Pair>, true, _Hashtable>::mapped_type& std::__detail::_Map_base<_Key, _Pair, std::_Select1st<_Pair>, true, _Hashtable>::operator[](const _Key&) [with _Key = Node, _Pair = std::pair<const Node, int>, _Hashtable = std::_Hashtable<Node, std::pair<const Node, int>, std::allocator<std::pair<const Node, int> >, std::_Select1st<std::pair<const Node, int> >, std::equal_to<Node>, std::hash<Node>, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, false, false, true>, std::__detail::_Map_base<_Key, _Pair, std::_Select1st<_Pair>, true, _Hashtable>::mapped_type = int]’
3sum.cpp:149:5:   instantiated from here
/usr/include/c++/4.6/bits/stl_function.h:209:23: error: passing const Node as this argument of bool Node::operator==(Node)’ discards qualifiers [-fpermissive]
make: *** [threeSum] Error 1

Immagino, ho bisogno di dire al C ++ come fare la classe hash Node, tuttavia, non sono sicuro di come farlo. Come posso svolgere queste attività?


2
Il terzo argomento del modello è la funzione hash che devi fornire.
chrisaycock,

4
cppreference ha un esempio semplice e pratico di come farlo: en.cppreference.com/w/cpp/container/unordered_map/unordered_map
jogojapan

Risposte:


488

Per poter utilizzare std::unordered_map(o uno degli altri contenitori associativi non ordinati) con un tipo di chiave definito dall'utente, è necessario definire due cose:

  1. Una funzione hash ; questa deve essere una classe che sovrascrive operator()e calcola il valore hash dato un oggetto del tipo chiave. Un modo particolarmente diretto per farlo è quello di specializzare il std::hashmodello per il tuo tipo di chiave.

  2. Una funzione di confronto per l'uguaglianza ; questo è necessario perché l'hash non può fare affidamento sul fatto che la funzione hash fornirà sempre un valore hash univoco per ogni chiave distinta (cioè, deve essere in grado di gestire le collisioni), quindi ha bisogno di un modo per confrontare due chiavi date per una corrispondenza esatta. Puoi implementarlo sia come una classe che sovrascrive operator(), sia come specializzazione std::equal, o - il più semplice di tutti - sovraccaricando il operator==()tuo tipo di chiave (come hai già fatto).

La difficoltà con la funzione hash è che se il tipo di chiave è composto da più membri, di solito la funzione hash calcola i valori hash per i singoli membri e quindi li combina in un valore hash per l'intero oggetto. Per una buona prestazione (cioè poche collisioni) dovresti pensare attentamente a come combinare i singoli valori di hash per evitare di ottenere lo stesso output per oggetti diversi troppo spesso.

Un punto di partenza abbastanza buono per una funzione hash è quello che utilizza bit shifting e XOR bit a bit per combinare i singoli valori di hash. Ad esempio, supponendo un tipo di chiave come questo:

struct Key
{
  std::string first;
  std::string second;
  int         third;

  bool operator==(const Key &other) const
  { return (first == other.first
            && second == other.second
            && third == other.third);
  }
};

Ecco una semplice funzione hash (adattata da quella utilizzata nell'esempio cppreference per le funzioni hash definite dall'utente ):

namespace std {

  template <>
  struct hash<Key>
  {
    std::size_t operator()(const Key& k) const
    {
      using std::size_t;
      using std::hash;
      using std::string;

      // Compute individual hash values for first,
      // second and third and combine them using XOR
      // and bit shifting:

      return ((hash<string>()(k.first)
               ^ (hash<string>()(k.second) << 1)) >> 1)
               ^ (hash<int>()(k.third) << 1);
    }
  };

}

Con questo in atto, è possibile creare un'istanza a std::unordered_mapper il tipo di chiave:

int main()
{
  std::unordered_map<Key,std::string> m6 = {
    { {"John", "Doe", 12}, "example"},
    { {"Mary", "Sue", 21}, "another"}
  };
}

Utilizzerà automaticamente std::hash<Key>come definito sopra per i calcoli del valore hash e la funzione operator==definita membro diKey per i controlli di uguaglianza.

Se non vuoi specializzare il template nello stdspazio dei nomi (anche se in questo caso è perfettamente legale), puoi definire la funzione hash come una classe separata e aggiungerla all'elenco degli argomenti del template per la mappa:

struct KeyHasher
{
  std::size_t operator()(const Key& k) const
  {
    using std::size_t;
    using std::hash;
    using std::string;

    return ((hash<string>()(k.first)
             ^ (hash<string>()(k.second) << 1)) >> 1)
             ^ (hash<int>()(k.third) << 1);
  }
};

int main()
{
  std::unordered_map<Key,std::string,KeyHasher> m6 = {
    { {"John", "Doe", 12}, "example"},
    { {"Mary", "Sue", 21}, "another"}
  };
}

Come definire una migliore funzione hash? Come detto sopra, definire una buona funzione di hash è importante per evitare collisioni e ottenere buone prestazioni. Per un vero bene è necessario prendere in considerazione la distribuzione dei possibili valori di tutti i campi e definire una funzione hash che proietta tale distribuzione su uno spazio di possibili risultati il ​​più ampio e uniformemente distribuito possibile.

Questo può essere difficile; il metodo XOR / bit-shifting di cui sopra probabilmente non è un brutto inizio. Per un inizio leggermente migliore, è possibile utilizzare il modello di funzione hash_valuee hash_combinedalla libreria Boost. Il primo agisce in modo simile a quello std::hashdei tipi standard (recentemente includendo anche tuple e altri utili tipi standard); quest'ultimo ti aiuta a combinare i singoli valori di hash in uno. Ecco una riscrittura della funzione hash che utilizza le funzioni di aiuto Boost:

#include <boost/functional/hash.hpp>

struct KeyHasher
{
  std::size_t operator()(const Key& k) const
  {
      using boost::hash_value;
      using boost::hash_combine;

      // Start with a hash value of 0    .
      std::size_t seed = 0;

      // Modify 'seed' by XORing and bit-shifting in
      // one member of 'Key' after the other:
      hash_combine(seed,hash_value(k.first));
      hash_combine(seed,hash_value(k.second));
      hash_combine(seed,hash_value(k.third));

      // Return the result.
      return seed;
  }
};

Ed ecco una riscrittura che non usa boost, ma usa un buon metodo per combinare gli hash:

namespace std
{
    template <>
    struct hash<Key>
    {
        size_t operator()( const Key& k ) const
        {
            // Compute individual hash values for first, second and third
            // http://stackoverflow.com/a/1646913/126995
            size_t res = 17;
            res = res * 31 + hash<string>()( k.first );
            res = res * 31 + hash<string>()( k.second );
            res = res * 31 + hash<int>()( k.third );
            return res;
        }
    };
}

11
Puoi spiegare perché è necessario spostare i bit KeyHasher?
Chani,

46
Se non si spostassero i bit e due stringhe fossero uguali, lo xor causerebbe la cancellazione reciproca. Quindi hash ("a", "a", 1) sarebbe uguale a hash ("b", "b", 1). Anche l'ordine non avrebbe importanza, quindi l'hash ("a", "b", 1) sarebbe uguale all'hash ("b", "a", 1).
Buge,

1
Sto solo imparando il C ++ e una cosa con cui ho sempre difficoltà è: dove mettere il codice? Ho scritto un std::hashmetodo specializzato per la mia chiave come hai fatto tu. Ho messo questo in fondo il mio file Key.cpp ma sto ottenendo il seguente errore: Error 57 error C2440: 'type cast' : cannot convert from 'const Key' to 'size_t' c:\program files (x86)\microsoft visual studio 10.0\vc\include\xfunctional. Immagino che il compilatore non stia trovando il mio metodo hash? Dovrei aggiungere qualcosa al mio file Key.h?
Ben

4
@Ben Inserirlo nel file .h è corretto. std::hashnon è in realtà una struttura, ma un modello (specializzazione) per una struttura . Quindi non è un'implementazione - verrà trasformata in un'implementazione quando il compilatore ne ha bisogno. I modelli devono sempre andare nei file di intestazione. Vedere anche stackoverflow.com/questions/495021/...
jogojapan

3
@nightfury find()restituisce un iteratore e tale iteratore punta a una "voce" della mappa. Una voce è std::paircomposta da chiave e valore. Quindi, se lo fai auto iter = m6.find({"John","Doe",12});, otterrai la chiave iter->firste il valore (cioè la stringa "example") in iter->second. Se si desidera direttamente la stringa, è possibile utilizzare m6.at({"John","Doe",12})(che genererà un'eccezione se la chiave non esce) oppure m6[{"John","Doe",12}](che creerà un valore vuoto se la chiave non esiste).
jogojapan,

16

Penso che il jogojapan abbia dato una risposta molto buona ed esauriente . Dovresti assolutamente dare un'occhiata prima di leggere il mio post. Tuttavia, vorrei aggiungere quanto segue:

  1. È possibile definire una funzione di confronto per una unordered_mapseparatamente, anziché utilizzare l'operatore di confronto di uguaglianza ( operator==). Ciò potrebbe essere utile, ad esempio, se si desidera utilizzare quest'ultimo per confrontare tutti i membri di due Nodeoggetti tra loro, ma solo alcuni membri specifici come chiave di un unordered_map.
  2. Puoi anche usare espressioni lambda invece di definire le funzioni hash e comparative.

Tutto sommato, per il tuo Node classe, il codice potrebbe essere scritto come segue:

using h = std::hash<int>;
auto hash = [](const Node& n){return ((17 * 31 + h()(n.a)) * 31 + h()(n.b)) * 31 + h()(n.c);};
auto equal = [](const Node& l, const Node& r){return l.a == r.a && l.b == r.b && l.c == r.c;};
std::unordered_map<Node, int, decltype(hash), decltype(equal)> m(8, hash, equal);

Appunti:

  • Ho appena riutilizzato il metodo di hashing alla fine della risposta di jogojapan, ma puoi trovare l'idea per una soluzione più generale qui (se non si desidera utilizzare Boost).
  • Il mio codice è forse un po 'troppo minimizzato. Per una versione leggermente più leggibile, consulta questo codice su Ideone .

da dove viene l'8 e cosa significa?
AndiChin

@WhalalalalalalaCHen: dai un'occhiata alla documentazione del unordered_mapcostruttore . Il 8rappresenta il cosiddetto "conteggio dei bucket". Un bucket è uno slot nella tabella hash interna del container, vedere ad esempio unordered_map::bucket_countper ulteriori informazioni.
suonare il clacson il

@WhalalalalalalaCHen: ho scelto 8a caso. A seconda del contenuto che si desidera archiviare nel proprio unordered_map, il conteggio dei bucket può influire sulle prestazioni del contenitore.
suonare il clacson il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.