hashtable总结

本文详细介绍了哈希表(hashtable)的内部结构,包括使用分离链接法解决冲突,节点由链表组成,每个桶内存储链表节点。hashtable迭代器为前向迭代器,不支持后退操作,且提供了插入、resize等功能。在内存管理方面,插入操作会根据元素数量动态调整表的大小,释放旧数组时采用swap技巧。此外,还讨论了hash函数的实现,包括内置的hash函数和自定义hash仿函数的必要性。
摘要由CSDN通过智能技术生成

hashtable的节点和桶子

1.hashtable使用分离链接法来解决散列冲突。表的结构是vector,每个元素时一个桶,桶里放的是一个链表。
2.bucket所维护的链表,并不使用标准库的list和slist结构,而是新定义了一个数据结构:

template <class Value>
struct __hashtable_node
{
	__hashtable_node* next;
	Value val;
}

hashtable的迭代器

1.迭代器类型是forward_iterator(和forward_list类型一样)。它没有后退操作。 也没有逆向迭代器。

2.迭代器的模板参数是:

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
  • Value:节点的实值型别。和红黑树一样,它其实也是键值对
  • Key:节点的键值型别
  • HashFcn:hash function的函数型别
  • ExtractKey:从节点中取出键值的方式(仿函数)
  • EqualKey:判断键值相同与否的方法
  • Alloc:空间配置器,缺省使用std::alloc

3.有两个指针成员。cur指针指向当前节点。ht指针指向hashtable容器,作为迭代器和容器的连接。因为迭代器递增的时候,可能需要从一个桶跳到另一个桶。
operator++()函数中,能看到迭代器走到一个桶的尾巴的情况:

if (!cur) {
	size_type bucket = ht->bkt_num(old->val);
	while (!cur && ++bucket < ht->buckets.size())
		cur = ht->buckets[bucket];
}

先用bkt_num函数得到当前迭代器所在的bucket号,然后递增号码,取出下一个桶的链表头节点。如果这个头节点是空的,说明这个桶里面没有元素,接着查看下一个桶。直到头节点不为空,返回curr。

4.如上所示,迭代器的递增行为就是移动到链表的下一个节点;如果已经走到了链表的尾巴,就需要走到下一个有元素的桶,接着沿链表往下走。

hashtable的数据结构

1.模板参数和迭代器相同。

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>

2.数据成员有5个:HashFcn、ExtractKey、EqualKey默认构造的对象,和buckets数组,以及这个数组的大小。

3.标准库的表格大小为质数。标准库已经预先储存了28个质数(逐渐呈现大约2倍的关系),以备随时访问。储存的最大的质数是4294967291,所以桶数量最多也就是这么多。

标准库的__stl_next_prime函数用来计算,不小于n并且最接近n的那个质数。

hashtable的构造与内存管理

1.不提供默认构造函数。

2.其中的一个构造函数是:

hashtable(size_type n, const HashFcn& hf, const EqualKey& eql)
 : hash(hf), equals(eql), get_key(ExtractKey()), num_elements(0)
{
	initialize_buckets(n);
}

void initialize_buckets(size_type n)
{
	const size_type n_buckets = next_size(n);
	buckets.reserve(n_buckets);
	buckets.insert(buckets.end(), n_buckets, (node*_ 0);
	num_elements = 0;
}

它接受三个参数,节点数量n,HashFcn类实例化的一个对象,EqualKey类实例化的一个对象。注意初始化get_key的是ExtractKey的默认构造函数构造的对象。而表中元素的数量初始化为0。
initialize_buckets函数中,next_size函数获得的就是储存的质数中大于等于n的最小的那个,把它作为桶的数量n_buckets。然后把buckets数组的capacity调整为n_buckets。并将vector的每个元素都设置为空指针

3.插入操作。和红黑树类似,插入操作也有insert_equalinsert_unique之分。

4.resize()函数。

上面两种插入操作,在真正插入前都要先判断是不是需要重新调整哈希表的大小。判断标准似乎有些奇特:如果元素个数(把新增元素计入)超过了buckets数组的大小,就重新计算表格的大小。如果计算出来新的表格大小比原来的大(一般情况下当然会增大,除了一种特殊情况,就是旧的数组大小是最大的4294967291,它没法继续增大了),就需要重新建立一个新的buckets数组,把旧数组中的元素放进新的数组。

注意上面所说的“放进”,不需要在新的数组中重新构造元素,而只需要修改原来的链表的指针指向。于是也只需要将旧的数组释放掉,旧的链表节点都还在,只不过连接到了新的地方。

释放旧的数组也很特别,新构造的数组是temp,将它和旧的数组进行swap,于是数据结构中的buckets就成了新的数组,而temp会随着函数调用结束自动释放空间,相当于将旧的数组释放了。

5.insert_unique_noresize函数。

在resize后,insert_unique会调用insert_unique_noresize来进行真正的插入操作。bkt_num函数用来计算应该放进哪个桶中。然后要判断这个桶里面是不是已经放了这个元素,如果是,就直接返回;否则,构造新的链表节点,把节点插入这个桶的链表头(链表插入都是这样的,往头上插,不会忘尾巴上插)。

和红黑树的插入类似,返回值也是一个pair类型,第一个成员是插入后指向这个元素的迭代器,第二个成员是bool类型,用来反映插入是否成功。如果原来这个元素就已经放在桶里面了,就是失败,否则就是成功。

6.insert_equal_noresize函数

在resize后,insert_equal会调用insert_equal_noresize来进行真正的插入操作。它和insert_unique_noresize的区别在于,它如果发现桶里面已经有要插入的元素了,仍然会构造一个节点,插入到这个重复存在的节点后面

7.判断元素应该放在哪一个桶里,是用bkt_num函数实现的。这个函数先调用hash function处理元素,然后再模n,这个n可以作为参数传入,否则n就默认是buckets数组的大小

SGI标准库内建了一些hash functions,它们都是仿函数。在p268能看到它们的定义。能看到,对于像int、long、char等整数型别,hash functions什么也不做,就是直接返回其自身;但是对于字符字符串(const char*),就设计了一个转换函数

inline size_t __stl_hash_string(const char* s)
{
	unsigned long h = 0;
	for ( ; *s; ++s)
		h = 5 * h + *s;
	return size_t(h);
}

标准库在这里使用了泛化和偏特化的方法,首先定义一个泛化版本的hash函数,然后定义多个偏特化的版本:

/* 泛化版本 */
template <class Key> struct hash {};

/* 一个偏特化版本 */
template<> hash<char*>
{
	size_t operator() (const char* s) const {return __stl_hash_string(s);}
}

/* 另一个偏特化版本 */
template<> hash<int>
{
	size_t operator() (int x) const {return x;}
};

可以看出,标准库默认支持的键的类型包括整数类型(int、long、char)等,以及字符串类型。标准库没有提供其他类型的hash特化版本,要想使用其他类型的键,用户必须提供自定义的仿函数。比如对于pair类型,网上能找到的最常见的一个仿函数设计:

struct pair_hash
{
    template<class T1, class T2>
    std::size_t operator() (const std::pair<T1, T2>& p) const
    {
        auto h1 = std::hash<T1>{}(p.first);
        auto h2 = std::hash<T2>{}(p.second);
        return h1 ^ h2;
    }
};

7.hashtable的复制和整体删除

由于使用链表结构,所以在复制和删除的时候都需要注意内存空间的释放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值