С++ unordered_map с использованием настраиваемого типа класса в качестве ключа

Я пытаюсь использовать пользовательский класс в качестве ключа для unordered_map, как unordered_map ниже:

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

Однако g++ дает мне следующую ошибку:

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

Я думаю, мне нужно рассказать C++, как хэшировать класс Node, однако я не совсем уверен, как это сделать. Как я могу выполнить эту задачу?

Ответ 1

Чтобы иметь возможность использовать std::unordered_map (или один из других неупорядоченных ассоциативных контейнеров) с определенным пользователем типом ключа, вам необходимо определить две вещи:

  • A хэш-функция; это должен быть класс, который переопределяет operator() и вычисляет значение хеширования, заданное объектом типа ключа. Один особенно прямой способ сделать это - специализировать шаблон std::hash для вашего типа ключа.

  • A функция сравнения для равенства; это необходимо, потому что хеш не может полагаться на то, что хеш-функция всегда будет предоставлять уникальное значение хэша для каждого отдельного ключа (т.е. он должен иметь возможность иметь дело с коллизиями), поэтому ему нужен способ сравнения двух заданных ключей для точного соответствия. Вы можете реализовать это как класс, переопределяющий operator(), или как специализацию std::equal, или – самый легкий из всех – перегружая operator==() для вашего типа ключа (как вы уже делали).

Трудность с хэш-функцией заключается в том, что если ваш тип ключа состоит из нескольких членов, вы обычно будете иметь хеш-функцию для вычисления хэш-значений для отдельных членов, а затем каким-то образом объединить их в одно значение хэша для всего объекта. Для хорошей производительности (т.е. Нескольких коллизий) вы должны тщательно подумать о том, как объединить отдельные хеш-значения, чтобы вы не слишком часто получали одинаковый вывод для разных объектов.

Хорошей отправной точкой для хеш-функции является функция, которая использует смещение бит и побитовое XOR для объединения отдельных хеш-значений. Например, предположив такой тип ключа:

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

Вот простая хеш-функция (адаптированная из той, которая используется в примере cppreference для пользовательских хеш-функций):

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

}

С помощью этого места вы можете создать экземпляр std::unordered_map для ключевого типа:

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

Он автоматически использует std::hash<Key>, как определено выше, для вычислений хеш-значений, а operator== определяется как функция-член Key для проверок равенства.

Если вы не хотите специализировать шаблон внутри пространства имен std (хотя в этом случае он совершенно легальный), вы можете определить хэш-функцию как отдельный класс и добавить его в список аргументов шаблона для карты:

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

Как определить лучшую хэш-функцию? Как было сказано выше, определение хорошей хэш-функции важно, чтобы избежать столкновений и получить хорошую производительность. Для действительно хорошего вам нужно учитывать распределение возможных значений всех полей и определять хеш-функцию, которая прогнозирует распределение в пространстве возможных результатов, как можно более широкое и равномерно распределенное.

Это может быть сложно; метод XOR/бит-сдвига выше, вероятно, не плохой старт. Для немного лучшего начала вы можете использовать шаблон функции hash_value и hash_combine в библиотеке Boost. Первый действует аналогично std::hash для стандартных типов (в последнее время также включает кортежи и другие полезные стандартные типы); последний помогает вам комбинировать отдельные хеш-значения в одном. Ниже приведена перепись хеш-функции, использующей вспомогательные функции 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;
  }
};

И heres переписывает, который не использует boost, но использует хороший метод объединения хэшей:

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

Ответ 2

Я думаю, джогоджапан дал очень хороший и исчерпывающий ответ. Вы обязательно должны взглянуть на это, прежде чем читать мой пост. Тем не менее, я хотел бы добавить следующее:

  1. Вы можете определить функцию сравнения для unordered_map отдельно, вместо использования оператора сравнения равенства (operator==). Это может быть полезно, например, если вы хотите использовать последний для сравнения всех элементов двух объектов Node друг с другом, но только некоторых конкретных элементов в качестве ключа unordered_map.
  2. Вы также можете использовать лямбда-выражения вместо определения хеш-функций и функций сравнения.

В целом, для вашего класса Node код может быть написан следующим образом:

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

Заметки:

  • Я просто повторно использовал метод хеширования в конце ответа jogojapan, но вы можете найти идею для более общего решения здесь (если вы не хотите использовать Boost).
  • Мой код, возможно, слишком минимизирован. Для немного более читаемой версии, пожалуйста, смотрите этот код на Ideone.

Ответ 3

Я думаю, что хранение адрес класса способ

MyClass a, b;

unordered_set<MyClass *> s;
s.insert(&a);
cout << s.count(&a) << s.count(&b);