std::unordered_map(提供自己的Hash函数和等价准则)

来源:互联网 发布:网页美工设计工资多少 编辑:程序博客网 时间:2024/06/09 17:26

在使用容器std::unordered_map< key, value >时,当key是内置类型或者std::string时,容器都能正常使用,而且由于查找时间为O(1),在编程时,特别适合充当hash_table来使用。

如果key是自定义类型时,直接使用std::unordered_map,编译时会报错,错误信息为:”error C2338: The C++ Standard doesn’t provide a hash for this type.”大意是,C++标准库没有为该类型提供hash操作!因此,针对自定义类型,我们在使用std::unordered_map时必须提供自定义的Hash函数

注:以下内容全部参考和引用自《C++标准库》(第二版)

提供自己的Hash函数

所有的hash table都需要一个hash函数,把你放进去的元素的value映射至某个相关的bucket(注:标准库的unordered_map,底层实现是基于hashtable的,其避免冲突的方法是使用开链(seperate chaining)法,这种做法是在每一个表格元素中维护一个list,每个表格元素称之为buket(桶),如下图(摘自《STL源码剖析》))。
这里写图片描述
它的目标是,两个相等的value总是导致相同的bucket索引,而不同的value理应导致不同的bucket索引。对于任何范围内的(被传入的)value,hash函数应该提供良好的hash value分布。

Hash函数必须是个函数,或function object,它接收一个元素类型下的value作为参数,并返回一个类型为std::size_t的value。因此,bucket的当前数量并未考虑。将其返回值映射至合法的bucket索引范围内,是由容器内部完成。因此,你的目标是提供一个函数,可以把不同的元素值均匀映射至区间[0, size_t)内

下面示范的是如何提供你自己的hash函数:

#include <functional>class Customer{    ...};class CustomerHash{public:    std::size_t operator()(const Customer& c) const    {        return ...    }};

在这里,CustomerHash是一个function object,为class Customer定义出hash函数。

如果不愿意传递一个function object成为容器的一部分,你也可以传递一个hash函数作为构造函数实参。然而请注意,hash函数相应的template类型也必须对应设妥:

std::size_t customer_hash_func(const Customer& c){    return ...}std::unordered_set<Customer, std::size_t(*)(const Customer&)>     custset(20, customer_hash_func);

在这里,customer_hash_func()被传递为构造函数第二个实参,其类型为”一个pointer,指向某函数,该函数接受一个Customer并返回一个std::size_t”,作为第二template实参。

如果没有给予特殊的hash函数,默认的hash函数是hash<>,这是< functional >提供的一个function object,可以对付常见类型:包括所有整数类型、所有浮点数类型、pointer、std::string,以及若干特殊类型。这些之外的类型,就必须提供你自己的hash函数。

提供一个好的hash函数,说起来容易做起来难。就像搭便车一样,可以使用默认的hash函数来完成自己的hash函数。一个天真(naive)的做法是,单纯把那些数据栏”由默认之hash函数产生”的所有hash value加起来。举个例子:

class CustomerHash{public:    std::size_t operator()(const Customer& c) const    {        return  std::hash<std::string>()(c.fname) +                    std::hash<std::string>()(c.lname) +                    std::hash<long>()(c.no);    }};

这里,返回的hash value只不过是Customer的数据栏fname、lname和no的hash value总和。如果预定义的所有hash函数对这些数据栏的类型以及所给予的值都能运作良好,那么三个值的总和必然也在[0, size_t)范围内。根据一般溢出规则(common overflow rule),上述结果值应该也能够有良好的分布。

然而专家认为,这仍然是粗劣的hash函数。提供一个良好的hash函数可能是十分棘手的事,似乎不如想象中那么轻松。

一个较好的做法如下,使用由Boost提供的hash函数和一个便利的接口

#include <functional>template<typename T>inline void hash_combine(std::size_t& seed, const T& val){    seed ^= std::hash<T>()(val)+0x9e3779b9 + (seed << 6) + (seed >> 2);}template<typename T>inline void hash_val(std::size_t& seed, const T& val){    hash_combine(seed, val);}template<typename T, typename... Types>inline void hash_val(std::size_t& seed, const T& val, const Types&... args){    hash_combine(seed, val);    hash_val(seed, args...);}template<typename... Types>inline std::size_t hash_val(const Types& ...args){    std::size_t seed = 0;    hash_val(seed, args...);    return seed;}

这里实现出一个辅助函数hash_val(),使用variadic template(可变参数模板),允许调用时给予任意数量、任意类型的元素,然后逐一个别处理(计算)hash value。例如:

class CustomerHash{public:    std::size_t operator()(const Customer& c) const    {        return  hash_val(c.fname, c.lname, c.no);    }};

在其内部,hash_combine()会被调用。若干实验证明,它是”一般性hash函数”的优秀候选。

提供自己的等价准则(Equivalence Criterion)

作为unordered容器类型的第三(set)或第四(map)template参数,可以传递等价准则(equivalence criterion),那应该是一个predicate(判别式),用以找出同一个bucket内的相等value。默认使用的是equal_to<>, 它以operator==进行比较。基于此,提供合法等价准则的最方便做法就是为自己的类型提供operator==(如果它没有预先被定义为成员函数或全局函数)。例如:

class Customer{    ...};bool operator==(const Customer& c1, const Customer& c2){    ...}std::unordered_set<Customer, CustomerHash> custset;std::unordered_map<Customer, std::string, CustomHash> custmap;

当然,也可以提供自己的等价准则,例如:

#include <functional>class Customer{    ...};class CustomerEqual{public:    bool operator()(const Customer& c1, const Customer&c2) const    {        return ...    }};std::unordered_set<Customer, CustomerHash, CustomerEqual> custset;std::unordered_map<Customer, std::string, CustomHash, CustomerEqual> custmap;

这里针对类型Customer定义了一个function object,必须在其中实现operator()使它能够比较两个元素(对map而言是两个key)并返回一个bool值指示他们是否相等。

只要value在当前的等价准则下被视为相等,他们也应该在当前的hash函数下产生相同的hash value。基于这个原因,一个unordered容器如果被实例化时带有一个非默认的等价准则,通常也需要一个非默认的hash函数。

提供自己的Hash函数和等价准则

下面的程序展示了如何为类型Customer定义及指定一个hash函数和一个等价准则。

#include <functional>template<typename T>inline void hash_combine(std::size_t& seed, const T& val){    seed ^= std::hash<T>()(val)+0x9e3779b9 + (seed << 6) + (seed >> 2);}template<typename T>inline void hash_val(std::size_t& seed, const T& val){    hash_combine(seed, val);}template<typename T, typename... Types>inline void hash_val(std::size_t& seed, const T& val, const Types&... args){    hash_combine(seed, val);    hash_val(seed, args...);}template<typename... Types>inline std::size_t hash_val(const Types& ...args){    std::size_t seed = 0;    hash_val(seed, args...);    return seed;}class Customer{public:    Customer(const std::string& fn, const std::string& ln, long no)        : fname(fn), lname(ln), no(no)    {    }    friend std::ostream& operator<<(std::ostream& strm, const Customer& c)    {        return strm << "[" << c.fname << "," << c.lname << ","            << c.no << "]";    }    friend class CustomerHash;    friend class CustomerEqual;private:    std::string fname;    std::string lname;    long            no;};class CustomerHash{public:    std::size_t operator()(const Customer& c) const    {        return  hash_val(c.fname, c.lname, c.no);    }};class CustomerEqual{public:    bool operator()(const Customer& c1, const Customer& c2) const    {        return c1.no == c2.no;    }};int main(){    std::unordered_map<Customer, int, CustomerHash, CustomerEqual> custmap;    custmap.insert(std::pair<Customer, int>(Customer("nico", "journalist", 42), 1));    std::cout << custmap[Customer("nico", "journalist", 42)] << std::endl;    return 0;}

使用Lambda作为Hash函数和等价准则

使用lambda具体指定hash函数和/或等价准则,例如:

...class Customer{public:    Customer(const std::string& fn, const std::string& ln, long no)        : fname(fn), lname(ln), no(no)    {    }    std::string firstname() const    {        return fname;    }    std::string lastname() const    {        return lname;    }    long number() const    {        return no;    }    friend std::ostream& operator<<(std::ostream& strm, const Customer& c)    {        return strm << "[" << c.fname << "," << c.lname << ","            << c.no << "]";    }private:    std::string fname;    std::string lname;    long            no;};int main(){    auto hash = [](const Customer& c)    {        return  hash_val(c.firstname(), c.lastname(), c.number());    };    auto eq = [](const Customer& c1, const Customer& c2)    {        return c1.number() == c2.number();    };    std::unordered_map<Customer, int, decltype(hash) , decltype(eq)> custmap(10, hash, eq);    custmap.insert(std::pair<Customer, int>(Customer("nico", "journalist", 42), 1));    std::cout << custmap[Customer("nico", "journalist", 42)] << std::endl;    return 0;}

在VS2013中,以上代码会报错,错误信息为:”C3497: 无法构造 lambda 实例”,出现该错误的原因是->“lambda 的默认构造函数被隐式删除”,因此,在VS2013里无法使用Lambda函数作为Hash函数和等价准则。

修改办法如下,将Hash函数和等价准则作为普通函数,以函数指针的形式作为构造函数的一部分,具体如下:

...std::size_t hash(const Customer& c){    return  hash_val(c.firstname(), c.lastname(), c.number());};bool eq(const Customer& c1, const Customer& c2){    return c1.number() == c2.number();};int main(){    std::unordered_map<Customer, int, decltype(&hash) , decltype(&eq)> custmap(10, hash, eq);    custmap.insert(std::pair<Customer, int>(Customer("nico", "journalist", 42), 1));    std::cout << custmap[Customer("nico", "journalist", 42)] << std::endl;}

修改后的代码,可以正常运行。但有一个需要特别注意的地方,decltype(hash)与decltype(&hash)返回的类型是不一样的,取获取函数指针的方式为后者,这里我们是需要函数指针的,因此需要选择后者(decltype(&hash)),如果选择前者会导致运行错误。

原创粉丝点击