1. 程式人生 > >雜湊連結串列及其變種

雜湊連結串列及其變種

前言

先來直觀的比較下普通連結串列和雜湊連結串列:

普通連結串列

普通連結串列的表頭和節點相同

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