Anahtar olarak özel bir sınıf türü kullanan C ++ unordered_map


286

unordered_mapAşağıdaki gibi bir için anahtar olarak özel bir sınıf kullanmaya çalışıyorum :

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

Ancak, g ++ bana aşağıdaki hatayı verir:

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

Sanırım, nasıl C sınıfı hash sınıfına söylemek gerekir Node, ancak, nasıl yapılacağından emin değilim. Bu görevleri nasıl yapabilirim?


2
Üçüncü şablon argümanı sen sağlamanız gerekmektedir karma işlevidir.
chrisaycock

3
cppreference'ın bunun nasıl yapılacağına dair basit ve pratik bir örneği vardır: en.cppreference.com/w/cpp/container/unordered_map/unordered_map
jogojapan

Yanıtlar:


488

Kullanım edebilmek std::unordered_mapkullanıcı tanımlı anahtar türü ile (ya da diğer sırasız ilişkisel kapların biri), iki şeyi tanımlamak için gerekmez:

  1. Bir karma işlevi ; bu operator()anahtar türünde bir nesne verilen karma değerini geçersiz kılan ve hesaplayan bir sınıf olmalıdır . Bunu yapmanın özellikle basit bir yolu, std::hashşablonu anahtar türünüz için uzmanlaştırmaktır .

  2. Eşitlik için bir karşılaştırma işlevi ; bu gereklidir, çünkü hash, hash fonksiyonunun her farklı anahtar için her zaman benzersiz bir hash değeri sağlayacağına güvenemez (yani, çarpışmalarla başa çıkabilmesi gerekir), bu nedenle verilen iki anahtarı karşılaştırmanın bir yolu gerekir kesin bir eşleşme için. Bunu, anahtar türünüz için aşırı yükleyerek (daha önce yaptığınız gibi) geçersiz kılan bir sınıf operator()olarak veya bir uzmanlaşma olarak std::equalveya - hepsinden daha kolay - olarak uygulayabilirsiniz operator==().

Karma işlevinin zorluğu, anahtar türünüz birkaç üyeden oluşuyorsa, genellikle karma işlevinin tek tek üyeler için karma değerlerini hesaplaması ve daha sonra bunları bir şekilde tüm nesne için bir karma değerinde birleştirmenizdir. İyi performans için (yani, birkaç çarpışma), farklı nesneler için aynı çıktıyı çok sık elde etmekten kaçınmak için bireysel karma değerlerini nasıl birleştireceğinizi dikkatlice düşünmelisiniz.

Bir karma işlevi için oldukça iyi bir başlangıç ​​noktası, bireysel karma değerlerini birleştirmek için bit kaydırma ve bitsel XOR kullanan bir başlangıç ​​noktasıdır. Örneğin, aşağıdaki gibi bir anahtar türü varsayarsak:

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

Basit bir karma işlevi ( kullanıcı tanımlı karma işlevler için cppreference örneğinde kullanılandan uyarlanmıştır ):

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

}

Bu yerdeyken std::unordered_map, anahtar türü için a başlatabilirsiniz :

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

std::hash<Key>Karma değer hesaplamaları için yukarıda operator==tanımlandığı gibi Keyve eşitlik denetimleri için üye işlevi olarak tanımlanan şekilde otomatik olarak kullanılır .

Şablonu stdad alanı içinde uzmanlaştırmak istemiyorsanız (bu durumda tamamen yasal olmasına rağmen), hash işlevini ayrı bir sınıf olarak tanımlayabilir ve haritanın şablon bağımsız değişken listesine ekleyebilirsiniz:

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

Daha iyi bir karma işlevi nasıl tanımlanır? Yukarıda belirtildiği gibi, iyi bir hash fonksiyonu tanımlamak, çarpışmaları önlemek ve iyi performans elde etmek için önemlidir. Gerçek bir iyi için, tüm alanların olası değerlerinin dağılımını dikkate almanız ve bu olası sonuçları mümkün olduğunca geniş ve eşit olarak dağıtılmış bir alana yansıtan bir karma fonksiyonu tanımlamanız gerekir.

Bu zor olabilir; yukarıdaki XOR / bit kaydırma yöntemi muhtemelen kötü bir başlangıç ​​değildir. Biraz daha iyi bir başlangıç için Boost kütüphanesindeki hash_valueve hash_combinefonksiyon şablonunu kullanabilirsiniz . Birincisi std::hash, standart tiplerle (son zamanlarda tuples ve diğer faydalı standart tipler dahil) benzer şekilde hareket eder ; ikincisi, bireysel karma değerlerini tek bir değerde birleştirmenize yardımcı olur. Boost yardımcı işlevlerini kullanan karma işlevinin yeniden yazılması:

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

Ve burada boost kullanmayan, ancak hash'ları birleştirmek için iyi bir yöntem kullanan bir yeniden yazma:

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
Bitleri neden kaydırmanın gerekli olduğunu açıklayabilir misiniz KeyHasher?
Chani

45
Bitleri değiştirmediyseniz ve iki dize aynı olsaydı, xor onların birbirlerini iptal etmelerine neden olur. Yani karma ("a", "a", 1) karma ("b", "b", 1) ile aynı olur. Ayrıca düzen de önemli olmaz, bu yüzden karma ("a", "b", 1) karma ("b", "a", 1) ile aynı olur.
Buge

1
Ben sadece C ++ öğreniyorum ve her zaman mücadele bir şey: nereye kodu koymak? std::hashYaptığın gibi anahtarım için özel bir yöntem yazdım . Benim Key.cpp dosyasının altındaki bu koyun ama aşağıdaki hatayı alıyorum: 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. Derleyici hash yöntemimi bulamadığını tahmin ediyorum? Key.h dosyama bir şey eklemeli miyim?
Ben

4
@Ben .h dosyasına koymak doğrudur. std::hashaslında bir yapı değil, bir yapı için bir şablon (uzmanlaşma) . Dolayısıyla bu bir uygulama değildir - derleyici buna ihtiyaç duyduğunda bir uygulamaya dönüştürülecektir. Şablonlar her zaman başlık dosyalarına gitmelidir. Ayrıca bkz stackoverflow.com/questions/495021/…
jogojapan

3
@nightfury find()bir yineleyici döndürür ve yineleyici haritanın bir "girişini" gösterir. Bir giriş, std::pairanahtar ve değerden oluşur. Böylece bunu yaparsanız auto iter = m6.find({"John","Doe",12});, anahtarı iter->firstve değeri (yani dize "example") alırsınız iter->second. Dizeyi doğrudan istiyorsanız, m6.at({"John","Doe",12})(anahtar çıkmazsa bir istisna atar) veya m6[{"John","Doe",12}](anahtar yoksa boş bir değer oluşturur) kullanabilirsiniz.
jogojapan

16

Bence jogojapan çok iyi ve kapsamlı bir cevap verdi . Gönderiyi okumadan önce kesinlikle bir göz atmalısınız. Ancak, aşağıdakileri eklemek istiyorum:

  1. unordered_mapEşitlik karşılaştırma işlecini ( operator==) kullanmak yerine, ayrı bir karşılaştırma işlevi tanımlayabilirsiniz . Örneğin, iki Nodenesnenin tüm üyelerini birbiriyle karşılaştırmak için ikincisini kullanmak istiyorsanız , ancak yalnızca bazı belirli üyelerin anahtara anahtarı olarak kullanılması yararlı olabilir unordered_map.
  2. Karma ve karşılaştırma işlevlerini tanımlamak yerine lambda ifadeleri de kullanabilirsiniz .

Sonuç olarak, Nodesınıfınız için kod aşağıdaki gibi yazılabilir:

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

Notlar:

  • Jogojapan'ın cevabının sonunda hashing yöntemini tekrar kullandım, ancak burada daha genel bir çözüm fikri bulabilirsiniz (Boost kullanmak istemiyorsanız).
  • Kodum belki biraz fazla küçültülmüş. Biraz daha okunabilir bir sürüm için lütfen Ideone'daki bu koda bakın .

8 nereden geldi ve bu ne anlama geliyor?
AndiChin

@WhalalalalalalaCHen: bakmak Lütfen belgelenmesi unordered_mapyapıcısı . Burada 8"kova sayısı" denir. Bir kova, kabın dahili karma tablosundaki bir yuvadır, unordered_map::bucket_countdaha fazla bilgi için bkz .
honk

@WhalalalalalalaCHen: 8Rastgele seçtim . İçinde saklamak istediğiniz içeriğe bağlı olarak, unordered_mapkova sayısı kabın performansını etkileyebilir.
honk
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.