STL hashtable杂谈
Hashtable在C++的STL里占据着比较重要的一席之地。其中的hash_set、hash_map、hash_multiset、hash_multimap四个关联容器都是以hashtable为底层实现方法(技巧)。应该说,上述的四个关联式容器提供的api都是对hashtable原生态api的高层封装,因为hashtable本身都提供了它们所需要的基础api。接下来,说说自己对hashtable实现技巧的理解和总结吧!
Hashtable底层实现是通过开链法来实现的,hash table表格内的元素称为桶(bucket),而由桶所链接的元素称为节点(node),其中存入桶元素的容器为stl本身很重要的一种序列式容器——vector容器。之所以选择vector为存放桶元素的基础容器,主要是因为vector容器本身具有动态扩容能力,无需人工干预。而节点元素为自定义的结构体:
template<class Value>
struct __hashtable_node{
__hashtable_node* next;
Value val;
};
可以看到,这本身就是一种很典型的链式列表元素的表示方法,通过当前节点,我们可以很方便地通过节点自身的next指针来获取下一链接节点元素。
在hashtable的实现过程中,我们会认识到一个专有名词:负载系数(loading factor),意指元素个数除以表格大小。很显然,通过开链法,负载系数会大于1。同样在开链方法中,用于装载桶元素的vector容器大小恒定为一个质数大小,在具体实现中表现为28个从小到大的28个质数,如:53、97、193、389。。。等等。每次重新分配vector容器大小时,总是将新容器大小设定为第一个大于当前需要的新容器大小的质数值(上述28个质数中的其中一个)。接下来,我们来谈谈hashtable中几个比较重要的原生态api的实现原理吧。具体的实现技巧建议查看stl源码。
resize(...)方法:此api主要用于判断当前装载桶元素的vector容器是否需要重建(或者说是扩容),api的唯一参数即为当前桶元素所链接所有节点个数,即链表节点个数(包括当前桶元素),假定为new_num。将new_num值与当前装载桶元素vector容器的大小进行比较,如果大于则进行容器扩容,依次将所有的节点元素都根据其本身的hash值放置到新的容器对应的桶元素所在的链表中,每次都是从头部进入插入操作,如果小于或者等于,则直接插入到其本身属于的桶元素所在的链表头部中。从中我们可以看到每个桶元素所在的链表的元素个数最多为装载桶元素的vector容器的容量。否则就要进行重建工作。
insert_unique_noresize(...)方法:此api主要用于往hashtable中插入新值,同时新值不允许重复,否则就不插入,立刻返回。事实上,在正式开始执行此方法之前,首先需要运行前一个api:resize()方法,确定vector容器是否需要重建。需要的话则执行,否则再是执行当前api。原理很简单,第一步,根据待插入的新值运行内置函数:bkt_num(new_value),此方法主要是用于确定当前新值归属于哪个桶元素所在的链表中,方便之后插入操作。获取其所在的桶元素之后,依次遍历当前桶元素所在的链表,检查是否有与待插入的新值重复的元素,如果有,则直接退出当前函数,如果没有,则将待插入的新值插入当前链表的头部,完成新值的插入过程。
insert_equal(...)方法与insert_unique_noresize()原理类似,唯一不同的地方只是可以插入重复值,即若发现相同值,则直接将待插入新值插入到当前节点的后面即可。否则仍然插入到当前链表的头部位置。探究到这里,想必大家都已经知道呢,hashtable内置的api实现方式并不是很复杂,无非主要是对链表的操作,比如查找、插入,删除等操作。像clear(...)函数实现方式也主要就是依次删除各个桶元素所在的链表中的节点元素即可。
经过上述分析和总结,可以看到,虽然stl本身实现很复杂,但是其实里面真正的实现细节并非我们大多数人想像的那样不可理解,只要大家敢于跨出第一步(探究源码的第一步),相信离真像就会越来越近。最后,以《stl 源码剖析》一书前言的一句经典结束今天的hashtable的探索之旅吧!——源码之前,了无秘密!与大家共勉吧!