一.哈希表
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。
数组的特点是:寻址容易,插入和删除困难
而链表的特点是:寻址困难,插入和删除容易
哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”
左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
举一个例子,假如我的数组A中,第i个元素里面装的key就是i,那么数字3肯定是在第3个位置,数字10肯定是在第10个位置。哈希表就是利用利用这种基本的思想,建立一个从key到位置的函数,然后进行直接计算查找。
好的散列函数=计算简单+分布均匀(计算得到的散列地址分布均匀)
二.linux内核里面哈希表的实现
1.哈希链表表头
2.哈希链表节点
因为哈希链表并不需要双向循环的技能,它一般适用于单向散列的场景。所以,为了减少开销,并没有用struct hlist_node{}来代表哈希表头,而是重新设计struct hlist_head{}这个数据结构。此时,一个哈希表头就只需要4Byte了,相比于struct hlist_node{}来说,存储空间已经减少了一半。这样一来,在需要大量用到哈希链表的场景,其存储空间的节约是非常明显的,特别是在嵌入式设备领域。
3.两级指针pprev的目的及意义
a.如果没有pprev
显然链表结构会变成单链表的结构,对于节点插入还好,但是对于节点删除便无法直接删除,需要遍历链表才能找到删除节点的前一个节点,造成效率降低
b.如果pprev为一级指针
则结构变为
在往哈希链myhlist里插入node1时
在插入node2~node4以及后续其他节点时
显然在表头的第一个位置上插入元素,和插入在哈希链表的其他位置上的代码处理逻辑是不一样的,因为哈希表头是list_head类型,而其他节点都是list_node类型,这是linux内核设计者不能容忍的
c.采用二级指针pprev的方案后
还是以删除节点为例,如果要删除首节点,因为node1->pprev里保存的是myhlist的地址,而myhlist.first永远都指向哈希链表的第一个节点,我们要间接改变表头里的hlist_node类型的first指针的值,能想到的最直接的办法当然是二级指针,这是两级指针的宿命所决定的,为了间接改变一级指针所指的内存地址的场景。这样一来,node节点里的pprev其实指向的是其前一个节点里的第一个指针元素的地址。对于hlist_head来说,它里面只有一个指针元素,就是first指针;而对于hlist_node来说,第一个指针元素就是next。具体如下所示:
当我们在代码中看到类似与*(hlist_node->pprev)这样的代码时,此时正在哈希表里操作当前节点前一个节点里的第一个指针元素所指向的内存地址,只是以间接的方式实现罢了
删除首节点时:
删除非首节点的情况也一样:
下面为linux内核中删除节点的代码
在头指针后添加节点
连续添加节点效果图如下
以上参考:
http://blog.chinaunix.net/uid-23069658-id-4975027.html
https://zhuanlan.zhihu.com/p/45430524