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

286

Я пытаюсь использовать пользовательский класс в качестве ключа для 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, однако я не совсем уверен, как это сделать. Как я могу выполнить эту задачу?

Альфред Чжун
источник
2
Третий аргумент шаблона является хэш - функция , то необходимо предоставить.
chrisaycock
4
У cppreference есть простой и практичный пример того, как это сделать: en.cppreference.com/w/cpp/container/unordered_map/unordered_map
jogojapan

Ответы:

489

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

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

  2. Функция сравнения на равенство ; это необходимо, поскольку хеш-функция не может полагаться на тот факт, что хеш-функция всегда будет предоставлять уникальное хеш-значение для каждого отдельного ключа (т. е. она должна иметь возможность обрабатывать коллизии), поэтому ей необходим способ сравнения двух заданных ключей. для точного соответствия. Вы можете реализовать это либо как класс, который переопределяет 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;
  }
};

И вот переписывание, которое не использует 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;
        }
    };
}
jogojapan
источник
11
Не могли бы вы объяснить, почему необходимо сдвинуть биты KeyHasher?
Чани
46
Если вы не сдвинули биты и две строки были одинаковыми, xor заставил бы их взаимно уничтожать друг друга. Таким образом, хэш ("a", "a", 1) будет таким же, как хеш ("b", "b", 1). Также порядок не имеет значения, поэтому хеш ("a", "b", 1) будет таким же, как хеш ("b", "a", 1).
Buge
1
Я только изучаю C ++, и с чем я всегда борюсь: где разместить код? Я написал специальный std::hashметод для моего ключа, как вы сделали. Я положил это в нижней части моего файла Key.cpp , но я получаю следующее сообщение об ошибке: 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. Я предполагаю, что компилятор не находит мой метод хеширования? Должен ли я что-нибудь добавить в мой файл Key.h?
Бен
4
@Ben Правильно положить его в файл .h. std::hashна самом деле это не структура, а шаблон (специализация) для структуры . Так что это не реализация - она ​​будет превращена в реализацию, когда это понадобится компилятору. Шаблоны всегда должны идти в заголовочные файлы. См. Также stackoverflow.com/questions/495021/…
jogojapan
3
@nightfury find()возвращает итератор, и этот итератор указывает на «запись» карты. Запись std::pairсостоит из ключа и значения. Так что если вы это сделаете auto iter = m6.find({"John","Doe",12});, вы получите ключ iter->firstи значение (то есть строку "example") в iter->second. Если вы хотите получить строку напрямую, вы можете использовать ее m6.at({"John","Doe",12})(которая вызовет исключение, если ключ не m6[{"John","Doe",12}]завершится ), или (которая создаст пустое значение, если ключ не существует).
Джогоджапан
16

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

  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 .
сигналить
источник
откуда взялись 8 и что это значит?
AndiChin
@WhalalalalalalaCHen: Пожалуйста, ознакомьтесь с документацией unordered_mapконструктора . 8Представляет собой так называемый «счетчик ведро». Bucket - это слот во внутренней хеш-таблице контейнера, см., Например, unordered_map::bucket_countдополнительную информацию.
Гудок
@WhalalalalalalaCHen: Я выбрал 8наугад. В зависимости от содержимого, которое вы хотите сохранить unordered_map, количество сегментов может повлиять на производительность контейнера.
Грн