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

在使用容器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)),如果选择前者会导致运行错误。

  • 13
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值