在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 O(logN),即最差情况下 需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次 数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器。
这四个容器与关联式容器(map、set、multimap、multiset)使用方式基本类似,它们都属于关联式容器,但是主要区别有(主要分析unordered_map与map,其他都是对应相同的):
unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
哈希就是一种数据结构,它是将一个元素的关键码通过哈希函数计算出该元素在哈希表当中的位置进行存放,使元素的位置与它的关键码能够建立一一映射的关系,在查找时候通过该哈希函数也能很快的找到。
如果不同元素的关键字通过哈希哈数计算出相同的哈希地址,就叫做哈希冲突或者哈希碰撞。其实产生哈希冲突的主要原因就是哈希函数设计的不合理,设计哈希函数需要尽可能的保证计算出来的地址能均匀分布在整个空间中 。常见哈希函数:
哈希函数还可以根据程序员的需求通过自己设定,写出一个仿函数来计算散列地址,然后将unordered_map的第三个模板参数显示指出对应的自己设定的仿函数。
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
闭散列(又叫开放地址法):当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那 么可以把key存放到冲突位置中的“下一个” 空位置中去。但是如何找到下一个空位置?
方法①—》线性探测:从发生冲突的位置开始,依次向后继续探测,直到找到一个空的位置为止结束探测。Hash(key)+ i (i = 0,1,2,3…)
方法②—》二次探测:从发生冲突的位置开始,并不挨个进行探测,而是每次都增加一个 i ^2。Hash(key) + i ^2(i = 0,1,2,3,4…)。
线性探测实现简单,但是一旦发生冲突就会冲突成为一片。所以二次探测就是线性探测的优化,但是二次探测仍旧会产生冲突
注意1:在插入和查找时,找到空的位置就分别插入和结束,但是在进行删除的时候不能够直接删出,因为可能有个元素与要删除的元素产生的哈希冲突经过线性探测映射到了其他位置,如果直接删除会影响其他元素的搜索,比如删除元素4,如果直接删除掉,44查找起来可能会受影响:
所以,为每一个空间采用一个状态标识来解决直接删除带来的问题,当删除元素时将该对应空间设置为DELETE;在进行查找某个元素时,判断标识位如果是EXIST和DELETE就继续往后探测查找,直到遇到EMPTY结束:
哈希表每个空间进行标记:
EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
注意2:为了避免哈希冲突,当哈希表满足一定条件时还需要进行增容,如何增容是通过负载因子来确定的。负载因子 = 存储的实际数据个数 / 表的大小。负载因子越大,哈希冲突越大,哈希查找效率就越低;负载因子越小,空间利用率就越低。所以对于开放地址法,应该将负载因子设置到0.7~0.8以下。C++JAVA中将负载因子限制为0.75,超过这个值将resize散列表。
注意3:散列表增容不能直接增容,需要重新开空间,将元素重新进行映射。
开散列(拉链法):首先对关键码用散列函数计算散列地址,具有相同地址的关键码 通过一个单链表链接起来形成一个哈希桶,将哈希桶的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
注意1:桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一 个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件
怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时(也就是负载因子
== 1时候),可以给哈希表进行增容。
注意2:散列表增容不能直接增容,需要重新开空间,将元素重新进行映射。
开散列的思考: