聊聊c++ Hashtable(二)

接着上一篇,我们继续来聊聊Hashtable的实现,包括以下三个方面:

1、rehash的时机、条件、大小选择

2、bucket index的计算及hash函数的使用

3、multihashtable

相关代码我都加了注释、同时只保留了关键代码,方便大家理解,关键部分还会画图来说明。作为开头,还是先放一张整体结构图。

  首先看下自动rehash的时机。在insert时会调用need_rehash进行判断,条件成立,则rehash。

cpp 复制代码
iterator _M_insert_unique_node(size_type __bkt, __hash_code __code,__node_type* __node){
    const __rehash_state& __saved_state = _M_rehash_policy._M_state();
    //看看是否需要rehash
    std::pair<bool, std::size_t> __do_rehash
        = _M_rehash_policy._M_need_rehash(_M_bucket_count, _M_element_count, 1);
    if (__do_rehash.first){
        _M_rehash(__do_rehash.second, __saved_state);
    }
}

那么什么条件下会需要rehash呢?这里涉及到Hashtable中的负载因子、最大负载因子、最大元素数、bucket个数、当前元素数等概念。最大负载因子默认为1.0,负载因子是为了防止Hashtable中元素过多,导致某个bucket元素过多,降低查找效率。

最大元素数 = 最大负载因子 * bucket个数;

负载因子 = 当前元素数 / bucket个数;

cpp 复制代码
std::pair<bool, std::size_t> _M_need_rehash(std::size_t __n_bkt, std::size_t __n_elt,std::size_t __n_ins) const{
    if (__n_elt + __n_ins >= _M_next_resize){
        long double __min_bkts = (__n_elt + __n_ins)
        / (long double)_M_max_load_factor;
        //当前元素数 + 增长个数 大等于最大元素数并且按照此时元素数量计算出来的min_bkts大等于当前bucket时,需要进行rehash。
        if (__min_bkts >= __n_bkt)
            //期望的bucket数量=max(min_bkts + 1,bkt * _S_growth_factor),_其中S_growth_factor默认为2。也就是取了当前bkt数量加倍后与min_bkts + 1之间的最大值。
            //可以看到这里也是翻倍扩容,但具体细节又和vector的翻倍扩容存在差别。
            return std::make_pair(true,
            _M_next_bkt(std::max<std::size_t>(__builtin_floor(__min_bkts) + 1,
            __n_bkt * _S_growth_factor)));
    }
}

rehash时,期望的bucket确定了,那么实际的bucket数量又是多少呢?这里涉及到了hash冲突的避免,当hash函数的分母为质数时,hash的冲突相对会比较小,hash后的数据会分布的更加均匀,所以实际的bucket数量需要是质数,具体要取:最小的大等于期望bucket的质数。libstdc++中预先计算好了用到的质数,以数组的形式存在了__prime_list中,其中的质数都是递增的。那么最大的质数会到多少呢?这是通过一个常量形式的质数个数来控制的,保存在_S_n_primes中。为什么会有最大质数的限制呢?因为32位无符号数的最大值为4,294,967,295,32位和64位系统中unsigned long表示的范围不同,为了防止溢出,需要控制质数的范围。

cpp 复制代码
enum { _S_n_primes = sizeof(unsigned long) != 8 ? 256 : 256 + 48 };
cpp 复制代码
// Return a prime no smaller than n.
std::size_t _Prime_rehash_policy::_M_next_bkt(std::size_t __n) const{
    // Optimize lookups involving the first elements of __prime_list.
    // (useful to speed-up, eg, constructors)
    static const unsigned char __fast_bkt[12]
        = { 2, 2, 2, 3, 5, 5, 7, 7, 11, 11, 11, 11 };

    //期望bucket数量小于等于11时,直接使用fast_bkt,根据索引查表得到实际的bucket数量。
    if (__n <= 11){
        _M_next_resize =
        __builtin_ceil(__fast_bkt[__n] * (long double)_M_max_load_factor);
        return __fast_bkt[__n];
    }

    //期望bucket大于11时,因为prime_list是排好序的,所以可以使用二分搜索,在prime_list中查找不小于n的第一个数。
    const unsigned long* __next_bkt =
        std::lower_bound(__prime_list + 5, __prime_list + _S_n_primes, __n);
    _M_next_resize =
        __builtin_ceil(*__next_bkt * (long double)_M_max_load_factor);
    return *__next_bkt;
}

看到这里,其实下面这张整体结构图更符合实际,不知道大家有没有看出和文章开头那张图的区别,欢迎留言交流。

  再看下bucket index的计算及hash函数的使用,首先看下bucket index的计算。是通过类型对应的hash code和bucket count取模得到的。

cpp 复制代码
//num为类型对应的hash code,den为bucket count.
result_type operator()(first_argument_type __num, second_argument_type __den) const{ 
    return __num % __den; 
}

那么不同类型的hash code是怎么计算的呢,这里使用了std::hash模版类。对不同的类型,其实现不同。可以看到下面的代码,对于整型数,c++默认把其本身作为hash code;对于指针,c++默认把其地址作为hash code。

cpp 复制代码
/// Partial specializations for pointer types.
template<typename _Tp>
struct hash<_Tp*> : public __hash_base<size_t, _Tp*>
{
    size_t
    operator()(_Tp* __p) const noexcept
    { return reinterpret_cast<size_t>(__p); }
};

// Explicit specializations for integer types.
#define _Cxx_hashtable_define_trivial_hash(_Tp) 	\
template<>						\
struct hash<_Tp> : public __hash_base<size_t, _Tp>  \
{                                                   \
    size_t                                            \
    operator()(_Tp __val) const noexcept              \
    { return static_cast<size_t>(__val); }            \
};

对于string以及自定义结构体,其hash code是怎么计算的呢?string在basic_string.h有hash的特化版本。其内部使用了Murmur hash来计算hash code。对于自定义类型,需要实现std::hash的特化版本才能使用Hashtable,不然会出现编译期错误。

cpp 复制代码
template<> 
struct hash<string>: public __hash_base<size_t, string>{
    size_t operator()(const string& __s) const noexcept{ 
        return std::_Hash_impl::hash(__s.data(), __s.length()); 
    }
};

最后再来说说multihashtable,什么是multihashtable呢?首先其也是hashtable,上面讲过的特性也适用于它;不同的是它可能存在相同的node。multihashtable因为其node相同的特性,所以相同的node总是在同一个bucket,并且总是连续的。

  至此,Hashtable中的关键实现都讲完了,c++标准库中的unordered_set、unordered_map、unordered_multiset、unordered_multimap都是基于Hashtable实现的。其中map是把key和value作为一个pair保存的,set只保存了key(也就是其value),这是两者的不同。有兴趣的话大家可以自己去看下源码。

随着我们对某一个事物理解的不断加深,我们之前的理解也在被不断推翻。

公众号原文链接 聊聊c++ Hashtable(二)

曾经沧海难为水,除却巫山不是云

相关推荐
程序员大金4 分钟前
基于SpringBoot+Vue+MySQL的智能物流管理系统
java·javascript·vue.js·spring boot·后端·mysql·mybatis
ac-er88882 小时前
在Flask中处理后台任务
后端·python·flask
ac-er88882 小时前
Flask中的钩子函数
后端·python·flask
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS在线文档管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
Flying_Fish_roe3 小时前
Spring Boot-版本兼容性问题
java·spring boot·后端
摇滚侠3 小时前
spring cxf 常用注解
java·后端·spring
许野平4 小时前
Rust:Result 和 Error
开发语言·后端·rust·error·result
晓晨CH4 小时前
Spring考点总结
java·后端·spring
惜.己5 小时前
MyBatis中一对多关系的两种处理方法
java·开发语言·后端·sql·mysql·mybatis·idea
新知图书5 小时前
Django后台管理复杂模型
后端·python·django