C ++ unordered_map usando um tipo de classe personalizado como a chave

285

Estou tentando usar uma classe personalizada como chave para um unordered_map, como o seguinte:

#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;
}

No entanto, o g ++ me fornece o seguinte erro:

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

Eu acho que, eu preciso dizer ao C ++ como classe de hash Node, no entanto, não tenho muita certeza de como fazê-lo. Como posso realizar essas tarefas?

Alfred Zhong
fonte
2
O terceiro argumento do modelo é a função de hash que você precisa fornecer.
chrisaycock
3
cppreference tem um simples e exemplo prático de como fazer isso: en.cppreference.com/w/cpp/container/unordered_map/unordered_map
jogojapan

Respostas:

485

Para poder usar std::unordered_map(ou um dos outros contêineres associativos não ordenados) com um tipo de chave definido pelo usuário, você precisa definir duas coisas:

  1. Uma função de hash ; deve ser uma classe que substitua operator()e calcule o valor do hash, dado um objeto do tipo de chave. Uma maneira particularmente direta de fazer isso é especializar o std::hashmodelo para o seu tipo de chave.

  2. Uma função de comparação para igualdade ; isso é necessário porque o hash não pode confiar no fato de que a função hash sempre fornecerá um valor de hash exclusivo para cada chave distinta (ou seja, precisa ser capaz de lidar com colisões), portanto, é necessário uma maneira de comparar duas chaves fornecidas para uma correspondência exata. Você pode implementar isso como uma classe que substitui operator(), ou como uma especialização de std::equal, ou - o mais fácil de tudo - sobrecarregando operator==()seu tipo de chave (como você já fez).

A dificuldade com a função de hash é que, se o seu tipo de chave consistir em vários membros, você normalmente terá a função de hash para calcular valores de hash para os membros individuais e, de alguma forma, combiná-los em um valor de hash para o objeto inteiro. Para um bom desempenho (ou seja, poucas colisões), você deve pensar cuidadosamente sobre como combinar os valores de hash individuais para garantir que você evite obter a mesma saída para objetos diferentes com muita frequência.

Um ponto de partida bastante bom para uma função de hash é aquele que usa XOR de deslocamento de bits e bit a bit para combinar os valores individuais de hash. Por exemplo, assumindo um tipo de chave como este:

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);
  }
};

Aqui está uma função hash simples (adaptada da usada no exemplo cppreference para funções hash definidas pelo usuário ):

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);
    }
  };

}

Com isso, você pode instanciar a std::unordered_mappara o tipo de chave:

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

Ele será usado automaticamente std::hash<Key>como definido acima para os cálculos de valor de hash e o operator==definido como função de membro Keypara verificações de igualdade.

Se você não deseja se especializar no modelo dentro do stdnamespace (embora seja perfeitamente legal nesse caso), defina a função hash como uma classe separada e adicione-a à lista de argumentos do modelo para o mapa:

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"}
  };
}

Como definir uma melhor função de hash? Como dito acima, é importante definir uma boa função de hash para evitar colisões e obter um bom desempenho. Para um valor realmente bom, é necessário levar em consideração a distribuição dos possíveis valores de todos os campos e definir uma função de hash que projete essa distribuição em um espaço de resultados possíveis da forma mais ampla e distribuída possível.

Isso pode ser difícil; o método XOR / deslocamento de bits acima provavelmente não é um começo ruim. Para um início um pouco melhor, você pode usar o modelo de função hash_valuee hash_combineda biblioteca Boost. O primeiro atua de maneira semelhante std::hashà dos tipos padrão (incluindo recentemente tuplas e outros tipos padrão úteis); o último ajuda a combinar valores individuais de hash em um. Aqui está uma reescrita da função hash que usa as funções auxiliares do 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;
  }
};

E aqui está uma reescrita que não usa impulso, mas usa um bom método de combinar os hashes:

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;
        }
    };
}
jogojapan
fonte
11
Você pode explicar por que é necessário mudar os bits KeyHasher?
Chani
45
Se você não trocasse os bits e duas cordas fossem iguais, o xor faria com que elas se cancelassem. Portanto, o hash ("a", "a", 1) seria o mesmo que o hash ("b", "b", 1). A ordem também não importa, portanto o hash ("a", "b", 1) seria o mesmo que o hash ("b", "a", 1).
Buge
1
Estou apenas aprendendo C ++ e uma coisa com a qual sempre luto é: onde colocar o código? Eu escrevi um std::hashmétodo especializado para minha chave, como você fez. Eu coloquei isso na parte inferior do meu arquivo Key.cpp mas estou recebendo o seguinte erro: 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. Eu estou supondo que o compilador não está encontrando meu método hash? Devo adicionar algo ao meu arquivo Key.h?
217 Ben
4
@ Ben Colocá-lo no arquivo .h está correto. std::hashnão é realmente uma estrutura, mas um modelo (especialização) para uma estrutura . Portanto, não é uma implementação - será transformada em uma implementação quando o compilador precisar. Os modelos sempre devem ir para os arquivos de cabeçalho. Veja também stackoverflow.com/questions/495021/…
jogojapan
3
@nightfury find()retorna um iterador e esse apontador aponta para uma "entrada" do mapa. Uma entrada é std::paircomposta por chave e valor. Então, se você fizer isso auto iter = m6.find({"John","Doe",12});, receberá a chave iter->firste o valor (ou seja, a string "example") iter->second. Se você deseja a string diretamente, pode usar m6.at({"John","Doe",12})(isso gerará uma exceção se a chave não sair) ou m6[{"John","Doe",12}](que criará um valor vazio se a chave não existir).
jogojapan
16

Eu acho que o jogojapan deu uma resposta muito boa e exaustiva . Você definitivamente deve dar uma olhada antes de ler meu post. No entanto, gostaria de adicionar o seguinte:

  1. Você pode definir uma função de comparação para uma unordered_mapseparadamente, em vez de usar o operador de comparação de igualdade ( operator==). Isso pode ser útil, por exemplo, se você quiser usá-lo para comparar todos os membros de dois Nodeobjetos entre si, mas apenas alguns membros específicos como chave de um unordered_map.
  2. Você também pode usar expressões lambda em vez de definir as funções de hash e comparação.

Em resumo, para sua Nodeclasse, o código pode ser escrito da seguinte maneira:

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);

Notas:

  • Acabei de reutilizar o método de hash no final da resposta do jogojapan, mas você pode encontrar a idéia para uma solução mais geral aqui (se você não quiser usar o Boost).
  • Meu código talvez seja um pouco minificado demais. Para uma versão um pouco mais legível, consulte este código no Ideone .
buzinar
fonte
de onde vieram os 8 e o que isso significa?
AndiChin 14/02
@WhalalalalalalaCHen: Por favor, dê uma olhada na documentação do unordered_mapconstrutor . O 8representa a chamada "contagem de baldes". Um balde é um slot na tabela de hash interna do contêiner, consulte, por exemplo, unordered_map::bucket_countpara obter mais informações.
honk
@WhalalalalalalaCHen: Escolhi 8aleatoriamente. Dependendo do conteúdo que você deseja armazenar unordered_map, a contagem de buckets pode afetar o desempenho do contêiner.
honk