C ++ unordered_map sử dụng loại lớp tùy chỉnh làm khóa


285

Tôi đang cố gắng sử dụng một lớp tùy chỉnh làm khóa cho một unordered_map, như sau:

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

Tuy nhiên, g ++ cho tôi lỗi sau:

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

Tôi đoán, tôi cần nói với C ++ cách băm lớp Node, tuy nhiên, tôi không chắc chắn làm thế nào để làm điều đó. Làm thế nào tôi có thể hoàn thành nhiệm vụ này?


2
Đối số mẫu thứ ba là hàm băm bạn cần cung cấp.
chrisaycock

3
cppreference có một ví dụ đơn giản và thực tế về cách thực hiện việc này: en.cppreference.com/w/cpp/container/unordered_map/unordered_map
jogojapan

Câu trả lời:


485

Để có thể sử dụng std::unordered_map(hoặc một trong các thùng chứa kết hợp không có thứ tự khác) với loại khóa do người dùng xác định, bạn cần xác định hai điều:

  1. Hàm băm ; đây phải là một lớp ghi đè operator()và tính toán giá trị băm cho một đối tượng của kiểu khóa. Một cách đặc biệt đơn giản để làm điều này là chuyên môn hóa std::hashmẫu cho loại khóa của bạn.

  2. Một hàm so sánh cho đẳng thức ; điều này là bắt buộc vì hàm băm không thể dựa vào thực tế là hàm băm sẽ luôn cung cấp một giá trị băm duy nhất cho mỗi khóa riêng biệt (nghĩa là nó cần có khả năng xử lý các va chạm), vì vậy nó cần một cách để so sánh hai khóa đã cho cho một trận đấu chính xác. Bạn có thể thực hiện điều này như là một lớp ghi đè operator(), hoặc như một chuyên môn hóa std::equal, hoặc - dễ nhất trong tất cả - bằng cách nạp chồng operator==()cho loại khóa của bạn (như bạn đã làm).

Khó khăn với hàm băm là nếu loại khóa của bạn bao gồm nhiều thành viên, bạn thường sẽ có hàm băm tính toán giá trị băm cho từng thành viên, và sau đó bằng cách nào đó kết hợp chúng thành một giá trị băm cho toàn bộ đối tượng. Để có hiệu suất tốt (nghĩa là ít va chạm), bạn nên suy nghĩ cẩn thận về cách kết hợp các giá trị băm riêng lẻ để đảm bảo bạn tránh nhận được cùng một đầu ra cho các đối tượng khác nhau quá thường xuyên.

Điểm khởi đầu khá tốt cho hàm băm là một điểm sử dụng dịch chuyển bit và XOR bitwise để kết hợp các giá trị băm riêng lẻ. Ví dụ: giả sử loại khóa như thế này:

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

Đây là một hàm băm đơn giản (được điều chỉnh từ hàm được sử dụng trong ví dụ cppreference cho các hàm băm do người dùng định nghĩa ):

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

}

Với vị trí này, bạn có thể khởi tạo một std::unordered_mapkiểu khóa:

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

Nó sẽ tự động sử dụng std::hash<Key>như được định nghĩa ở trên để tính toán giá trị băm và operator==được xác định là hàm thành viên của Keykiểm tra đẳng thức.

Nếu bạn không muốn chuyên môn hóa mẫu trong stdkhông gian tên (mặc dù trong trường hợp này là hoàn toàn hợp pháp), bạn có thể định nghĩa hàm băm là một lớp riêng biệt và thêm nó vào danh sách đối số mẫu cho bản đồ:

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

Làm thế nào để xác định hàm băm tốt hơn? Như đã nói ở trên, việc xác định hàm băm tốt là rất quan trọng để tránh va chạm và có hiệu suất tốt. Để thực sự tốt, bạn cần tính đến việc phân phối các giá trị có thể của tất cả các trường và xác định hàm băm dự kiến ​​phân phối đến một không gian kết quả có thể càng rộng và phân bố đều nhất có thể.

Điều này có thể khó khăn; phương pháp XOR / bit-shift ở trên có lẽ không phải là một khởi đầu tồi. Để bắt đầu tốt hơn một chút, bạn có thể sử dụng mẫu hash_valuehash_combinehàm từ thư viện Boost. Các hành vi trước đây theo cách tương tự như std::hashđối với các loại tiêu chuẩn (gần đây cũng bao gồm các bộ dữ liệu và các loại tiêu chuẩn hữu ích khác); cái sau giúp bạn kết hợp các giá trị băm riêng lẻ thành một. Dưới đây là viết lại hàm băm sử dụng các hàm trợ giúp 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;
  }
};

Và đây là một bản viết lại không sử dụng boost, nhưng sử dụng phương pháp kết hợp băm tốt:

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
Bạn có thể giải thích tại sao cần phải thay đổi các bit trong KeyHasher?
Chani

45
Nếu bạn không dịch chuyển các bit và hai chuỗi giống nhau, xor sẽ khiến chúng triệt tiêu lẫn nhau. Vì vậy, hàm băm ("a", "a", 1) sẽ giống như hàm băm ("b", "b", 1). Ngoài ra thứ tự sẽ không quan trọng, vì vậy băm ("a", "b", 1) sẽ giống như băm ("b", "a", 1).
Buge

1
Tôi chỉ đang học C ++ và một điều tôi luôn đấu tranh là: Đặt mã ở đâu? Tôi đã viết một std::hashphương pháp chuyên biệt cho khóa của tôi như bạn đã làm. Tôi đặt phần này ở dưới cùng của tệp Key.cpp nhưng tôi gặp lỗi sau : 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. Tôi đoán rằng trình biên dịch không tìm thấy phương thức băm của tôi? Tôi có nên thêm bất cứ điều gì vào tệp Key.h của mình không?
Ben

4
@Ben Đưa nó vào tệp .h là chính xác. std::hashkhông thực sự là một cấu trúc, mà là một khuôn mẫu (chuyên môn hóa) cho một cấu trúc . Vì vậy, nó không phải là một triển khai - nó sẽ được chuyển thành một triển khai khi trình biên dịch cần nó. Các mẫu phải luôn đi vào các tệp tiêu đề. Xem thêm stackoverflow.com/questions/495021/
Mạnh

3
@nightfury find()trả về một iterator và iterator đó trỏ đến một "mục" của bản đồ. Một mục là std::pairbao gồm khóa và giá trị. Vì vậy, nếu bạn làm như vậy auto iter = m6.find({"John","Doe",12});, bạn sẽ nhận được khóa iter->firstvà giá trị (tức là chuỗi "example") iter->second. Nếu bạn muốn chuỗi trực tiếp, bạn có thể sử dụng m6.at({"John","Doe",12})(điều đó sẽ đưa ra một ngoại lệ nếu khóa không thoát) hoặc m6[{"John","Doe",12}](điều đó sẽ tạo ra một giá trị trống nếu khóa không tồn tại).
jogojapan

16

Tôi nghĩ rằng, jogojapan đã đưa ra một câu trả lời rất hay và đầy đủ . Bạn chắc chắn nên xem nó trước khi đọc bài viết của tôi. Tuy nhiên, tôi muốn thêm vào như sau:

  1. Bạn có thể xác định hàm so sánh cho unordered_mapriêng, thay vì sử dụng toán tử so sánh đẳng thức ( operator==). Điều này có thể hữu ích, ví dụ, nếu bạn muốn sử dụng cái sau để so sánh tất cả các thành viên của hai Nodeđối tượng với nhau, nhưng chỉ một số thành viên cụ thể là chìa khóa của một unordered_map.
  2. Bạn cũng có thể sử dụng biểu thức lambda thay vì xác định hàm băm và so sánh.

Nói chung, đối với Nodelớp của bạn , mã có thể được viết như sau:

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

Ghi chú:

  • Tôi vừa sử dụng lại phương pháp băm vào cuối câu trả lời của jogojapan, nhưng bạn có thể tìm thấy ý tưởng cho một giải pháp tổng quát hơn ở đây (nếu bạn không muốn sử dụng Boost).
  • Mã của tôi có thể là một chút quá nhỏ. Để có phiên bản dễ đọc hơn một chút, vui lòng xem mã này trên Ideone .

8 đến từ đâu và có nghĩa là gì?
AndiChin

@WhalalalalalalaCHen: Vui lòng xem tài liệu của nhà unordered_mapxây dựng . Các 8đại diện cho cái gọi là "số lượng xô". Một nhóm là một vị trí trong bảng băm bên trong của bộ chứa, xem ví dụ unordered_map::bucket_countđể biết thêm thông tin.
bấm còi

@WhalalalalalalaCHen: Tôi chọn 8ngẫu nhiên. Tùy thuộc vào nội dung bạn muốn lưu trữ trong số của bạn unordered_map, số lượng xô có thể ảnh hưởng đến hiệu suất của container.
bấm còi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.