雜湊連結串列及其變種
前言
先來直觀的比較下普通連結串列和雜湊連結串列:
普通連結串列
普通連結串列的表頭和節點相同
struct list_head {
struct list_head *next, *prev;
};
雜湊連結串列
雜湊連結串列頭
struct hlist_head {
struct hlist_node *first;
};
雜湊連結串列節點
struct hlist_node {
struct hlist_node *next, **pprev;
};
設計原理
Linux連結串列設計者認為雙指標表頭雙迴圈連結串列對於HASH表來說過於浪費,因而另行設計了一套用於HASH表的hlist資料結構,
即單指標表頭雙迴圈連結串列。hlist表頭僅有一個指向首節點的指標,而沒有指向尾節點的指標,這樣在海量的HASH表中儲存
的表頭就能減少一半的空間消耗。
這裡還需要注意:struct hlist_node **pprev,也就是說pprev是指向前一個節點(也可以是表頭)中next指標的指標。
Q:為什麼不使用struct hlist_node *prev,即讓prev指向前一個節點呢?
A:因為這時候表頭(hlist_head)和節點(hlist_node)的資料結構不同。如果使用struct hlist_node *prev,只適用於前一個為節點
的情況,而不適用於前一個為表頭的情況。如果每次操作都要考慮指標型別轉換,會是一件麻煩的事情。
所以,我們需要一種統一的操作,而不用考慮前一個元素是節點還是表頭。
struct hlist_node **pprev,pprev指向前一個元素的next指標,不用管前一個元素是節點還是表頭。
當我們需要操作前一個元素(節點或表頭),可以統一使用*(node->pprev)來訪問和修改前一元素的next(或first)指標。
原理圖如下:
常用操作
(1) 初始化
/* * Double linked lists with a single pointer list head. * Mostly useful for hash tables where the two pointer list head is too wasteful. * You lose the ability to access the tail in O(1). */ #define HLIST_HEAD_INIT { .first = NULL } #define HLIST_HEAD (name) struct hlist_head name = { .first = NULL } #define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)
(2) 插入
/* next must be != NULL */
static inline void hlist_add_before(struct hlist_node *n, struct hlist_node *next)
{
n->pprev = next->pprev;
n->next = next;
next->pprev = &n->next;
*(n->pprev) = n;
}
(3) 刪除
static inline void __hlist_del(struct hlist_node *n)
{
struct hlist_node *next = n->next;
struct hlist_node **prev = n->pprev;
*pprev = next;
if (next)
next->pprev = pprev;
}
(4) 遍歷
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *) 0)->MEMBER)
/*
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*/
#define container_of(ptr, type, member) ({ \
const typeof(((type *) 0)->member) * __mptr = (ptr); \
(type *) ((char *) __mptr - offsetof(type, member)); })
#define hlist_entry(ptr, type, member) container_of(ptr, type, member)
#define hlist_for_each(pos, head) \
for (pos = (head)->first; pos; pos = pos->next)
/**
* hlist_for_each_entry - iterate over list of given type
* @tpos: the type * to use as a loop cursor.
* @pos: the &struct hlist_node to use a loop cursor.
* @head: the head for your list.
* @member: the name of the hlist_node within the struct.
*/
#define hlist_for_each_entry(tpos, pos, head, member) \
for (pos = (head)->first; \
pos && ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \
pos = pos->next)
雜湊連結串列變種
以下是作者的說明:
Special version of lists, where end of list is not a NULL pointer,
but a 'nulls' marker, which can have many different values.
(up to 2^31 different values guaranteed on all platforms)
In the standard hlist, termination of a list is the NULL pointer.
In this special 'nulls' variant, we use the fact the objects stored in
a list are aligned on a word (4 or 8 bytes alignment).
We therefore use the last significant bit of 'ptr':
Set to 1: This is a 'nulls' end-of-list maker (ptr >> 1)
Set to 0: This is a pointer to some object (ptr)
設計原理
當遍歷標準的雜湊連結串列時,如果節點為NULL,表示連結串列遍歷完了。
雜湊連結串列變種和標準雜湊連結串列的區別是:連結串列的結束節點不是NULL。如果first或者next指標的最後一位為1,
就說明遍歷到連結串列尾部了。
Q:為什麼可以根據節點指標的最後一位是否為1來判斷連結串列是否結束?
A:因為在一個結構體中,其元素是按4位元組(32位機器)或者8位元組(64位機器)對齊的。所以有效的節點指標的
最後一位總是為0。因此我們可以通過把節點指標的最後一位置為1,來作為結束標誌。
/* 表頭 */
struct hlist_nulls_head {
struct hlist_nulls_node *first;
};
/* 節點 */
struct hlist_nulls_node {
struct hlist_nulls_node *next, **pprev;
};
原理圖如下:
常用操作
(1) 初始化
#define INIT_HLIST_NULLS_HEAD(ptr, nulls) \
((ptr)->first = (struct hlist_nulls_node *) (1UL | (((long) nulls) << 1)))
(2) 判斷是否為結束標誌
/*
* ptr_is_a_nulls - Test if a ptr is a nulls
* @ptr: ptr to be tested
*/
static inline int is_a_nulls(const struct hlist_nulls_node *ptr)
{
return ((unsigned long) ptr & 1);
}
(3) 獲取結束標誌
/*
* get_nulls_value - Get the 'nulls' value of the end of chain
* @ptr: end of chain
* Should be called only if is_a_nulls(ptr);
*/
static inline unsigned long get_nulls_value(const struct hlist_nulls_node *ptr)
{
return ((unsigned long)ptr) >> 1;
}
(4) 插入
把節點n插入為連結串列的第一個節點。
static inline void hlist_nulls_add_head(struct hlist_nulls_node *n, struct hlist_nulls_head *h)
{
struct hlist_nulls_node *first = h->first;
n->next = first;
n->pprev = &h->first;
h->first = n;
if (! is_a_nulls(first))
first->pprev = &n->next;
}
(5) 刪除
/*
* These are non-NULL pointers that will result in page faults
* under normal circumstances, used to verify that nobody uses
* non-initialized list entries.
*/
#define LIST_POISON1 ((void *) 0x00100100 + POISON_POINTER_DELTA)
#define LIST_POISON2 ((void *) 0x00200200 + POISON_POINTER_DELTA)
static inline void __hlist_nulls_del(struct hlist_nulls_node *n)
{
struct hlist_nulls_node *next = n->next;
struct hlist_nulls_node **pprev = n->pprev;
*pprev = next;
if (! is_a_nulls(next))
next->pprev = pprev;
}
static inline void hlist_nulls_del(struct hlist_nulls_node *n)
{
__hlist_nulls_del(n);
n->pprev = LIST_POISON2; /* 防止再通過n訪問連結串列 */
}
(6) 遍歷
同標準雜湊連結串列的基本一樣。
hlist_nulls_for_each_entry(tpos, pos, head, member)
hlist_nulls_for_each_entry_from(tpos, pos, member)
Author
zhangskd @ csdn blog