C ++ unororder_map, використовуючи в якості ключа спеціальний тип класу


285

Я намагаюся використовувати спеціальний клас як ключ для 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

3
У cppreference є простий і практичний приклад того, як це зробити: en.cppreference.com/w/cpp/container/unordered_map/unordered_map
jogojapan

Відповіді:


487

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

  1. Хеш - функція ; це повинен бути клас, який переосмислює operator()та обчислює значення хеша, заданого об’єктом типу key. Один із особливо прямих способів цього - спеціалізувати 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;
        }
    };
}

11
Чи можете ви поясніть, чому потрібно переміщувати біти KeyHasher?
Chani

45
Якщо ви не змістили біти і дві струни були однаковими, 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}](що створить порожнє значення, якщо ключа не існує).
jogojapan

16

Думаю, jogojapan дав дуже гарну та вичерпну відповідь . Ви, безумовно, повинні поглянути на це, перш ніж прочитати мій пост. Однак я хочу додати наступне:

  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Являє собою так званий «лічильник відро». Відро - це гніздо у внутрішній хеш-таблиці контейнера, unordered_map::bucket_countдля отримання додаткової інформації див.
хан

@WhalalalalalalaCHen: Я вибрав 8навмання. Залежно від вмісту, який ви хочете зберегти у вашому unordered_map, кількість відра може вплинути на продуктивність контейнера.
хрюк
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.