從 Redis 源碼了解雙向鏈表的設(shè)計(jì)與實(shí)現(xiàn)
近期一直嘗試用go語言復(fù)刻redis,所以開始深入研究redis那些巧妙的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)與實(shí)現(xiàn),本篇文章將針對redis中鏈表的設(shè)計(jì)與實(shí)現(xiàn)進(jìn)行源碼級別的分析,希望對你有所啟發(fā)。
詳解redis中鏈表的設(shè)計(jì)與實(shí)現(xiàn)
鏈表底層結(jié)構(gòu)的設(shè)計(jì)
鏈表是由無數(shù)個(gè)節(jié)點(diǎn)也就是我們常說的listNode構(gòu)成,每個(gè)節(jié)點(diǎn)通過前驅(qū)和后繼節(jié)點(diǎn)指針維護(hù)其前驅(qū)和后繼節(jié)點(diǎn)信息:
對應(yīng)的我們也給出redis中鏈表節(jié)點(diǎn)listNode 的源碼,它位于adlist.h的定義中,可以看到它通過prev指針和next指針分別管理當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)和后繼節(jié)點(diǎn),然后通過value指針維護(hù)當(dāng)前節(jié)點(diǎn)的值,由這一個(gè)個(gè)節(jié)點(diǎn)的串聯(lián)構(gòu)成雙向鏈表:
typedef struct listNode {
//指向前驅(qū)節(jié)點(diǎn)
struct listNode *prev;
//指向后繼節(jié)點(diǎn)
struct listNode *next;
//維護(hù)當(dāng)前節(jié)點(diǎn)的值的指針
void *value;
} listNode;
雙向鏈表需要一個(gè)頭指針和尾指針管理首尾節(jié)點(diǎn),從而實(shí)現(xiàn)后續(xù)靈活的頭插法和尾插法的操作,所以在設(shè)計(jì)雙向鏈表的時(shí)候,我們就需要一個(gè)head和tail指針管理鏈表的首尾節(jié)點(diǎn)。同時(shí),為了實(shí)現(xiàn)O(1)級別的長度計(jì)算,在元素添加或者刪除操作的時(shí)候,我們還需要一個(gè)len字段記錄當(dāng)前鏈表的長度:
而redis中雙向鏈表的結(jié)構(gòu)體list 也是同理:
typedef struct list {
//頭節(jié)點(diǎn)指針
listNode *head;
//尾節(jié)點(diǎn)指針
listNode *tail;
//......
//鏈表長度
unsigned long len;
} list;
了解了基本的數(shù)據(jù)結(jié)構(gòu),我們再來說說鏈表的初始化,redis中的雙向鏈表會(huì)為其分配一塊內(nèi)存空間,然后將頭尾節(jié)點(diǎn)的指針設(shè)置為空,長度初始化為0:
對應(yīng)的我們給出雙向鏈表初始化的源碼即位于adlist.c的listCreate函數(shù),它完成空間分配和指針、長度初始化之后返回這個(gè)鏈表的指針:
list *listCreate(void)
{
//為list結(jié)構(gòu)體分配內(nèi)存空間
struct list *list;
if ((list = zmalloc(sizeof(*list))) == NULL)
return NULL;
//頭尾指針初始化設(shè)置為空
list->head = list->tail = NULL;
//鏈表長度設(shè)置為0
list->len = 0;
//......
return list;
}
節(jié)點(diǎn)頭插法的實(shí)現(xiàn)
通過上文我們了解了鏈表的基本數(shù)據(jù)結(jié)構(gòu),接下來我們就來聊聊鏈表的第一個(gè)操作,也就是頭插法,這個(gè)操作就是將最新的節(jié)點(diǎn)插入的鏈表的首部,我們以初次插入為例,此時(shí)鏈表全空,雙向鏈表初始化節(jié)點(diǎn)之后,就會(huì)讓鏈表的頭尾指針指向這個(gè)node,然后長度自增為1:
若非第一次操作,則初始化一個(gè)新節(jié)點(diǎn)之后,讓這個(gè)節(jié)點(diǎn)指向原有的頭節(jié)點(diǎn),最后讓原有的頭節(jié)點(diǎn)作為新節(jié)點(diǎn)的后繼即可:
圖片
對此我們也給出頭插法的源碼,可以看到它會(huì)傳入當(dāng)前需要操作的鏈表和新節(jié)點(diǎn)的value指針完成節(jié)點(diǎn)生成和頭插工序,對應(yīng)的源碼操作細(xì)節(jié)和上述講解大體一致,讀者可自行參閱:
list *listAddNodeHead(list *list, void *value)
{
//初始化node節(jié)點(diǎn)內(nèi)存空間
listNode *node;
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
//value指針指向傳入的值
node->value = value;
//如果鏈表長度為0,則讓首尾節(jié)點(diǎn)指向這個(gè)node,然后node前驅(qū)和后繼節(jié)點(diǎn)為空
if (list->len == 0) {
list->head = list->tail = node;
node->prev = node->next = NULL;
} else {
//節(jié)點(diǎn)的前驅(qū)指向空,后繼節(jié)點(diǎn)指向原有的頭節(jié)點(diǎn),完成后再讓原有的頭節(jié)點(diǎn)作為新節(jié)點(diǎn)的后繼節(jié)點(diǎn)
//最后head指針指向當(dāng)前node
node->prev = NULL;
node->next = list->head;
list->head->prev = node;
list->head = node;
}
//維護(hù)一下鏈表的長度+1
list->len++;
return list;
}
尾插法的實(shí)現(xiàn)
尾插法就是將最新節(jié)點(diǎn)插入到鏈表末尾,初次插入和頭插法一致,即頭指針head和尾指針tail都指向最新node節(jié)點(diǎn),這里就不做贅述。 我們重點(diǎn)說說常規(guī)操作的尾插法,雙向鏈表在進(jìn)行尾插法時(shí)步驟如下:
- 新節(jié)點(diǎn)前驅(qū)節(jié)點(diǎn)指向原有尾節(jié)點(diǎn)。
- 原有的尾節(jié)點(diǎn)后繼指針指向新節(jié)點(diǎn)。
- 修改tail指針指向,讓新節(jié)點(diǎn)作為最新的尾節(jié)點(diǎn)。
尾插法的函數(shù)為listAddNodeTail,入?yún)橐M(jìn)行操作的list指針和value值,操作步驟的上圖表述基本一致,讀者可結(jié)合注釋自行參閱:
list *listAddNodeTail(list *list, void *value)
{
listNode *node;
//分配node內(nèi)存空間
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
//node的value指針指向value
node->value = value;
//如果長度為0,則首尾指針指向這個(gè)node
if (list->len == 0) {
list->head = list->tail = node;
node->prev = node->next = NULL;
} else {
//新節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)指向尾節(jié)點(diǎn),然后讓原有尾節(jié)點(diǎn)指向新節(jié)點(diǎn),最后讓tail指針指向新節(jié)點(diǎn)
node->prev = list->tail;
node->next = NULL;
list->tail->next = node;
list->tail = node;
}
//長度信息維護(hù)一下
list->len++;
return list;
}
指定節(jié)點(diǎn)插入
該函數(shù)會(huì)傳入修改前驅(qū)后繼關(guān)系的節(jié)點(diǎn),如果希望將新節(jié)點(diǎn)n插入到舊節(jié)點(diǎn)后面,則會(huì)讓新節(jié)點(diǎn)n的前驅(qū)指向原有節(jié)點(diǎn),后繼節(jié)點(diǎn)指向原有節(jié)點(diǎn)的后繼,最后讓新節(jié)點(diǎn)的前驅(qū)后繼節(jié)點(diǎn)指向插入的新節(jié)點(diǎn)n:
同理插入前面也很節(jié)點(diǎn)后添加差不多,這里就不多贅述,對此我們給出listInsertNode的源碼,可以看到它傳入需要進(jìn)行操作的list指針,再傳入需要維護(hù)新關(guān)系的old_node指針和需要插入的value,將value封裝為node之后,如果after為1則執(zhí)行上述所說的old_node后節(jié)點(diǎn)插入操作:
- node的前驅(qū)指向old_node。
- node后繼指向old_node的后繼。
- old_node的next指針和old_node的后繼節(jié)點(diǎn)都指向node。
對應(yīng)的源碼如下,讀者可參考筆者上述圖解并結(jié)合源碼注釋了解整個(gè)插入過程:
list *listInsertNode(list *list, listNode *old_node, void *value, int after) {
listNode *node;
//節(jié)點(diǎn)初始化并設(shè)置value
if ((node = zmalloc(sizeof(*node))) == NULL)
return NULL;
node->value = value;
//如果after為1則將新節(jié)點(diǎn)插入到old_node后面
if (after) {
//node前驅(qū)指向old_node,node指向old_node的后繼
node->prev = old_node;
node->next = old_node->next;
//如果old_node是尾節(jié)點(diǎn),則讓tail指向新插入的node
if (list->tail == old_node) {
list->tail = node;
}
} else {
//將新節(jié)點(diǎn)插入到old_node前面
node->next = old_node;
node->prev = old_node->prev;
//如果old_node是頭節(jié)點(diǎn),則修改head指向,讓其指向新節(jié)點(diǎn)
if (list->head == old_node) {
list->head = node;
}
}
//將node原有的前驅(qū)后繼節(jié)點(diǎn)指向當(dāng)前node維護(hù)的前驅(qū)和后繼節(jié)點(diǎn)
if (node->prev != NULL) {
node->prev->next = node;
}
if (node->next != NULL) {
node->next->prev = node;
}
//維護(hù)一下長度
list->len++;
return list;
}
獲取指定位置的元素
雙向鏈表支持基于索引查找指定位置的元素,操作時(shí)間復(fù)雜度為O(n),我們以從頭查找為例,如果希望查找索引2的元素,也就是第3個(gè)元素,它就會(huì)從head開始跳越2條,由此走到第3個(gè)節(jié)點(diǎn)的位置并返回這個(gè)節(jié)點(diǎn)的指針:
對應(yīng)我們給出listIndex的源碼,可以看到如果傳入的index為負(fù)數(shù),則說明調(diào)用者要從后往前找,假設(shè)我們傳入-2也就是要找到倒數(shù)第2個(gè)元素,最終取正計(jì)算得到1,這也就意味著我們只需從尾節(jié)點(diǎn)跳1下就能得到倒數(shù)第2個(gè)元素,而index若為正數(shù)則是順序查找,原理如上圖解析,這里就不多贅述了,讀者可自行查閱listIndex函數(shù)及其源碼:
listNode *listIndex(list *list, long index) {
listNode *n;
//如果小于0,說明從后往前照
if (index < 0) {
//將負(fù)數(shù)轉(zhuǎn)為正數(shù),例如傳入-2,也就找倒數(shù)第2個(gè)元素,轉(zhuǎn)為正為1,也就是往前1跳,返回這個(gè)node
index = (-index)-1;
n = list->tail;
while(index-- && n) n = n->prev;
} else {
//說明從前往后照,跳n跳即可得到對應(yīng)元素
n = list->head;
while(index-- && n) n = n->next;
}
return n;
}
刪除指定位置的元素
最后一個(gè)就是鏈表刪除操作了,操作比較簡單,讓被刪除節(jié)點(diǎn)的前驅(qū)和后繼節(jié)點(diǎn)構(gòu)成關(guān)聯(lián)關(guān)系,然后釋放當(dāng)前被刪節(jié)點(diǎn),然后減小一下長度即可:
對應(yīng)的源碼如下,讀者可自行參閱學(xué)習(xí):
void listDelNode(list *list, listNode *node)
{
//如果node前驅(qū)有節(jié)點(diǎn),則讓這個(gè)節(jié)點(diǎn)指向被刪除節(jié)點(diǎn)的后繼
//反之說明這個(gè)節(jié)點(diǎn)是頭節(jié)點(diǎn),則讓head指向這個(gè)后繼節(jié)點(diǎn)
if (node->prev)
node->prev->next = node->next;
else
list->head = node->next;
//如果這個(gè)節(jié)點(diǎn)有后繼節(jié)點(diǎn),則讓這個(gè)后繼的prev指向被刪節(jié)點(diǎn)的前驅(qū)
//反之說明被刪的是尾節(jié)點(diǎn),則讓tail指針指向被刪節(jié)點(diǎn)的后繼
if (node->next)
node->next->prev = node->prev;
else
list->tail = node->prev;
//釋放被刪除節(jié)點(diǎn)的內(nèi)存空間,并減小鏈表長度
if (list->free) list->free(node->value);
zfree(node);
list->len--;
}
小結(jié)
自此我們將redis底層的雙向鏈表的設(shè)計(jì)與實(shí)現(xiàn)的源碼進(jìn)行的深入分析,從中了解到redis雙向鏈表數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)和節(jié)點(diǎn)操作的實(shí)現(xiàn)細(xì)節(jié),希望對你有所幫助。