雙向鏈表都不懂,還說懂Redis?

目錄node

redis源碼分析系列文章redis

前言數據結構

API使用架構

lpush左側插入數據app

rpush右側插入數據函數

刪除某個數據源碼分析

修改某個數據spa

具體邏輯圖.net

雙向鏈表的定義指針

節點ListNode

總體架構

雙向鏈表的實現

建立表頭

清空表

添加元素到表頭

添加元素到表尾

插入

刪除

總結


redis源碼分析系列文章

 

[Redis源碼系列]在Liunx安裝和常見API 

爲何要從Redis源碼分析 

 

String底層實現——動態字符串SDS 

前言

hello,又見面了。不要問爲何,問就是勤勞。立刻要開啓爆更模式啦。在Redis中鏈表List的應用很是普遍,可是Redis是採用C語言來寫,底層採用雙向鏈表實現(這邊提一嘴,若是是科班出身或者大學有學過數據結構的同窗,能夠划走啦)。咱們今天的重點就是雙向鏈表。

API使用

先來使用一下API。若是以前有用過的同窗,能夠直接跳到下一小節。

lpush左側插入數據

使用lpush命令往list的左側中插入a,b,c三個字符,這邊注意順序,查詢出來的是c,b,a。下面會說爲何,先挖個坑。

rpush右側插入數據

使用rpush命令往list中插入d,e兩個字符,查詢出來的順序是和咱們想的同樣,最後兩位是d,e。

刪除某個數據

使用lrem命令刪除a字符,那麼中間1表明什麼意思呢?其爲count,表示移除列表中與a相等的元素個數。即若是count>0,表示從表頭開始向表尾搜索,移除count個與a相等的元素。若是count<0,表示從表尾開始向表頭搜索,移除count個與a相等的元素。若是count=0,移除全部與a相等的元素,由於是移除全部,因此無論從表頭仍是表尾,結果是同樣的。

修改某個數據

使用lset命令將mylist的下標爲1的元素修改成dd,原來list爲c ,b,d,e,修改後的結果爲c,dd,d,e。

具體邏輯圖

這邊看不懂不要緊,下面會針對每一個模塊詳細說明。

雙向鏈表的定義

節點ListNode

包括頭指針prev,尾指針next,當前的值value,以下圖所示。每一個節點都有兩個指針,既能從表頭根據尾指針找到表尾,又能從表尾根據頭指針prev找到表頭,若是將他們連起來,就構成了雙向鏈表。

具體代碼以下:

//定義鏈表節點的結構體 
typedef struct listNode {
    //前面一個節點的指針 
    struct listNode *prev;
    //後面一個節點的指針 
    struct listNode *next;
    //當前節點的值的指針 ,由於值的類型不肯定 
    void *value;
} listNode;

 

總體架構

包括頭指針head,尾指針tail,整個鏈表長度len,一些函數(我的認爲不重要,若是有知道的小夥伴歡迎評論),以下圖所示。頭指針head指向整個鏈表的第一個節點,尾指針tail指向整個鏈表的最後一個節點。

具體代碼以下:

//定義鏈表,對鏈表節點的再封裝 
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表結構,首先須要判斷當前是否有可分配的空間來建立,使用zmalloc方法來分配空間,若是分配不了,則返回NULL,若是能夠分配,則繼續。接着賦值list的頭節點head和尾節點tail爲NULL,len爲0,賦值相關函數爲NULL。最後返回結果list。

//建立一個表頭,返回值是鏈表結構的指針 
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;
} 

 

 

清空表

傳入list的指針,首先定義當前節點current,使其指向頭指針,定義len,使其等於list的長度。接着進行循環,每次len減一,定義新節點next,始終指向當前節點current的下一個節點,若是有值,則釋放該節點,當前節點current後移,next節點一樣後移。直到len爲0,釋放完全部節點,退出循環。最後賦值list的頭節點head和尾節點tail爲NULL,len爲0。

注意:這邊和SDS同樣,清空並非直接刪除list,而是刪除其數據,外層的list結構仍然存在。這其實上是惰性刪除。

void listEmpty(list *list)
{
    unsigned long len;
    //定義兩個節點指針current和next
    listNode *current, *next;
    //當前節點指針current指向list的頭節點位置,即list的第一個數據
    current = list->head;
    //len爲list的長度
    len = list->len;
    //開始循環,每次len減1
    while(len--) {
        //先讓下一個指針指向下一個節點,由於底下直接釋放當前節點,若是不在此處複製,底下就獲取不到了
        next = current->next;
        //釋放當前節點的值
        if (list->free) list->free(current->value);
        //釋放當前節點
        zfree(current);
        //當前節點等於剛纔的下一個節點next,即開始日後移,開始下一輪循環
        current = next;
    }
    //釋放完給頭指針head,尾指針tail賦值爲NULL
    list->head = list->tail = NULL;
    //len賦值0
    list->len = 0;
}

 

添加元素到表頭

添加元素到表頭,首先新建一個新節點node,判斷是否有內存分配,若是有,則繼續,若是沒有,則返回NULL,退出方法。這邊新節點是用來存在輸入參數中的value的,因此須要內存。接着將新節點node的value值賦值爲輸入參數value。最後須要調整list的頭指針,尾指針,原來第一個節點的指針狀況(這邊看下圖,描述起來有點混亂,圖片一目瞭然)。最最後,就是list的len加1,返回list。

舉個例子,若是要在list中插入節點f,首先將節點的頭指針賦值爲空(對應步驟1),而後將新節點的尾指針next指向第一個節點(對應步驟2),將第一個節點的prev指向新節點(對應步驟3),最後將list的頭指針head指向新節點(對應步驟4)。這邊須要注意的是,步驟2和步驟3須要在步驟4前面,否則會找到第一個節點。

具體代碼以下:

//添加一個元素到表頭 
list *listAddNodeHead(list *list, void *value)
{
    listNode *node;

    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;
    node->value = value;//爲當前節點賦值 
    //若是當前list爲空 
    if (list->len == 0) {
        list->head = list->tail = node;//頭尾指針都指向改節點 
        node->prev = node->next = NULL;//當前節點的頭尾指針都爲null 
    } else {//若是當前list不爲空 
        node->prev = NULL;//新節點的頭指針爲null 
        node->next = list->head;//新節點的尾指針指向原來的尾指針 
        list->head->prev = node;//原來的第一個節點的頭指針指向新節點 
        list->head = node;//鏈表的頭指針指向新節點 
    }
    list->len++;//list長度+1 
    return list;
}

 

 

添加元素到表尾

添加元素到表尾,首先新建一個新節點node,判斷是否有內存分配,若是有,則繼續,若是沒有,則返回NULL,退出方法。這邊新節點是用來存在輸入參數中的value的,因此須要內存。接着將新節點node的value值賦值爲輸入參數value。最後須要調整list的頭指針,尾指針,原來最後一個節點的指針狀況(這邊看下圖,描述起來有點混亂,圖片一目瞭然)。最最後,就是list的len加1,返回list。

舉個例子,若是要在list中插入節點f,首先將節點的尾指針賦值爲空(對應步驟1),而後將新節點的頭指針指向最後一個節點(對應步驟2),將最後一個節點的next指向新節點(對應步驟3),最後將list的尾指針tail指向新節點(對應步驟4)。

步驟以下:

//添加元素到表尾
list *listAddNodeTail(list *list, void *value)
{
    //新建節點node
    listNode *node;
    //嘗試分配內存
    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;
    //爲新節點node賦值
    node->value = value;
    //調整指針
    if (list->len == 0) {
        list->head = list->tail = node;
        node->prev = node->next = NULL;
    } else {
        node->prev = list->tail;
        node->next = NULL;
        list->tail->next = node;
        list->tail = node;
    }
    //len加1
    list->len++;
    return list;
} 

 

 

插入

爲list的某個節點old_node的after(先後)查詢新值value,首先新建一個新節點node,判斷是否有內存分配,若是有,則繼續,若是沒有,則返回NULL,退出方法。這邊新節點是用來存在輸入參數中的value的,因此須要內存。(這段話是否是聽的耳朵都起繭子啦🤣)接着根據after的值肯定是在節點old_node的前面插入新數據,仍是在節點old_node的後面插入新數據,具體的是指針的調整。最後len加1,返回list。

//在list的某個位置old_node的after(先後)插入value值 
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) {//大於0,後面插入新節點 
        node->prev = old_node;
        node->next = old_node->next;
        if (list->tail == old_node) {
            list->tail = node;
        }
    } else {//小於0,前面插入新節點 
        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;
}

 

 

刪除

從list中刪除節點node,若是該節點的前面存在節點,使其前面一個節點的next指針指向node後面一個節點的地址,其實就是跳過了node節點,若是該節點的前面不存在節點,則將list的頭指針指向node的下一節點地址。一樣的,若是該節點的後面存在節點,邏輯同樣的。最後釋放要刪除的節點node內存,len減1。

//從鏈表list中刪除某個節點node 
void listDelNode(list *list, listNode *node)
{
    //若是該節點的前面存在節點 
    if (node->prev)
        node->prev->next = node->next;
    else
        list->head = node->next;
    //若是該節點的前面存在節點    if (node->next)
        node->next->prev = node->prev;
    else
        list->tail = node->prev;
    //釋放當前節點node的值
    if (list->free) list->free(node->value);
    //釋放內存 
    zfree(node);
     //len-1 
    list->len--;
}

 

總結

該篇主要講了Redis的list數據類型的底層實現雙向鏈表adlist,先從list的一些API使用,引出雙向鏈表數據結構,進而結合源碼對雙向鏈表進行描述,包括節點listNode和list的頭指針和尾指針,最後針對list的往表頭插入元素,往表尾插入元素,刪除,修改等方法進行源碼解析,使其對雙向鏈表有更清晰的認識。

若是以爲寫得還行,麻煩給個贊👍,您的承認纔是我寫做的動力!

若是以爲有說的不對的地方,歡迎評論指出。

好了,拜拜咯。

相關文章
相關標籤/搜索