哈希——unordered_map以及unordered_set的封装
1.哈希
1.1哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log2Nlog_2 Nlog2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素
重要的就是建立一个有一个的一一映射关系。
插入元素:
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素:
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
以上两种数据处理方式,就是哈希或者散列的方法。
1.2哈希问题
我们知道因为哈希是数据于数组的位置有了映射关系,那必然会出现这种情况:多个数据映射了数组的同一个位置,那么这种现象就是哈希冲突,又叫做哈希碰撞。
2.处理哈希碰撞
处理哈希冲突一般有两种常见的处理方法,那就是闭散列,以及开散列。
2.1闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
这时候就需要我们开始线性探测了。
这里的线性探测就需要我们根据每个数组位置的状态,来判断,如果不是空我们就向后遍历,如果是空,就停止。
3.闭散列代码实现
3.1成员
我们之前讲了,闭散列的遍历需要我们根据每个数据的状态来判断,因此闭散列就必然需要一个状态位,同时这里们同样使用pair的数据对来存储数据,为后面的unordered_map和undered_set的封装做准备。

3.2插入接口实现
首先我们先不用考虑是否需要扩容的问题,则插入代码一定是:

关于扩容问题:
1.数据集种,空间利用率高,但遍历效率低
2.数据不集中,空间利用率低,但遍历效率高
因此我们需要一个数据判断是否需要扩容,这时候,就引出负载因子,一般是0.7,我们判断如果数组中的空间利用率高于0.7那么就需要扩容,反之就不需要。
但是扩容后随之而来的问题就是,如果扩容了,那么前面已经插入的数据的位置,就又要改变,难道我们需要重新遍历,然后重新插入吗?显然不是。
我们可以重新开辟一个哈希表,然后让这个哈希表的数组容量是需要扩容的容量的2,倍,然后关于数据的插入,我们可以值调用自己的insert函数,让函数帮助我们实现数据插入的过程。

3.3Find接口的实现
关于这个接口的实现,重要的还是先求出,所找数据,在哈希表中的下标,然后从这个下标开始往后遍历,接着重要的是判断条件,要知道,即使中间有DETLETE状态的位置,我们依然不能停止遍历,所以,可以用既不为空,有时EXIST的状态来判断循环。

4.泛型思想在哈希里的体现
我们知道,一个数据结构的重要特性就是有模板,可以复用,同样的我们的哈希表内同样也可以存储各种类型的数据,如果插入的int’类型的数据,那么我们的哈希下标可以直接求出来,但如果我们存储的时string的数据呢?
这里我们不妨仿照之前的map和set以及优先级队列的方法,使用仿函数。
当然这里的仿函数使用也是有两种方法的:
1.直接通过定义时传入仿函数的类型
2.使用模板特化


下面这份代码就是模板的特化。
5.开散列
开散列是重要的一种哈希方式,因为后面的哈希方式的map’和set都是用的开散列方式实现的。
5.1开散列概念
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
简单来说,开散列里面vector存储的不是数据而是一个Node*的指针,vector相当于指针数组。
6.开散列代码实现
开散列里面存储的是指针,每个指针偶对应着一个单链表,因此:


6.1开散列的插入
开散列的插入就是链表的插入,这里这里我们如果用头删的方式实现,然后将新节点设置为hashi的节点,会更简单一些:

同样的开散列也是需要扩容的,不过开散列扩容的要求是空间利用率等于1
的时候在扩容,同样我们需要设置一个新的散列表,然后接下来就是怎么将原来的散列表的数据正确的插入到新的散列表,难道还是和闭散列表的方式一样吗?这里就不用了,因为我们是链表,我们不妨直接将原链表的节点摘出来,然后将他插入到新的正确的位置上,这样就,同时这里我们因为存储的是node,所以还要自己delet原来的空间。

6.2开散列的erase
开散列的erase我们需要注意的就是先找到所在的哈希桶,然后删除时要注意保留前后节点,链接在一起:

7.unordered_map和undered_set的封装
这里的哈希map和set的封装和搜索树的map和set的封装的模板形式是一样的。不过前者底层是一个开散列表,后者底层是一个红黑树。
同样
unordered_map底层是<k,pair<k,v>>类型的
unordered_set的底层是<k,k>类型的
所以这里我们需要对开散列表的节点数据类型,使用模板T来表示,为了完成复用。
同样的为了让map和set使用同一个哈希表但是可以拿出不同的数据,这里我们同样需要在设置一个仿函数,用来特殊处理map和set对于的节点node的T不同数据类型,因为思想和搜索树形式的mpa和set相同,这里不过多解释:


7.1迭代器的封装
迭代器同样我们需要封装起来,因为我们要做到使用迭代器可以拿到节点的数据,而不是节点,因此需要对他进行各种形式的重载:

7.2迭代器的++
哈希表的重点和难点就是怎么实现迭代器的++,我们知道,如果是正常的下一个位置不为nullptr,那么迭代器的++就是:
cur=cur->next;
return cur;即可
但是如果我们的下一个位置是nullptr呢?
所以这里我们不妨将这个哈希表指针引入我们的迭代器。
有了HashTable,那么我们就可以通过遍历这个HashTable来找寻下一个桶,所以;

7.3const迭代器和普通迭代器
我们上面的代码已经给出了,为了让一份封装的迭代器实现两个不同的迭代器类型,我们采用来模板的方式,诸多细节桶map和set的树的方式相同,而且关于两者的转换和map和set相同,这一部分呢可以参考上篇文章。
8.总结
哈希是数据结构,但是unordered_map和unordered_set,是一种容器,前者数据结构的分析算是我们新讨论的内容,但是后面的容器包装,大家可以多参考上文中map和set的树形式的封装,难点很多,但理清楚关系后,就很清晰。
