纯属个人理解记录,大神请绕道。如有错误,感谢指出。
哈希表
哈希表的作用是为了提高查询的速度:通过将key映射为数组的下标从而实现o(1)复杂度的查找。
数组的大小是有限的,而key的范围一般都比数组大小大,所以必然会出现冲突的情况,即两个不同的key被映射到了数组的同一个位置。
一般的解决办法有两个:
- 设置算法进行再映射,直到找到空闲的位置
- 数组中不存储单个的元素,而是存储一个链表,映射到相同位置的元素存储在一个链表中。通过合理设置哈希算法,不出现单个链表过长的情况,复杂度依然是o(1)级别。
单向链表
链表又分为单向链表和双向链表。
单向链表:
单向列表结构简单。
查找操作使用简单的遍历来完成。
在节点a后增加一个节点n的操作如下:
n->next = a->next;
a->next=n;
删除一个节点n的操作比较复杂,必须从头节点(head)进行遍历,找到节点n的前一个节点,假设为a:
temp=head
while(temp->next!=n){temp=temp->next;}
temp->next=n->next;
可以看出,单向链表的缺点就是删除操作复杂度比较高。
双向链表
所以,引入了双向链表。
增加节点(节点a后增加节点n):
a->next->prev=n;
n->pre=a;
n->next=a->next;
a->next=n;
删除节点n:
n-next->pre=n->pre;
n->pre->next=n->next;
可以看出,由于形成了完全环形的双向链表,无论操作的节点处于何位置,操作都是一样的,这是一个很优良的特性,不用根据不同的情况去判断。
但是双向链表有一个缺点,就是空间占用太大(是单向链表的2倍)。特别是在使用哈希链表的场景,为了追求效率,往往将数组设置得特别大,2倍的空间占用还是很可观的。
hlist_head与hlist_node结构
所以在linuxkernel中使用了hlist_head与hlist_node结构:
include/linux/types.h
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
对于普通节点(非hlist_head后的第一个节点),增加节点(节点a后增加节点n):
n->next=a->next;
a->next=n;
if(n->next)
{
a->next->prev=n;
}
n->pre=a;
和普通双向链表基本是一样的,只是多了一个判空。
但是,如果删除的是第一个节点的话,情况就不一样了。因为第一个节点的结构都不一样,是hlist_head而不是hlist_node,结构里根本就没有next这个变量。
更关键的是,个人理解似乎根本没法判断出要删除的节点到底是不是第一个节点,所以,即使想进行区分判断,分别处理,都是不可行的。
个人理解,似乎有两个可行的办法:
-
将hlist_head中的first名称修改为next,和hlist_node保持一致,进行操作的时候,不管前一个节点是hlist_head还是hlist_node,都统一当做hlist_node处理,并且修改其next变量即可。
这样做似乎不太优雅。 -
linux kernel选择了另一种做法:将hlist_node第二个元素定义为hlist_node **ppre。其实结构是完全一样的,只是用了另一种方式表示。
hlist_node * pre这种写法,表示pre是一个指针。其值指向的地址为hlist_node结构体所在的位置。
如果pre是hlist_node,hlist_node的第一个元素next,本身还是一个指向hlist_node结构的指针。
如果pre是hlist_head,hlist_node的第一个元素first,本身还是一个指向hlist_node结构的指针。
所以说,hlist_node **ppre这种定义方法,就直接将两种情况进行了归一,确实是一种很优雅的实现。
hlist_head相关操作
INIT_HLIST_HEAD&INIT_HLIST_NODE
定义:include/linux/list.h
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)
static inline void INIT_HLIST_NODE(struct hlist_node *h)
{
h->next = NULL;
h->pprev = NULL;
}