STL关联式容器之hashtable

news2024/11/25 8:45:18

hashtable的桶子与节点

        下图为开链法(separate chaining)完成hashtable的图形表述。为了剖析SGI STL源码,我们遵循SGI的命名,称hash table表格内的元素为桶(bucket),此名称的大约意义是,表格内的每个单元,涵盖的不只是个节点(元素),可能是一桶节点。(nil标识null节点)

下面是hash table的节点定义

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

        注意,bucket所维护的linked list,并不采用STL的list或slist,而是自行维护上述的hashtable node。至于buckets聚合体,则以vector完成,以便有动态扩展能力。稍后在hash table的定义中我们可以清楚看到这一点。

hashtable迭代器

        以下是hashtable迭代器的定义:

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {
    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;
    
    __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; }
};

        注意,hashtable迭代器必须永远维系着与整个bucket vector的关系,并记录目前所指的节点。其前进操作是首先尝试从目前所指节点出发,前进一个位置节点,由于节点被安置在list内,所以利用节点的next指针可以轻易的达成前进的操作。如果目前节点正巧是list的尾端,就跳至下一个bucket身上,那正式指向下一个list的头部节点。

template<class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++() {
    const node* old = cur;
    cur = cur->next;
    if (!cur) {
        size_type bucket = ht->bkt_num(old_val); // 对old_value值重新进行了一次hash映射
        while(!cur && ++bucket < ht->buckets.size()) {
            cur = ht->buckets[bucket];
        }
    }
    return *this;
}


template<class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++(int) {

    iterator tmp = *this;
    ++*this;
    return tmp;
}

        注意,hash table的迭代器没有后退操作(operator--()),hashtabl也没有定义逆向迭代器(reverse iterator)。

hashtable的数据结构

        下面是hashtable的定义摘要,其中可见buckets聚合体以vector完成,以利于动态扩充:

template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc = alloc>
class hashtable {
public:
    typedef HashFcn hasher;
    typedef EqualKey key_equal;
    typedef size_t size_type;

private:
    hasher hash;
    key_equal equals;
    ExtractKey get_key;

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

    vector<node*, Alloc> buckets;
    size_type num_elements;

public:
    size_type bucket_count() const { return buckets.size(); }
...
};

        hashtable的模板参数相当多,包括:

  • Value:节点的实值型别
  • Key:节点的键值型别
  • HashFcn:hash function的函数型别
  • ExtractKey: 从节点中取出键值的方法(函数或仿函数)
  • EqualKey:判断键值相同与否的方法(函数或仿函数)
  • Alloc:空间配置器,缺省为std::alloc

        后续会给出使用这些型别的样例。注意,先前谈及概念时,我们说hash function是计算元素位置的函数,SGI将这项任务赋予bkt_num(),由它调用hash function取得一个可执行modulus(取模)运算的值。稍后执行这项"计算元素位置"的任务时,我们会再次看到。

        虽然开链法(seperate chaining)并不要求表格大小必须为质数,但SGI STL仍然以质数来设计表格大小,并且现将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数中,“最接近某数并大于某数”的质数:

static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] = {
    53,    97,    193,    389,    769,
  1543,    3079,    6151,    12289,    24593,
 49157,    98317,    196613,    393241,    784633,
1572869,    3145739,    6291469,    12582917,    25165843,
50331653,    100663319,    201326611,    402653189,    805306457,
1610612741,    3221225473ul,    4294967291ul
};

inline unsigned long __stl_next_prime(unsigned long n) {
    const unsigned long* first = __stl_prime_list;
    const unsigned long* last = __stl_prime_list + __stl_num_primes;
    const unsigned long* pos = lower_bound(first, last, n);
    // 以上lower_bound是泛型算法,后续章节会介绍
    // lower_bound,序列需先排序。
    return pos == last ? *(last -1) : *pos;
}


size_type max_bucket_count() const {
    return __stl_prime_list[__stl_num_primes - 1];
}

hashtable的构造与内存管理

        上一节hashtable定义中展现了一个专属的节点配置器:

typedef simple_alloc<node, Alloc>node_allocator;

        下面是节点配置函数与节点释放函数:

node *new_node(const value_type& obj) {
    node* n = node_allocator::allocate();
    n->next = 0;
    __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);
} 

当我们初始构造一个拥有50个节点的hash table如下:

hashtable<int, int, hash<int>, identity<int>, equal_to<int>, alloc>
iht(50, hash<int>(), euqal_to<int>());

cout << iht.size() << endl;                // 0
cout << iht.bucket_count() << endl;        // 53. STL 提供的第一个质数

上述定义调用一下构造函数

hashtable(size_type n, const HashFcn& hlf, const EqualKey& eql)
: hash(half), equals(eql), get_key(Extrack_key()), num_elements(0) {
    initial_buckets(n); }

void initial_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;
}

其中next_size()返回最近并大于n的质数;

size_type next_size(size_type n) const { return __stl_next_prime(n); }

然后buckets vector保留空间,设定所有buckets的初值为0(NULL指针)。

插入操作(insert)与表格重整

        当客户端开始插入元素(节点)时:

        iht.insert_unique(59);

        iht.insert_unique(63);

        iht.insert_unique(108);

        hash table内将进行一下操作:

pair<iterator, bool> insert_unique(const value_type& obj) {
    resize(num_elements + 1);
    return insert_unique_noresize(obj);
}

以下函数判断是否需要重建表格。如果不需要,立即回返,如果需要,就开始resize

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();
    if (num_elements_hint > old_n ) {
        const size_type n = next_size(num_elements_hint);
        if (n > old_n) {
            vector<node*, A> tmp(n, (node*)0);
            for (size_type bucket = 0; bucket < old_n; ++bucket) {
                node *first = buckets[bucket];
                while (first) {
                    size_type new_bucket = bkt_num(firs->val, n);

                    // 以下四个操作颇为微妙
                    // (1)令旧bucket指向其所对应之串行节点的下一个节点(以便于迭代处理)
                    buckets[bucket] = first->next;
                    // (2) (3) 将当前节点插入到新的bucket内,成为其对应串行的第一个节点
                    first->next = tmp[new_bucket];
                    tmp[new_bucket] = first;
                    // (4) 回到旧的所指的待处理串行,准备处理下一个节点
                    first = buckets[bucket]
                } // while (first)
            } // for
            buckets.swap(tmp);
        } // if (n > old_n)
    }
}

//在不需要重建表格的情况下插入新节点。键值不允许重复

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 = bit_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);
        }
    }

    node *tmp = new_node(obj);
    tmp->next = first;
    buckets[n] = tmp;
    ++num_elements;
    return pair<iterator, bool>(iterator(tmp, this), true);
}

hashtable同时允许键值重复的的函数,其函数定义如下

iterator insert_equal(const value_type& obj) {
    resize(num_elements + 1);
    return insert_euqal_noresize(obj);
}

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];
    
    // 此操作为了后续遍历时,相同key的元素总是连续出现,如果不做这个要求,该循环可以不处理,直接将元素插入list的头部,效率会更高
    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);
    
}

判知元素的落脚处(bkt_num)

        本节程序代码在许多地方都需要知道某个元素值落脚于哪一个bucket之内。这本来是hash function的责任,SGI把这个任务包装了一层,先交给bkt_num()函数,再由此函数调用hash function,取得一个可执行modulus(取模)运算的数值。为什么要这么做?因为有些型别无法直接拿来对hashtable的大小进行模运算,例如字符字符串const  char*,这时候我们需要做一些转换。下面是bkt_num函数,

// 版本1:接受实值(value),和buckets个数
size_type bkt_num(const value_type& obj, size_t n) {
    return bkt_num_key(get_key(obj), n); //调用版本4
}

// 版本2:只接受实值(value)
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
}

// 版本4:接受键值和buckets个数
size_type bkt_num_key(const key_type&key, size_t n) const {
    return hash(key) % n;                         // SGI所有内建hash函数后续会进行说明
}

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

        由于整个hash table由vector和linked-list组合而成,因此,复制和整体删除,都需要特别注意内存的释放问题。下面是hashtable提供的两个相关函数:

template <class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>::clear() {
    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;
        } // while
        buckets[i]=0;
    } // for
    num_elements=0;
    // 注意,buckets vector并未释放掉空间,仍保留原来大小
}
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();

    buckets.reserve(ht.buckets.size());

    buckets.insert(buckets.end(), ht.buckets.size(), (node*)0);
    
    __STL_TRY {
        for (size_type i=0; i<bt.buckets.size(); ++i) {
            if (const node* cur = ht.buckets[i]) {
                node *copy = new_node(cur->val);
                buckets[i] = copy;
                for (node* next = cur->next; next; cur=next, next = cur->next) {
                    copy->next = new_node(next->val);
                    copy = copy->next;                

                } // for

            } // if
        } // for
        num_elements = ht.num_elements;
    }
    __STL_UNWIND(clear());
}

hash functions

        <stl_hash_fun.h>定义有数个现成的hash functions,全部是仿函数。先前谈及概念时,我们说hash function是计算元素位置的函数,SGI将这项任务赋予了先前提过的bkt_num,再由它来调用这里提供的hash function,取得一个可以对hashtable进行模运算的值。针对char,int,long等整型型别,这里大部分的hash function什么也没做,只是忠实的返回原值。但对于字符串(const char*),就设计了一个转换函数如下:

template <class Key> struct hash {};

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

__STL_TEMPLATE_NULL struct hash<char*> {
    size_t operator()(const char*s) const { return __stl_hash_string(s); }
};

__STL_TEMPLATE_NULL struct hash<const char*> {
    size_t operator()(const char*s) const { return __stl_hash_string(s); }
};

__STL_TEMPLATE_NULL struct hash<char> {
    size_t operator()(char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned char> {
    size_t operator()(unsigned char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<signed char> {
    size_t operator()(unsigned char x) const { return x; }
};

__STL_TEMPLATE_NULL struct hash<short> {
    size_t operator()(short x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned short> {
    size_t operator()(unsigned short x) const { return x; }
};


__STL_TEMPLATE_NULL struct hash<int> {
    size_t operator()(int x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned int> {
    size_t operator()(unsigned int x) const { return x; }
};

__STL_TEMPLATE_NULL struct hash<long> {
    size_t operator()(long x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned long> {
    size_t operator()(unsigned long x) const { return x; }
};

由此可见,SGI hashtable无法处理上述所列各型别以外的元素,例如string,double, float,预处理这些型别,用户必须自定义他们的hash function。下面直接以SGI hashtable处理string所获得的错误现象:

#include <hash_set>
#include <iostream>
#include <string>
using namespace std;
using namespace __gnu_cxx;

int main() {
    hash_set<string> shs;
    hash_set<double> dhs;
    
    shs.insert(string("chnming"));
    dhs.insert(15.0);

    return 0;
}

以上代码编译会报错(以上代码在gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0进行编译,可能实现上会有些差异)

从编译报错信息来看,大体和前文提到的未对hasher<string> ,hasher<double>进行特例化有关系。

参考文档《STL源码剖析》--侯捷

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2247178.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于python的长津湖评论数据分析与可视化,使用是svm情感分析建模

引言 研究背景及意义 上世纪初开始&#xff0c;中国电影就以自己独有的姿态登上了世界电影史的舞台。中国电影作为国家文化和思想观念的反映与延伸&#xff0c;能够增强文化自信&#xff0c;在文化输出方面有着极其重要的作用1[1]。 改革开放以来&#xff0c;随着生产力的提高…

阿里云oss转发上线-实现不出网钓鱼

本地实现阿里云oss转发上线&#xff0c;全部代码在文末&#xff0c;代码存在冗余 实战环境 被钓鱼机器不出网只可访问内部网络包含集团oss 实战思路 若将我们的shellcode文件上传到集团oss上仍无法上线&#xff0c;那么就利用oss做中转使用本地转发进行上线&#xff0c;先发送…

预测未来 | MATLAB实现Transformer时间序列预测未来

预测未来 | MATLAB实现Transformer时间序列预测未来 预测效果 基本介绍 1.Matlab实现Transformer时间序列预测未来&#xff1b; 2.运行环境Matlab2023b及以上&#xff0c;data为数据集&#xff0c;单变量时间序列预测&#xff1b; 3.递归预测未来数据&#xff0c;可以控制预…

局域网与广域网:探索网络的规模与奥秘(3/10)

一、局域网的特点 局域网覆盖有限的地理范围&#xff0c;通常在几公里以内&#xff0c;具有实现资源共享、服务共享、维护简单、组网开销低等特点&#xff0c;主要传输介质为双绞线&#xff0c;并使用少量的光纤。 局域网一般是方圆几千米以内的区域网络&#xff0c;其特点丰富…

可视化建模与UML《协作图实验报告》

有些鸟儿毕竟是关不住的。 一、实验目的&#xff1a; 1、熟悉协作图的构件事物。 2、掌握协作图的绘制方法。 二、实验环境&#xff1a; window7 | 10 | 11 EA15 三、实验内容&#xff1a; 下面列出了打印文件时的工作流&#xff1a; 用户通过计算机指定要打印的文件。(2)打…

docker搭建私有的仓库

docker搭建私有仓库 一、为什么要搭建私有的仓库&#xff1f; 因为在国内&#xff0c;访问&#xff1a;https://hub.docker.com/ 会出现无法访问页面。。。。&#xff08;已经使用了魔法&#xff09; 当然现在也有一些国内的镜像管理网站&#xff0c;比如网易云镜像服务、Dao…

微信小程序条件渲染与列表渲染的全面教程

微信小程序条件渲染与列表渲染的全面教程 引言 在微信小程序的开发中,条件渲染和列表渲染是构建动态用户界面的重要技术。通过条件渲染,我们可以根据不同的状态展示不同的内容,而列表渲染则使得我们能够高效地展示一组数据。本文将详细讲解这两种渲染方式的用法,结合实例…

订单日记为“惠采科技”提供全方位的进销存管理支持

感谢温州惠采科技有限责任公司选择使用订单日记&#xff01; 温州惠采科技有限责任公司&#xff0c;成立于2024年&#xff0c;位于浙江省温州市&#xff0c;是一家以从事销售电气辅材为主的企业。 在业务不断壮大的过程中&#xff0c;想使用一种既能提升运营效率又能节省成本…

mysql-分析并解决可重复读隔离级别发生的删除幻读问题

在 MySQL 的 InnoDB 存储引擎中&#xff0c;快照读和当前读的行为会影响事务的一致性。让我们详细分析一下隔离级别味可重复读的情况下如何解决删除带来的幻读。 场景描述 假设有一个表 orders&#xff0c;其中包含以下数据&#xff1a; 事务 A 执行快照读 START TRANSACTION…

使用itextpdf进行pdf模版填充中文文本时部分字不显示问题

在网上找了很多种办法 都解决不了; 最后发现是文本域字体设置出了问题; 在这不展示其他的代码 只展示重要代码; 1 引入扩展包 <dependency><groupId>com.itextpdf</groupId><artifactId>itext-asian</artifactId><version>5.2.0</v…

链表刷题|判断回文结构

题目来自于牛客网&#xff0c;本文章仅记录学习过程的做题理解&#xff0c;便于梳理思路和复习 我做题喜欢先把时间复杂度和空间复杂度放一边&#xff0c;先得有大概的解决方案&#xff0c;最后如果时间或者空间超了再去优化即可。 思路一&#xff1a;要判断是否为回文结构则…

0基础跟德姆(dom)一起学AI NLP自然语言处理01-自然语言处理入门

1 什么是自然语言处理 自然语言处理&#xff08;Natural Language Processing, 简称NLP&#xff09;是计算机科学与语言学中关注于计算机与人类语言间转换的领域. 2 自然语言处理的发展简史 3 自然语言处理的应用场景 语音助手机器翻译搜索引擎智能问答...

Linux系统使用valgrind分析C++程序内存资源使用情况

内存占用是我们开发的时候需要重点关注的一个问题&#xff0c;我们可以人工根据代码推理出一个消耗内存较大的函数&#xff0c;也可以推理出大概会消耗多少内存&#xff0c;但是这种方法不仅麻烦&#xff0c;而且得到的只是推理的数据&#xff0c;而不是实际的数据。 我们可以…

跨平台开发_RTC程序设计:实时音视频权威指南 2

1.2.1 一切皆bit 将8 bit分为一组&#xff0c;我们定义了字节(Byte)。 1956年6月&#xff0c;使用了Byte这个术语&#xff0c;用来表示数字信息的基本单元。 最早的字节并非8 bit。《计算机程序设计的艺术》一书中的MIX机器采用6bit作为1Byte。8 bit的Byte约定&#xff0c;和I…

WIFI:长GI与短GI有什么区别和影响

1、GI的作用 Short GI(Guard Interval)是802.11n针对802.11a/g所做的改进。射频芯片在使用OFDM调制方式发送数据时&#xff0c;整个帧是被划分成不同的数据块进行发送的&#xff0c;为了数据传输的可靠性&#xff0c;数据块之间会有GI&#xff0c;用以保证接收侧能够正确的解析…

ssm实战项目──哈米音乐(二)

目录 1、流派搜索与分页 2、流派的添加 3、流派的修改 4、流派的删除 接上篇&#xff1a;ssm实战项目──哈米音乐&#xff08;一&#xff09;&#xff0c;我们完成了项目的整体搭建&#xff0c;接下来进行后台模块的开发。 首先是流派模块&#xff1a; 在该模块中采用分…

C++使用minio-cpp(minio官方C++ SDK)与minio服务器交互简介

目录 minio简介minio-cpp简介minio-cpp使用 minio简介 minio是一个开源的高性能对象存储解决方案&#xff0c;完全兼容Amazon S3 API&#xff0c;支持分布式存储&#xff0c;适用于大规模数据架构&#xff0c;容易集成&#xff0c;而且可以方便的部署在集群中。 如果你已经部…

细说敏捷:敏捷四会之standup meeting

上一篇文章中&#xff0c;我们讨论了 敏捷四会 中 冲刺计划会 的实施要点&#xff0c;本篇我们继续分享敏捷四会中实施最频繁&#xff0c;团队最容易实施但往往也最容易走形的第二个会议&#xff1a;每日站会 关于每日站会的误区 站会是一个比较有标志性的仪式活动&#xff0…

二分法(折半法)查找【有动图】

二分法&#xff0c;也叫做折半法&#xff0c;就是一种通过有序表的中间元素与目标元素进行对比&#xff0c;根据大小关系排除一半元素&#xff0c;然后继续在剩余的一半中进行查找&#xff0c;重复这个过程直至找到目标值或者确定目标值不存在。 我们从结论往回推&#xff0c;…

FreeRTOS——低功耗管理

目录 一、概念及其应用 1.1应用 1.2STM32电源管理系统 2.3STM32低功耗模式 2.3.1睡眠模式 2.3.2停止模式 2.3.3待机模式 三、Tickless低功耗模式 3.1低功耗模式配置 3.2低功耗模式应用 3.3低功耗电路分析 3.4低功耗处理相关接口 四、实现原理 4.1任务等待删除的检查…