Redis使用及原始碼剖析-3.Redis連結串列-2021-1-17
文章目錄
前言
本文對Redis的底層資料結構連結串列做了簡要介紹,涉及的檔案是adlist.h和adlist.c。
一、連結串列簡介
連結串列是一種非常常用的資料結構,在很多高階語言中都有實現。Redis 使用的 C 語言並沒有內建這種資料結構, 所以 Redis 構建了自己的連結串列實現。
連結串列在 Redis 中的應用非常廣泛, 比如列表鍵的底層實現之一就是連結串列: 當一個列表鍵包含了數量比較多的元素, 又或者列表中包含的元素都是比較長的字串時, Redis 就會使用連結串列作為列表鍵的底層實現。
二、連結串列實現
1.連結串列節點實現
在adlist.h中定義了listNode結構代表連結串列節點,如下所示:
/*
* 雙端連結串列節點
*/
typedef struct listNode {
// 前置節點
struct listNode *prev;
// 後置節點
struct listNode * next;
// 節點的值
void *value;
} listNode;
可以看出連結串列節點是雙向的,並且通過void *指標可以存各種型別的值,通過節點的prev和next指標我們就可以連接出一個雙向連結串列如下圖所示:
2.連結串列實現
雖然可以直接用節點來構造雙向連結串列,但是實際連結串列實現時還會再定義一個連結串列結構。如下所示:
/*
* 雙端連結串列結構
*/
typedef struct list {
// 表頭節點
listNode *head;
// 表尾節點
listNode *tail;
// 節點值複製函式
void *(*dup)(void *ptr);
// 節點值釋放函式
void (*free)(void *ptr);
// 節點值對比函式
int (*match)(void *ptr, void *key);
// 連結串列所包含的節點數量
unsigned long len;
} list;
可以看出通過定義list結構體,就可以常數時間內獲得連結串列頭尾節點以及連結串列長度,這些都是隻用連結串列節點來表示連結串列時不具備的。
此外list中還定義了節點值的複製、釋放和對比函式,dup 函式用於複製連結串列節點所儲存的值;free 函式用於釋放連結串列節點所儲存的值;match 函式則用於對比連結串列節點所儲存的值和另一個輸入值是否相等。通過這些函式的定義使得連結串列的特性更加的複雜多樣。
3.連結串列迭代器實現
Redis為了便於訪問連結串列元素,還定義了連結串列的迭代器程式碼如下:
/* Directions for iterators
*
* 迭代器進行迭代的方向
*/
// 從表頭向表尾進行迭代
#define AL_START_HEAD 0
// 從表尾到表頭進行迭代
#define AL_START_TAIL 1
/*
* 雙端連結串列迭代器
*/
typedef struct listIter {
// 當前迭代到的節點
listNode *next;
// 迭代的方向
int direction;
} listIter;
可以看出list的迭代器可以支援兩個方向的遍歷。
4.連結串列API
選取了部分連結串列API進行剖析。建立一個空連結串列的程式碼如下所示:
list *listCreate(void)
{
struct list *list;
// 分配記憶體
if ((list = zmalloc(sizeof(*list))) == NULL)
return NULL;
// 初始化屬性
list->head = list->tail = NULL;
list->len = 0;
list->dup = NULL;
list->free = NULL;
list->match = NULL;
return list;
}
在指定節點之前或者之後插入節點的程式碼如下所示:
/*
* 建立一個包含值 value 的新節點,並將它插入到 old_node 的之前或之後
*
* 如果 after 為 0 ,將新節點插入到 old_node 之前。
* 如果 after 為 1 ,將新節點插入到 old_node 之後。
*
* T = O(1)
*/
list *listInsertNode(list *list, listNode *old_node, void *value, int after) {
listNode *node;
// 建立新節點
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
// 儲存值
node->value = value;
// 將新節點新增到給定節點之後
if (after) {
node->prev = old_node;
node->next = old_node->next;
// 給定節點是原表尾節點
if (list->tail == old_node) {
list->tail = node;
}
// 將新節點新增到給定節點之前
} else {
node->next = old_node;
node->prev = old_node->prev;
// 給定節點是原表頭節點
if (list->head == old_node) {
list->head = node;
}
}
// 更新新節點的前置指標
if (node->prev != NULL) {
node->prev->next = node;
}
// 更新新節點的後置指標
if (node->next != NULL) {
node->next->prev = node;
}
// 更新連結串列節點數
list->len++;
return list;
}
建立和釋放一個迭代器的程式碼如下所示:
/*
* 為給定連結串列建立一個迭代器,
* 之後每次對這個迭代器呼叫 listNext 都返回被迭代到的連結串列節點
*
* direction 引數決定了迭代器的迭代方向:
* AL_START_HEAD :從表頭向表尾迭代
* AL_START_TAIL :從表尾想表頭迭代
*
* T = O(1)
*/
listIter *listGetIterator(list *list, int direction)
{
// 為迭代器分配記憶體
listIter *iter;
if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
// 根據迭代方向,設定迭代器的起始節點
if (direction == AL_START_HEAD)
iter->next = list->head;
else
iter->next = list->tail;
// 記錄迭代方向
iter->direction = direction;
return iter;
}
/* Release the iterator memory */
/*
* 釋放迭代器
*
* T = O(1)
*/
void listReleaseIterator(listIter *iter) {
zfree(iter);
}
連結串列中查詢指定元素函式如下所示:
/*
* 查詢連結串列 list 中值和 key 匹配的節點。
*
* 對比操作由連結串列的 match 函式負責進行,
* 如果沒有設定 match 函式,
* 那麼直接通過對比值的指標來決定是否匹配。
*
* 如果匹配成功,那麼第一個匹配的節點會被返回。
* 如果沒有匹配任何節點,那麼返回 NULL 。
*
* T = O(N)
*/
listNode *listSearchKey(list *list, void *key)
{
listIter *iter;
listNode *node;
// 迭代整個連結串列
iter = listGetIterator(list, AL_START_HEAD);
while((node = listNext(iter)) != NULL) {
// 對比
if (list->match) {
if (list->match(node->value, key)) {
listReleaseIterator(iter);
// 找到
return node;
}
} else {
if (key == node->value) {
listReleaseIterator(iter);
// 找到
return node;
}
}
}
listReleaseIterator(iter);
// 未找到
return NULL;
}
總結
本文對Redis的連結串列結構進行了簡單介紹,如有不當之處,請指正。