本文分析的是 https://github.com/llvm-mirror/libcxx/ 中截止至 2016 年 1 月 30 日最新的 libc++
。
libc++
中, hashtable
的实现为链式结构。 在教科书(Introduction To Algorithm 3rd Edition
)中,介绍的实现是由一个数组作为buckets
,每个数组中存储一个链表。但是 libc++
中,使用一个单向链表贯穿整个 hashtable
,每个 slot
存储的是上一个元素结点的指针,这个元素充当当前链表的头结点。也就是说,每个 slot
的链表存储的实际上是一个左开右闭的区间。在 libc++
中,实现链表普遍使用了一个技巧:有一个基类 xxx_node_base
,这个里面仅存储指向下一个(或者还有上一个)结点的指针,而真正的 xxx_node
里面才有元素。选用 xxx_node_base
作为头结点,这样就减少了单个元素的内存占用。在 hashtable
中,可以找到:
template <class _NodePtr>
struct __hash_node_base
{
typedef __hash_node_base __first_node;
_NodePtr __next_;
_LIBCPP_INLINE_VISIBILITY __hash_node_base() _NOEXCEPT : __next_(nullptr) {}
};
template <class _Tp, class _VoidPtr>
struct __hash_node
: public __hash_node_base
<
typename __rebind_pointer<_VoidPtr, __hash_node<_Tp, _VoidPtr> >::type
>
{
typedef _Tp value_type;
size_t __hash_;
value_type __value_;
};
以下是 hashtable
的关键成员变量:
typedef unique_ptr<__node_pointer[], __bucket_list_deleter> __bucket_list;
// --- Member data begin ---
__bucket_list __bucket_list_;
__compressed_pair<__first_node, __node_allocator> __p1_;
__compressed_pair<size_type, hasher> __p2_;
__compressed_pair<float, key_equal> __p3_;
// --- Member data end ---
可以看到:
1. buckets
是一个 node_pointer
的数组;
2. __p1_.first()
就是上文中说的没有元素的头结点;
3. __p2_.first()
是现在的元素个数;
4. __p3_.first()
是现在的 load factor
。
在下文的代码中为了便于理解,__p1_first()
替换为 head_node
。
看过 rehash
函数后,就会对整个 hashtable
有个大致的印象。其大致思路为:若当前结点(cp
或许指的是 current pointer
?)与前一个结点的哈希值相同则不用理会——让它跟着前一个结点走好了。否则就说明我们找到了边界,那么就在这个 hashtable
的链表最前面插入当前结点。这时候分两种情况。第一种是那个 slot
是空的,那么就把当前结点对应的新 slot
更新为整个链表的头结点,把原来以整个链表的头结点为局部链表头结点的 slot
更新为这个结点的指针(这样就形成了开区间),第二种是非空,那么直接插入就好了。但是这里有个小优化:从 current_node
开始,把所有相等元素的结点同时插入进去,由于是链表,这个操作可以 O(1)
完成!另外,这个 hashtable
会把每个元素的 hash value
都缓存下来,这样在 rehash
的时候无需再次 hash
:
template <class _Tp, class _Hash, class _Equal, class _Alloc>
void
__hash_table<_Tp, _Hash, _Equal, _Alloc>::__rehash(size_type __nbc)
{
#if _LIBCPP_DEBUG_LEVEL >= 2
__get_db()->__invalidate_all(this); //在 rehash 后,需要 invalidate 所有相关 iterator
#endif // _LIBCPP_DEBUG_LEVEL >= 2
__pointer_allocator& __npa = __bucket_list_.get_deleter().__alloc();
__bucket_list_.reset(__nbc > 0 ?
__pointer_alloc_traits::allocate(__npa, __nbc) : nullptr); //分配新的 buckets
__bucket_list_.get_deleter().size() = __nbc;
if (__nbc > 0)
{
for (size_type __i = 0; __i < __nbc; ++__i)
__bucket_list_[__i] = nullptr; //先将每个 slot 置为 nullptr
__node_pointer previous_node(static_cast<__node_pointer>(pointer_traits<__node_base_pointer>::pointer_to(head_node)));
//从头结点开始遍历
__node_pointer current_node = previous_node->__next_;
if (current_node != nullptr)
{
//这是刚开始,需要特殊对待:先将当前结点(即紧挨着头结点的第一个结点)的 slot 更新
size_type __chash = __constrain_hash(current_node->__hash_, __nbc);
__bucket_list_[__chash] = previous_node;
size_type __phash = __chash;
for (previous_node = current_node, current_node = current_node->__next_; current_node != nullptr;
current_node = previous_node->__next_)
{
__chash = __constrain_hash(current_node->__hash_, __nbc);
if (__chash == __phash)
//如果当前结点的新哈希值和上一个一样,那么就跟着上一个走好了(上一个结点已经更新就位)
previous_node = current_node;
else
{
if (__bucket_list_[__chash] == nullptr)
{
//slot 里面什么都没有,那么根据上文说的“左开右闭”,把局部链表的头结点设置为上一个结点
__bucket_list_[__chash] = previous_node;
previous_node = current_node;
__phash = __chash;
}
else
{
//如果非空,那么一并将元素相等的一串[current_node, new_node]插入进去,这个操作可以瞬间完成!
__node_pointer new_node = current_node;
for (; new_node->__next_ != nullptr &&
key_eq()(current_node->__value_, new_node->__next_->__value_);
new_node = new_node->__next_)
;
previous_node->__next_ = new_node->__next_;
new_node->__next_ = __bucket_list_[__chash]->__next_;
__bucket_list_[__chash]->__next_ = current_node;
}
}
}
}
}
}
了解了 rehash
的思路,那么 insert
也就十分明了了:如果插入后的新 load factor
比 max_load_factor
大了,就 rehash
。(由于这个是 insert_unique
,所以一旦发现重复的就不管了) 如果待插入的 slot
是空,那么把新结点插入到最前面,然后整个链表的头结点设置为这个 slot
的头结点,然后将原先以 head_node
为头结点的局部链表的头结点更新为这个新插入的结点(这个新插入的结点在它们前面,实现了左开右闭),如果 slot
非空,就直接插入好了:
template <class _Tp, class _Hash, class _Equal, class _Alloc>
pair<typename __hash_table<_Tp, _Hash, _Equal, _Alloc>::iterator, bool>
__hash_table<_Tp, _Hash, _Equal, _Alloc>::__node_insert_unique(__node_pointer __nd)
{
__nd->__hash_ = hash_function()(__nd->__value_);
size_type __bc = bucket_count();
bool __inserted = false;
__node_pointer __ndptr;
size_t __chash;
if (__bc != 0)
{
__chash = __constrain_hash(__nd->__hash_, __bc);
__ndptr = __bucket_list_[__chash];
if (__ndptr != nullptr)
{
//通过比较当前结点值的哈希值与要插入结点的哈希值是否相等来找到边界,老把戏了。
for (__ndptr = __ndptr->__next_; __ndptr != nullptr &&
__constrain_hash(__ndptr->__hash_, __bc) == __chash;
__ndptr = __ndptr->__next_)
{
if (key_eq()(__ndptr->__value_, __nd->__value_))
goto __done;
}
}
}
{
if (size()+1 > __bc * max_load_factor() || __bc == 0)
{
rehash(_VSTD::max<size_type>(2 * __bc + !__is_hash_power2(__bc),
size_type(ceil(float(size() + 1) / max_load_factor()))));
__bc = bucket_count();
__chash = __constrain_hash(__nd->__hash_, __bc);
}
// insert_after __bucket_list_[__chash], or __first_node if bucket is null
__node_pointer __pn = __bucket_list_[__chash];
if (__pn == nullptr)
{
__pn = static_cast<__node_pointer>(pointer_traits<__node_base_pointer>::pointer_to(head_node));
__nd->__next_ = __pn->__next_;
__pn->__next_ = __nd;
// fix up __bucket_list_
__bucket_list_[__chash] = __pn;
if (__nd->__next_ != nullptr)
__bucket_list_[__constrain_hash(__nd->__next_->__hash_, __bc)] = __nd;
}
else
{
__nd->__next_ = __pn->__next_;
__pn->__next_ = __nd;
}
__ndptr = __nd;
// increment size
++size();
__inserted = true;
}
__done:
#if _LIBCPP_DEBUG_LEVEL >= 2
return pair<iterator, bool>(iterator(__ndptr, this), __inserted);
#else
return pair<iterator, bool>(iterator(__ndptr), __inserted);
#endif
}