前言
在上一小节中,我们介绍了hashtable
的基本概念,以及分析了在STL中,hashtable
的迭代器、采用的数据结构及它的构造/析构函数,还有部分成员函数。
在本小节中,我们讲继续分析剩下的一些操作,如插入、删除、复制等。
插入操作
关于插入操作,有大量的重载版本,我们只分析比较关键的函数,其他的可以随便看看。
/* insert_unique,插入元素(不允许重复)
* 首先先判断加上新增元素之后,需不需要重新建表(关于判断是否需要重新建表的标准,我们放在下面分析)
* 然后调用insert_unique_noresize进行插入
*/
pair<iterator, bool> insert_unique(const value_type& obj)
{
resize(num_elements + 1);
return insert_unique_noresize(obj);
}
/* 插入元素(允许重复的元素)
* 可以看到调用的插入元素的函数成了insert_equal_noresize
*/
iterator insert_equal(const value_type& obj)
{
resize(num_elements + 1);
return insert_equal_noresize(obj);
}
//定义
pair<iterator, bool> insert_unique_noresize(const value_type& obj);
iterator insert_equal_noresize(const value_type& obj);
/* 重载版本,范围插入
* 看看就好,主要还是调用的resize还有对应的插入函数
*/
......
void insert_unique(const value_type* f, const value_type* l)
{
size_type n = l - f;
resize(num_elements + n);
for ( ; n > 0; --n, ++f)
insert_unique_noresize(*f);
}
void insert_equal(const value_type* f, const value_type* l)
{
size_type n = l - f;
resize(num_elements + n);
for ( ; n > 0; --n, ++f)
insert_equal_noresize(*f);
}
void insert_unique(const_iterator f, const_iterator l)
{
size_type n = 0;
distance(f, l, n);
resize(num_elements + n);
for ( ; n > 0; --n, ++f)
insert_unique_noresize(*f);
}
void insert_equal(const_iterator f, const_iterator l)
{
size_type n = 0;
distance(f, l, n);
resize(num_elements + n);
for ( ; n > 0; --n, ++f)
insert_equal_noresize(*f);
}
.......
/* insert_unique调用的真正实施插入操作的函数
* 首先通过btk_num获取到放入hashtable的位置
* 然后遍历该链表
* 依次调用equals判断key是否相等
* 由于不允许重复,所以如果有与obj的key相等的直接返回
* 并将pair的第一个元素置成指向该重复的节点的迭代器,第二个元素置为false
* 没有重复的才执行插入操作
*/
template <class V, class K, class HF, class Ex, class Eq, class A>
pair<typename hashtable<V, K, HF, Ex, Eq, A>::iterator, bool>
hashtable<V, K, HF, Ex, Eq, A>::insert_unique_noresize(const value_type& obj)
{
const size_type n = bkt_num(obj);
node* first = buckets[n];
for (node* cur = first; cur; cur = cur->next)
if (equals(get_key(cur->val), get_key(obj)))
return pair<iterator, bool>(iterator(cur, this), false);
//配置一个新的值为obj的节点
node* tmp = new_node(obj);
//链表的头插法
tmp->next = first;
buckets[n] = tmp;
//节点数加1
++num_elements;
//返回指向该节点的迭代器
return pair<iterator, bool>(iterator(tmp, this), true);
}
/* 该函数供insert_equal调用,允许拥有重复的key的元素插入
* 前面和后面的操作都和上面的一样
* 只是当检测到插入的元素的key和已有的重复时
* 做的操作是将插入的元素放到已有的元素后
* 然后返回
*/
template <class V, class K, class HF, class Ex, class Eq, class A>
typename hashtable<V, K, HF, Ex, Eq, A>::iterator
hashtable<V, K, HF, Ex, Eq, A>::insert_equal_noresize(const value_type& obj)
{
const size_type n = bkt_num(obj);
node* first = buckets[n];
for (node* cur = first; cur; cur = cur->next)
if (equals(get_key(cur->val), get_key(obj))) {
node* tmp = new_node(obj);
tmp->next = cur->next;
cur->next = tmp;
++num_elements;
return iterator(tmp, this);
}
node* tmp = new_node(obj);
tmp->next = first;
buckets[n] = tmp;
++num_elements;
return iterator(tmp, this);
}
resize
该函数用于判断是否需要重新建表,注意不是调用了就重建表。
它的判断标准是:把元素个数和bucket个数来比较,如果前者大于后者,就重建表。
这就意味着,每个bucket的链表最大的节点数和bucket的个数相同。
template <class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>::resize(size_type num_elements_hint)
{
const size_type old_n = buckets.size();
//如果节点个数大于bucket个数,则重建
if (num_elements_hint > old_n) {
//求出下一个合适的n
const size_type n = next_size(num_elements_hint);
/* 若n大于旧的bucket个数时才进行重建 */
if (n > old_n) {
//新的hashtable
vector<node*, A> tmp(n, (node*) 0);
__STL_TRY {
/* 下面执行节点迁移操作
* 首先遍历bucket
* 然后遍历bucket中的链表
* 进行节点迁移
*/
for (size_type bucket = 0; bucket < old_n; ++bucket) {
node* first = buckets[bucket];
while (first) {
//将旧表上的节点需要插入到新表上的位置算出来
size_type new_bucket = bkt_num(first->val, n);
//然后将该节点移出旧表
buckets[bucket] = first->next;
//与新表进行连接
first->next = tmp[new_bucket];
tmp[new_bucket] = first;
first = buckets[bucket];
}
}
//最后将旧表与新表进行交换
//旧表的空间会随着tmp离开其作用域而释放
buckets.swap(tmp);
}
# ifdef __STL_USE_EXCEPTIONS
//下面是产生异常的情况
catch(...) {
for (size_type bucket = 0; bucket < tmp.size(); ++bucket) {
while (tmp[bucket]) {
node* next = tmp[bucket]->next;
delete_node(tmp[bucket]);
tmp[bucket] = next;
}
}
throw;
}
# endif /* __STL_USE_EXCEPTIONS */
}
}
}
clear
析构函数中调用的函数,用于释放所有节点。(vector
的空间并未被释放)
template <class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>::clear()
{
//遍历每个bucket
for (size_type i = 0; i < buckets.size(); ++i) {
//遍历链表
node* cur = buckets[i];
while (cur != 0) {
//释放节点
node* next = cur->next;
delete_node(cur);
cur = next;
}
//最后将该bucket置为null
buckets[i] = 0;
}
//将num_elements置成0
num_elements = 0;
}
复制操作
拷贝构造函数里面唯一调用的函数
template <class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>::copy_from(const hashtable& ht)
{
//首先将原空间的节点全部清除
buckets.clear();
//然后根据ht的大小来适当扩展hashtable的空间(reverse不会缩小空间)
buckets.reserve(ht.buckets.size());
//调用insert对hashtable进行初始化
buckets.insert(buckets.end(), ht.buckets.size(), (node*) 0);
__STL_TRY {
//接下来,我们只需要遍历bucket以及bucbucket上的链表,将每个节点拷贝过去就行了
for (size_type i = 0; i < ht.buckets.size(); ++i) {
//该bucket不为空,进行拷贝
if (const node* cur = ht.buckets[i]) {
//创建新的节点
node* copy = new_node(cur->val);
//令新的节点为bucket的头节点
buckets[i] = copy;
//依次创建新节点,并连接到该bucket上
for (node* next = cur->next; next; cur = next, next = cur->next) {
copy->next = new_node(next->val);
copy = copy->next;
}
}
}
//维护num_elements
num_elements = ht.num_elements;
}
__STL_UNWIND(clear());
}
查找元素,若没有则插入
template <class V, class K, class HF, class Ex, class Eq, class A>
typename hashtable<V, K, HF, Ex, Eq, A>::reference
hashtable<V, K, HF, Ex, Eq, A>::find_or_insert(const value_type& obj)
{
//首先判断是否需要重建表,因为可能涉及到插入操作
resize(num_elements + 1);
size_type n = bkt_num(obj);
node* first = buckets[n];
//遍历该key所在的bucket,如果找到了,则直接返回
for (node* cur = first; cur; cur = cur->next)
if (equals(get_key(cur->val), get_key(obj)))
return cur->val;
//否则,插入该obj
node* tmp = new_node(obj);
tmp->next = first;
buckets[n] = tmp;
++num_elements;
return tmp->val;
}
删除操作
关于erase
操作,也有很多重载的版本,无非就是针对某个值进行删除以及针对某个迭代器指向的位置进行删除、还有根据迭代器的范围进行删除或者删除n个元素之类的。
这里我们只分析前两种。
//根据key值删除,返回删除的个数(因为可能允许重复的元素插入,所以一个key可能对应多个元素)
template <class V, class K, class HF, class Ex, class Eq, class A>
typename hashtable<V, K, HF, Ex, Eq, A>::size_type
hashtable<V, K, HF, Ex, Eq, A>::erase(const key_type& key)
{
//获取该key对应hashtable存储的位置
const size_type n = bkt_num_key(key);
//指向该bucket的第一个节点
node* first = buckets[n];
//erased记录删除的节点个数
size_type erased = 0;
//若要删除的key对应的元素还没有插入,直接返回0,代表删除的个数为0
if (first) {
//遍历链表
node* cur = first;
node* next = cur->next;
while (next) {
//找到key值相同的节点
if (equals(get_key(next->val), key)) {
//将该节点移出,并释放
cur->next = next->next;
delete_node(next);
next = cur->next;
//删除的节点个数加1
++erased;
//总的节点数减1
--num_elements;
}
else {
cur = next;
next = cur->next;
}
}
//比较该key与第一个节点的key
if (equals(get_key(first->val), key)) {
buckets[n] = first->next;
delete_node(first);
++erased;
--num_elements;
}
}
//返回成功删除的节点个数
return erased;
}
//删除迭代器指向的节点
template <class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>::erase(const iterator& it)
{
if (node* const p = it.cur) {
const size_type n = bkt_num(p->val);
node* cur = buckets[n];
/* 如果删除的就是目前bucket的第一个节点
* 需要特殊处理一下,将cur->next设为新的头节点
*/
if (cur == p) {
buckets[n] = cur->next;
delete_node(cur);
--num_elements;
}
else {
/* 否则遍历该链表
* 找到要删除的节点进行删除
*/
node* next = cur->next;
while (next) {
if (next == p) {
cur->next = next->next;
delete_node(next);
--num_elements;
break;
}
else {
cur = next;
next = cur->next;
}
}
}
}
}
小结
关于hashtable
部分,本小节已经将剩下的大部分操作分析完了,关于stl_hash_fun.h
中哈希函数对象部分的内容,有兴趣的可以自己看看,其实就是通过重载()
操作符完成的,里面集成了根据key
的类型不同的各种不同的哈希函数,比如key
是字符串,就需要进行一定的转换了。并且SGISTL
中的hashtable
无法处理string
这类的key,需要用户来定义它们的哈希函数。
在下一小节中,我们将对hash_set
进行分析。