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

3
cppreference มีตัวอย่างที่เรียบง่ายและใช้งานได้จริงของวิธีการทำเช่นนี้: en.cppreference.com/w/cpp/container/unordered_map/unordered_map
jogojapan

คำตอบ:


487

เพื่อให้สามารถใช้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 / bit-shift ข้างต้นอาจไม่ใช่จุดเริ่มต้นที่ไม่ดี เพื่อการเริ่มต้นที่ดีขึ้นเล็กน้อยคุณสามารถใช้เทมเพลตhash_valueและhash_combineฟังก์ชันจากไลบรารี Boost การกระทำในอดีตในลักษณะที่คล้ายกันstd::hashสำหรับประเภทมาตรฐาน (เมื่อเร็ว ๆ นี้รวมถึงสิ่งอันดับและประเภทมาตรฐานที่มีประโยชน์อื่น ๆ ) ส่วนหลังช่วยให้คุณรวมค่าแฮชแต่ละรายการไว้ในที่เดียว นี่คือการเขียนซ้ำของฟังก์ชันแฮชที่ใช้ฟังก์ชัน Boost helper:

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

และนี่คือการเขียนที่ไม่ได้ใช้การเพิ่มประสิทธิภาพ แต่ใช้วิธีการที่ดีในการรวมแฮช:

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 จะทำให้พวกเขายกเลิกกัน ดังนั้น hash ("a", "a", 1) จะเหมือนกับ hash ("b", "b", 1) คำสั่งจะไม่สำคัญดังนั้น hash ("a", "b", 1) จะเหมือนกับ hash ("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ไม่จริง struct แต่แม่แบบ (เชี่ยวชาญ) สำหรับ struct ดังนั้นมันจึงไม่ใช่การนำไปใช้ - มันจะกลายเป็นการนำไปใช้เมื่อคอมไพเลอร์ต้องการมัน เทมเพลตควรเข้าไปในไฟล์ส่วนหัวเสมอ ดู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สำหรับข้อมูลเพิ่มเติม
honk

@WhalalalalalalaChen: ฉันเลือก8แบบสุ่ม unordered_mapจำนวนที่ฝากข้อมูลอาจมีผลต่อประสิทธิภาพของคอนเทนเนอร์ทั้งนี้ขึ้นอยู่กับเนื้อหาที่คุณต้องการเก็บไว้ใน
honk
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.