前言
关联式容器与序列式容器不同,元素插入关联式容器后,插入位置是由容器采用一定的算法计算的,与插入的时间无关。本文介绍STL中哈系表、集合和map等关联式容器。
哈希表
hashtable是SGI STL中的哈希表,标准库中使用unordered_map代替hashtable,但unordered_map也是以hashtable为基础的。哈希表是一种查找操作只需要o(1)时间复杂度的数据结构,哈希表由一串连续的的bucket构成,每个bucket上挂着一个链表,链表中储存的就是用户的数据,哈系表通过散列函数将用户数据与bucket的下标对应起来,然后挂到bucket链表的尾部。哈系表的结构可以由下图表示:
散列函数
哈希表接受一对键值对,通过散列函数将键与bucket对应起来,hashtable和unordered_map采用除留余数法,假设当前共有N个bucket,键为M,那么该键散列后的值为 M%N,值将插入到下标为M%N的bucket上的头节点处,用户也可以自定义散列函数传入hashtable。
用户查找某个键值对时,通过散列函数找到这个键对应的bucket,然后只需要在这条链表上查找目标键值对即可。
哈希碰撞和rehash
有时会遇到很多元素都挂在同一个bucket上的情况,如下图:
当出现这种情况时,哈希表就退化为了链表,哈希表查找的效率就退化为了链表的查找效率o(n),这显然是不希望看到的。像这样多个数据被散列为同一个值的情况成为哈希碰撞,解决哈希碰撞的办法称为rehash。
hashtable和unordered_map都提供了一个升序的质数表,并使用一个pos指针指向当前使用的质数,rehash时,将pos指向质数表中下一个质数,使用新的质数作为除留余数法的除数,显然bucket的个数与除数的大小是一致的,对原本储存的所有元素重新计算散列值,并挂到相应的bucket上,这样就减少了哈希碰撞。
那么什么时候应该进行rehash呢?unordered_map使用负载因子,负载因子的计算方法为元素的总量/bucket的数量,当负载因子超过1时,unordered_map就进行rehash,可以通过下面的程序验证这个过程:
int main(){
std::unordered_map<int, int> mp;
for(int i=0;i<15;++i){
mp.insert(std::make_pair(i,i));
std::cout << "插入第" << i <<"个键值对:" << std::endl;
std::cout << "负载因子为:" << mp.load_factor() << std::endl;
std::cout << "质数为::" << mp.bucket_count() << std::endl;
}
return 0;
}
运行结果如下:
_Hashtable_iterator
hashtable提供forward_iterator,_Hashtable将从第一个bucket的链表开始,遍历,遍历到链表的末尾时,则跳到下一个bucket的链表上,当然这通过重载operator++来实现,用户是无感知的。由于vector上存储的都是哑节点,因此所有数据都被存储在链表中,迭代器失效规则也与链表相同,仅删除数据会导致被删除节点的iterator失效。
hashmap和hashset
这两种适配器由SGI STL提供,并未归入标准库中,本质上仍旧是哈希表,且使用场景也较少,这里就不介绍这两中适配器,有兴趣可以自行下载源码查看。
map和set
map和set底层都是红黑树结构,map和set的操作与红黑树完全相同,仅是对红黑树操作的封装。
map接受一对键值对,红黑树按键的大小确定键值对的存放位置,这听起来与哈希表很相似,都是通过键值来决定存放位置,加快查询效率,但不同的是,map的增删查改的复杂度都是o(logN)级别的,而哈希表的增删查改都是o(1)级别的;哈希表的由于负载因子的控制,会有很多储存空间闲置,当储存的数据量很大时,这种空间的使用就显得非常浪费,而红黑树使用的空间数量和数据数量相同,因此如果数据量很大时,通常使用红黑树存储,而数据量较小时,可以发挥哈希表的优势,进行快速的增删查改。
set是STL中的集合,在红黑树中,是一种键和值相等的键值对,因此只需要传入一个参数即可。
multimap和multiset是几乎和map与set完全相同的适配器,唯一的不同点是multimap和multiset允许元素重复,而map和set不允许,这是因为STL的红黑树提供了insert_unique和insert_equal两种插入方式,map和set的插入使用的是insert_unqie方式,这使得插入元素的值已存在时会被丢弃,而multimap和multiset使用的insert_equal则不会。
总结
本文我们介绍了STL中的关联式容器及适配器,至此,我们已经介绍了STL中所有的容器及适配器,STL是数据结构和算法的集大成者,STL的容器和适配器几乎涵盖了所有常见的数据结构(除了图),值得我们反复学习推敲。