C++ STL和泛型编程(二)---- hashtable[unordered_set&unordered_map]

一、结构

hashtable称哈希表,又称散列表【因为其中当元素总数超过bucket时打散之后再排序的】。
在这里插入图片描述
当空间足够大时,考虑将一种东西(object)在 0 ∼ 2 32 0\sim2^{32} 0232范围内编号,然后按对应编号放进去这个大空间中,则占用内存大小为 s i z e o f ( T ∗ ) ∗ 2 32 sizeof(T^*)*2^{32} sizeof(T)232。但实际上用不到这么大的空间。
所以考虑下面那种空间较小的情况,即此时的编号范围是缩小一部分的。此时在缩小的编号范围内,就会出现所取编号相同的情况,这时科学家又考虑设置一些一次二次方程式将相同的编号经过一定计算挪到其他位置放置,或者将空间拉开插入但此时这些一次二次方程式的计算又会消耗大量的时间】,所以这种做法很快被舍弃了。

所以问题的焦点在于,但出现存放的元素编号相同碰撞的情况时,该如何操作!

所以科学家经过考虑,采取:如果编号出现相同碰撞在一起则通过链表的方式将它们串起来就好

在这里插入图片描述上图中,要将59,63,108,2,53,55放入空间大小为53的内存中,如:
59%53=6,则放入对应编号为#6的空间中;
63%53=10,则放入对应编号为#10的空间中;
108%53=2,2%53=2,55%53=2,此时这些元素编号都相同发生碰撞,则以链表的形式将其串起来放入编号为#2的空间中。

当如果继续安插48个元素,即元素个数48+6=54总量超过bucket的个数时,此时空间就会扩展到97【在G2.9中,bucket的大小是在gnuc下是已经规定写好的,即为53,97,193…,当需要扩充时则寻找离扩充目标最近的数来设定其空间大小】,然后对应的编号重新编取排列。这样就会避免出现某条链过长即元素偏多的情况而增加元素搜寻时间。

二、代码实现(G2.9)

template<class Value, class Key, class HashFcn, 
		 class ExtractKey, class EqualKey, class Alloc = alloc> // 由此知其需指定六个参数
class hashtable{
public:
	typedef HashFcn hasher;
	typedef EqualKey key_euqal;
	typedef size_t size_type;
private:
	hasher hash; // 函数对象,大小为1
	key_euqal equals; // 函数对象,大小为1
	ExtractKey get_key; // 函数对象,大小为1

	typedef __hashtable_node<Value> node; 

	vector<node*, Alloc> buckets; // vector容器在G2.9下大小为12
	size_type num_elements; // unsigned long 变量,大小为4,统计有多少个元素
	// 所以hashtable在G2.9下大小为12+1+1+1+4=19,调整为4的倍数则大小为20。
public:
	size_type buket_count() const{ // 统计bucket的个数
		return buckets.size();
	}
};

class HashFcn 是一个函数类,其作用是如何将传入的东西转化成相应的hash-code,然后hasg-code再除以相应的bucket大小从而放入对应的编号位置;
class ExtractKey 也是一个函数类,告诉编译器如何从value中取出key;
class EqualKey 也是一个函数类,告诉编译器如何比较key的大小。

发生碰撞时需要形成链表挂在同一个bucket上面【即为生成 __hashtable_node<Value> 类型的node,然后串成链表挂在对应编号的bucket下面】,其实现操作如下:
在这里插入图片描述

template<class Value>
struct __hashtable_node{
	__hashtable_node* next;
	Value val;
};
// 则其大小为 8

而关于其迭代器,应提供两个指针,一个指向对应元素的节点,一个指向对应bucket【当在某条链表中迭代器的操作超出其范围时,则需回退到bucket进入下一个链表中进行操作】:
在这里插入图片描述

template<class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator{
...
	node* cur; // 指向元素的指针
	hashtable* ht; // 指向bucket的指针
}

三、使用实例(G2.9)

struct eqstr{
	bool operator()(const char* s1, const char* s2) const{
		return strcmp(s1, s2) == 0;
	}
};

hashtable<const char*, const char*, hash<const char*>, identity<string>, eqstr, alloc> 
								ht(50, hash<const char*>(), eqstr());
ht.insert_unique("kiwi");
ht.insert_unique("plum");
ht.insert_unique("apple");

由 ht(50, hash<const char*>(), eqstr()) 知,其传入参数一般有三个,第一个50表示是bucket的个数,第二个hash<const char*>()【带( )表示创建的是临时的类对象】表示对元素取编号的方式,第三个eqstr()【带( )表示创建的是临时的类对象】表示比较判断key的方式

而对于eqstr(),内部并无现成的函数定义,所以要自己写。所以定义了eqstr类,其中通过调用strcmp()函数来实现。因为strcmp()函数传回来的有-1,0,1,所以不能直接将strcmp()的地址即strcmp()当成第五模板参数传进class EqualKey,因而是通过类eqstr将其转为只返回0,1然后才能传入作第五模板参数

- hash-function, hash-code

对于hash<const char*>,其是用到hashtable时必须写的,因为其是告知hashtable究竟如何求得元素的hash-code,再用元素的相应hash-code与bucket取模最终得到其在hashtable中的编号,的。

在这里插入图片描述
上面这些HashFcn的版本都是数值数据【char型为一个字符,其ASCII码也相当于一个数值】,所以其hash-code可以直接用其数值,所以这些hash-function实现的都是传进来什么就传出去什么当成hash-code。

int i = hash<int>()(32);
hash<int>() 表示创建一个临时hash<int>类对象,然后临时类对象调用( ),并传值32,最终得到hash-code编号int i = 32,然后再与bucket数去模值得到其在hashtable中的最终编号。

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

// 下面两个特化版本传进来的都是C下的字符串参数
__STL_TEMPLATE_NULL struct hash<char*>{
	size_t operator()(const char* s) const{
		return __stl_hash_string(s); // 调用了__stl_hash_string()函数
	}
};
__STL_TEMPLATE_NULL struct hash<const char*>{
	size_t operator()(const char* s) const{
		return __stl_hash_string(s); // 调用了__stl_hash_string()函数
	}
};

函数size_t __stl_hash_string(const char* s),比如char* s指向的是“abc”字符串,则:
第一次循环时,h=5 * 0 + ‘a’,即h=‘a’;
第二次循环时,h=5 * ‘a’ + ‘b’,即h=5 * ‘a’ + ‘b’;
第三次循环时,h=5 * (5 * ‘a’ + ‘b’) + ‘c’,即最终h=5 * (5 * ‘a’ + ‘b’) + ‘c’

具体操作例子
在这里插入图片描述而G2.9中,标准库并没有提供现成的C++的字符串string的hash<string>的版本【因而前面例子里用的是C下的字符串】,所以需要自己编写。

template<> struct hash<string>{
	size_t operator()(string s) const{
		return __stl_hash_string(s.c_str()); // 这里调用的也是__stl_hash_string()函数
											 // 另外这里还调用了将c++字符串转换成c的字符串的函数c_str()
	}
};

hashtable<string, string, hash<string>, identity<string>, 
			equal_to<string>, alloc> sht(50, hash<string>, equal_to<string>());
cout << sht.size() << endl; // 0
cout << sht.bucket_count() << endl; // 53,统计bucket个数,在G2.9中是以规定的素数的分配bucket,
									// 虽然传进为50,但实际取的是最接近50的那个规定的素数即53。
hashtable<pair<const string, int>, string, 
				hash<string>,select1st<pair<const string, int>>,
				 equal_to<string>, alloc> siht(100, hash<string>(), equal_to<string>());
cout << siht.size() << endl; // 0
cout << siht.bucket_count() << endl; // 193,统计bucket个数,在G2.9中是以规定的素数的分配bucket,
									// 虽然传进为100,但实际取的是最接近100的那个规定的素数即193。
siht.insert_unique(make_pair(string("jjhou"), 95)); // 这里的string("jjhou")表示的是一个临时C++的字符串对象
siht.insert_unique(make_pair(string("sabrina"), 90)); 
siht.insert_unique(make_pair(string("mjchen"), 85)); 
cout << siht.size() << endl; // 3
cout << siht.bucket_count() << endl; // 193
cout << siht.find(string("sabrina"))->second << endl; //90 find()返回的是一个指向对应元素节点的指针
cout << siht.find(string("jjhou"))->second << endl; //95
cout << siht.find(string("mjchen"))->second << endl; //85

G4.9用例:

在这里插入图片描述

注意:
hashfunction的目的,就是希望根据元素值算出一个hash-code一个可以进行modulus运算的值),使得元素经hash-code映射之后能够“够杂够乱够随机”地放置于hashtable内。因为愈是杂乱,愈不容易发生碰撞!

- modulus运算

size_type bkt_num_key(const key_type& key, size_t n) const{
	return hash(key) % n; // 这里的hash不是struct hash既不是hashfunction,
						  // 而是class hashtable中的hasher hash即为一个hashfunction的对象了。
}
size_type bkt_num_key(const key_type& key) const{
	return bkt_num_key(key, buckets.size());
}
size_type bkt_num(const value_type& obj) const{
	return bkt_num_key(get_key(obj));
}
size_type bkt_num(const value_type& obj, size_t n) const{
	return bkt_num_key(get_key(obj), n);
}


iterator find(const key_type& key){
	size_type n = bkt_num_key(key);
	...
}
size_type count(const key_type& key) const{
	const size_type n = bke_num_key(key);
}
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++(){
...
	size_type bucket = ht->bkt_num(old->val);
...
}

以上是通过传入hash-code进行modulus运算得到对应bucket编码的实现代码。

四、unordered_set, unordered_map, unordered_multiset, unordered_multimap

前面基于rb_tree实现的set/map是associated的容器,而这里基于hashtable实现的set/map便是unordered(未定式)的容器,因为其元素排序在某一时刻是会被打乱的。

在这里插入图片描述

由上图可知,基于hashtable实现的set容器只需指定一个模板参数typename Key即可,而map容器只需指定两个模板参数typename Key和typename Value即可。

  • unordered_set
    在这里插入图片描述在这里插入图片描述
  • unordered_multimap
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值