Chapter 5: 关联式容器之 hashtable

SGI 中的 hashtable 使用的是开链法(separate chaining)。这种做法是在每一个表格元素中维护一个 list;hash function 为我们分配某一个 list,然后我们在那个 list 身上执行元素的插入,搜寻和删除等操作。虽然针对 list 而进行的搜寻只能是一种线性操作,但如果 list 够短,速度还是够快的。

一:hashtable 的桶子(buckets)与节点(nodes)

1:首先我们将 hash table 表格内的元素称为桶子(bucket),此名称的大约意思是:表格内的每个单元,涵盖的不只是个节点(元素),甚至可能是一“桶”节点;

2:hash table 的节点定义代码如下:

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

注意,bucket 所维护的 linked list,并不采用 STL 的 list 或 slist,而是自行维护上述的 hash table node。至于 buckets 聚合体,则以 vector 完成,以便有动态扩张能力;

二:hashtable 的迭代器

hashtable 的迭代器为前向迭代器,没有后退操作(operator--()),其代码如下:

//hashtable 的前向声明
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc = alloc>
class hashtable;

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator
{
        typedef hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc> hashtable;
        typedef __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc> iterator;
        typedef __hashtable_const_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc> const_iterator;
        typedef __hashtable_node<Value> node;

        typedef forward_iterator_tag    iterator_category;
        typedef Value   value_type;
        typedef ptrdiff_t       difference_type;
        typedef size_t  size_type;
        typedef Value&  reference;
        typedef Value*  pointer;

        node* cur;      //迭代器目前所指之节点
        hashtable* ht;  //保持对容器的连结关系(因为可能需要从 bucket 跳到 bucket)

        __hashtable_iterator(node* n, hashtable* tab) : cur(n), ht(tab) {}
        __hashtable_iterator() {}
        reference operator*() const { return cur->val; }
        pointer operator->() const { return &(operator*()); }
        iterator& operator++();
        iterator operator++(int);

        bool operator==(const iterator& it) const { return cur == it.cur; }
        bool operator!=(const iterator& it) const { return cur != it.cur; }
};

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
auto __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::operator++()
 -> __hashtable_iterator&
{
        const node* old = cur;
        cur = cur->next;        //如果存在,就是它,否则进入 if 流程
        if (!cur) {
                // 根据元素值,定位出下一个 bucket,其起头处就是我们的目的地
                size_type bucket = ht->bkt_num(old->val);
                while (!cur && ++bucket < ht->buckets.size())
                        cur = ht->buckets[bucket];
        }

        return *this;
}

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
inline auto __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::operator++(int) 
-> __hashtable_iterator
{
        iterator tmp = *this;
        ++*this;        //调用 operator++

        return tmp;
}  

在 hashtable 的前向声明中,有六个模板参数,模板参数 Value 表示的是节点的实值型别,Key 表示的是节点的键值型别,HashFcn 表示的是 hash function 的函数型别,ExtractKey 表示的是从节点中取出键值的方法(函数或仿函数),EqualKey 表示的是判断键值相同与否的方法(函数或仿函数),Alloc 为空间配置器,默认使用 std::alloc。

三:hashtable 的数据结构

1:hashtable 的构造与内存管理

在 hashtable 中,专属的节点配置器被定义如下:

private:
        typedef __hashtable_node<Value> node;
        typedef simple_alloc<node, Alloc> node_allocator;

节点配置与释放函数代码如下:

private:
        node* new_node(const value_type& obj) {
                node* n = node_allocator::allocate();
                n->next = nullptr;
                __STL_TRY {
                        construct(&n->val, obj);
                        return n;
                }
                __STL_UNWIND(node_allocator::deallocate(n));
        }

        void delete_node(node* n) {
                destroy(&n->val);
                node_allocator::deallocate(n);
        }

hashtable 中的一个构造函数如下:

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

private:
        void initialize_buckets(size_type n) {
                const size_type n_buckets = next_size(n); //返回最接近 n 并大于等于 n 的质数
                bucket.reserve(n_buckets);
                bucket.insert(bucket.end(), n_buckets, static_cast<node*>nullptr)
                num_elements = 0;
        }

2:hashtable 的插入操作与表格重整

1):insert_unique()表示不允许插入重复的元素,代码如下:

public:
        pair<iterator, bool> insert_unique(const value_type& obj) {
                resize(num_elements + 1); //判断是否需要重建表格,如需要就扩充
                return insert_unique_noresize(obj);
        }

其中resize()函数用来判断是否需要重建表格,insert_unique_noresize()表示在不需要重建表格的情况下,插入新节点,键值不能重复,这两个函数的代码如下:

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
void hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::resize(size_type num_elements_hint)
{
        //如果元素个数大于 bucket vector 大小,则重建表格

        const size_type old_n = buckets.size();
        if (num_elements_hint > old_n) {        //确定真的需要重新配置
                const size_type n = next_size(num_elements_hint) //找出下一个质数
                if (n > old_n) {
                        vector<node*, Alloc> tmp(n, static_cast<node*>(nullptr)); //设立新的 buckets
                        __STL_TRY {
                                //以下处理每一个旧的 bucket
                                for (size_type bucket = 0; bucket < old_n; ++bucket) {
                                        node* first = buckets[bucket]; //指向节点所对应之串行的起始节点
                                        //以下处理每一个旧 bucket 所含(串行)的每一个节点
                                        while (first) { //串行还没有结束
                                                //以下找出节点落在哪一个新 bucket 内
                                                size_type new_bucket = bkt_num(first->val, n);
                                                //令旧 bucket 指向其所对应之串行的下一个节点
                                                buckets[bucket] = first->next;
                                                //将当前节点插入到新 bucket 内,成为其对应串行的第一个节点
                                                first->next = tmp[buckets];
                                                tmp[buckets] = first;
                                                //回到旧 bucket 所指的待处理行,准备处理下一个节点
                                                first = buckets[bucket];
                                        }
                                }
                                buckets.swap(tmp); // vector::swap. 新旧两个 buckets 对调
                                //离开时释放 local tmp 的内存
                        }

                }
        }
}

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
auto hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::insert_unique_noresize(const value_type& obj)
 -> pair<iterator, bool>
{
        const size_type n = bkt_num(obj); //决定 obj 应位于 #n bucket
        node* first = buckets[n];       //令 first 指向 bucket 对应之串行头部

        //如果 buckets[n] 已被占用,此时 first 将不为 0,于是进入以下循环,
        //走过 bucket 所对应的整个链表
        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);


        //离开以上循环(或根本未进入循环)时,first 指向 bucket 所指链表的头部结点
        node* tmp = new_node(obj);      //产生新节点
        tmp->next = first;
        buckets[n] = tmp;               //令新节点成为链表的第一节点
        ++num_elements;                 //节点个数累加1
        return pair<iterator, bool>(iterator(tmp, this), true);
}        

2):insert_equal()表示可以插入相同的元素,函数代码如下:

public:
        pair<iterator, bool> insert_equal(const value_type& obj) {
                resize(num_elements + 1); //判断是否需要重建表格,如需要就扩充
                return insert_equal_noresize(obj);
        }

insert_equal_noresize()函数代码如下:

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
auto hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::insert_equal_noresize(const value_type& obj)
 -> pair<iterator, bool>
{
        const size_type n = bkt_num(obj);       //决定 obj 应位于 #n bucket
        node* first = buckets[n];       //令 first 指向 bucket 对应之链表头部

        //如果 buckets[n] 已被占用,此时 first 将不为0,于是进入以下循环
        //走过 bucket 所对应的整个链表
        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;         //节点个数累加1
                        return iterator(tmp, this);     //返回一个迭代器,指向新增节点
                }
        }

        //进行至此,没有发现重复的键值
        node* tmp = new_node(obj);      //产生新节点
        tmp->next = first;
        buckets[n] = tmp;               //令新节点成为链表的第一节点
        ++num_elements;                 //节点个数累加1
        return pair<iterator, bool>(iterator(tmp, this), true); //返回一个迭代器,指向新增节点
}

3:判知元素的落脚处

当我们插入一个元素时,我们需要判断它应该要被插入哪一个 bucket 之间,这可求助于bkt_num()函数,代码如下:

public:
        //下面四个版本用来判断元素落在哪一个 bucket 之内
        //版本1:接受实值 (value) 和 buckets 个数
        size_type bkt_num(const value_type& obj, size_t n) const
        { return bkt_num_key(get_key(obj), n); }
        //版本2:只接受实值
        size_type bkt_num(const value_type& obj) const
        { return bkt_num_key(get_key(obj)); }
        //版本3:只接受键值
        size_type bkt_num_key(const key_type& key) const
        { return bkt_num_key(key, buckets.size()); }
        //版本4:接受键值和 buckets 个数
        size_type bkt_num_key(const key_type& key, size_t n) const
        { return hash(key) % n; }

4:复制(copy_from())和整体删除(clear())

复制和整体删除要特别注意内存的释放问题,代码如下:

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
void hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::clear()
{
        //针对每一个 bucket
        for (size_type i =0; i != buckets.size(); ++i) {
                node* cur = buckets[i];
                //将 bucket list 中的每个节点删除掉
                while (cur) {
                        node* next = cur->next;
                        delete_node(cur);
                        cur = next;
                }

                buckets[i] = nullptr;   //令 bucket 内容为 null 指针
        }

        num_elements = 0;       //令总节点个数为0

        //注意,buckets vector 并未释放掉空间,仍保持原来大小
}

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
void hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::copy_from(const hashtable& ht)
{
        //先清除己方的 buckets vector ,这操作是调用 vector::clear。将整个容器清空是buckets.clear()
        //为己方的 buckets vector 保留空间,使与对方相同
        //如果己方空间大于对方,就不动,如果己方空间小于对方,就增大
        buckets.reserve(ht.buckets.size());
        //从己方的 buckets vector 尾端开始,插入 n 个元素,其值为 null 指针
        //注意,此时的 buckets vector 为空,所以所谓尾端,就是起头处
        buckets.insert(buckets.end(), ht.buckets.size(), static_cast<node*>(nullptr));
        __STL_TRY {
                //针对 buckets vector
                for (size_type i = 0; i != ht.buckets.size(); ++i) {
                //复制 vector 的每一个元素(是个指针,指向 hastable 节点)
                        if (const node* cur = ht.buckets[i]) {
                                node* copy = new_node(cur->val);
                                buckets[i] = copy;

                                //针对同一个 bucket list,复制每一个节点
                                for (node* next = cur->next; next; cur = next, next = cur->next) {
                                        copy->next = new_node(next->val);
                                        copy = copy->next;
                                }
                        }                }
        num_elements = ht.num_elements;
        }
        __STL_UNWIND(clear());
}

5:查找(find())和计数(count())函数

这两个函数代码如下:

public:
        iterator find(const key_type& key) {
                size_type n = bkt_num_key(key); //首先寻找落在哪一个 bucket 内
                node* first;

                //以下,从 bucket list 的头开始,一一比对每个元素的键值,比对成功就跳出
                for (first = buckets[n]; first && !equals(get_key(first->val), key); first = first->next)
                {}

                return iterator(first, this);
        }

        size_type count(const key_type& key) const {
                const size_type n = bkt_num_key(key);   //首先寻找落在哪一个 bucket 内
                size_type result = 0;

                //以下,从 bucket list 的头开始,一一比对每个元素的键值,比对成功就累加1
                for (const node* cur = bucket[n]; cur; cur = cur-> next)
                        if (equals(get_key(cur->val), key))
                                ++result;

                return result;
        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值