カスタムクラスタイプをキーとして使用するC ++ unordered_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 2013年

3
cppreferenceには、これを行う方法の簡単で実用的な例があります。en.cppreference.com
cpp

回答:


485

std::unordered_mapユーザー定義のキータイプで(または他の順序付けされていない連想コンテナの1つ)を使用できるようにするには、次の2つを定義する必要があります。

  1. ハッシュ関数。これoperator()は、キータイプのオブジェクトが指定されたハッシュ値をオーバーライドして計算するクラスでなければなりません。これを行う特に簡単な方法の1つは、std::hashテンプレートをキータイプに特化することです。

  2. 平等のための比較関数。これは、ハッシュ関数がすべての個別のキーに対して常に一意のハッシュ値を提供する(つまり、衝突に対処できる必要がある)という事実にハッシュが依存できないために必要であり、指定された2つのキーを比較する方法が必要です。完全一致。これは、をオーバーライドするクラスとしてoperator()、またはの特殊化としてstd::equal、または-最も簡単な方法として- operator==()(すでに行ったように)キータイプのオーバーロードによって実装できます。

ハッシュ関数の難しさは、キータイプが複数のメンバーで構成されている場合、通常はハッシュ関数で個々のメンバーのハッシュ値を計算し、何らかの方法でそれらをオブジェクト全体の1つのハッシュ値に結合することです。良好なパフォーマンス(つまり、衝突が少ない)を実現するには、個々のハッシュ値を組み合わせる方法を慎重に検討して、異なるオブジェクトに対して同じ出力が頻繁に取得されないようにする必要があります。

ハッシュ関数のかなり適切な開始点は、ビットシフトとビット単位の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 /ビットシフト方法は、おそらく悪いスタートではありません。少し良いスタートとして、Boostライブラリのhash_valueand hash_combine関数テンプレートを使用できます。前者はstd::hash標準タイプの場合と同様に機能します(最近ではタプルやその他の有用な標準タイプも含まれています)。後者は、個々のハッシュ値を1つに結合するのに役立ちます。以下は、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か?
チャニー2014

45
ビットをシフトせず、2つの文字列が同じである場合、xorはそれらを互いにキャンセルします。したがって、hash( "a"、 "a"、1)はhash( "b"、 "b"、1)と同じになります。また、順序は関係ないため、hash( "a"、 "b"、1)はhash( "b"、 "a"、1)と同じになります。
2014

1
私はC ++を学習しているだけで、常に苦労していることの1つは、どこにコードを配置するかということです。あなたがしたように、私は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 2015

16

jogojapanは非常に優れた包括的な回答を提供したと思います。私の投稿を読む前に、ぜひ確認してください。ただし、以下を追加します。

  1. unordered_map等価比較演算子(operator==)を使用する代わりに、個別に比較関数を定義できます。これは、たとえば、後者を使用して2つの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を使用したくない場合)。
  • 私のコードは多分少々少なすぎます。もう少し読みやすいバージョンについては、Ideonでこのコードを参照しください。

8はどこから来たのですか、それはどういう意味ですか?
AndiChin

@WhalalalalalalalaCHen:コンストラクタのドキュメントをunordered_mapご覧ください。8いわゆる「バケット・カウント」を表しています。バケットは、コンテナの内部ハッシュテーブルのスロットunordered_map::bucket_countです。詳細については、例を参照してください。
クラクション

@WhalalalalalalaCHen:私8はランダムに選んだ。に保存するコンテンツによってunordered_mapは、バケット数がコンテナのパフォーマンスに影響を与える可能性があります。
クラクション
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.