Redis系列(五):資料結構List雙向連結串列中基本操作操作命令和原始碼解析

IT技術派發表於2020-06-19

1.介紹

Redis中List是通過ListNode構造的雙向連結串列。

特點:

1.雙端:獲取某個結點的前驅和後繼結點都是O(1)

2.無環:表頭的prev指標和表尾的next指標都指向NULL,對連結串列的訪問都是以NULL為終點

3.帶表頭指標和表尾指標:獲取表頭和表尾的複雜度都是O(1)

4.帶連結串列長度計數器:len屬性記錄,獲取連結串列長度O(1)

5.多型:連結串列結點使用void*指標來儲存結點的值,並且可以通過連結串列結構的三個函式為結點值設定型別特定函式,所以連結串列可以儲存各種不同型別的值

雙向連結串列詳解:https://www.cnblogs.com/vic-tory/p/13140779.html

中文網:http://redis.cn/commands.html#list

2.操作命令

Lpush——先進後出,在列表頭部插入元素
Rpush——先進先出,在列表的尾部插入元素
Lrange——出棧,根據索引,獲取列表元素
Lpop——左邊出棧,獲取列表的第一個元素
Rpop——右邊出棧,獲取列表的最後一個元素
Lindex——根據索引,取出元素
Llen——連結串列長度,元素個數
Lrem——根據key,刪除n個value
Ltrim——根據索引,刪除指定元素
Rpoplpush——出棧,入棧
Lset——根據index,設定value
Linsert before——根據value,在之前插入值
Linsert after——根據value,在之後插入值

127.0.0.1:6379> lpush mylist "world"
(integer) 1
127.0.0.1:6379> rpush  mylist "!"
(integer) 2
127.0.0.1:6379> llen mylist
(integer) 2
127.0.0.1:6379> lpop mylist
"world"
127.0.0.1:6379> rpop mylist
"!"
127.0.0.1:6379> lpush mylist "Hello"
(integer) 1
127.0.0.1:6379> lpush mylist "world"
(integer) 2
127.0.0.1:6379> lset mylist 1 World
OK

 

3.原始碼解析

// listNode 雙端連結串列節點
typedef struct listNode {

    // 前置節點
    struct listNode *prev;

    // 後置節點
    struct listNode *next;

    // 節點的值
    void *value;

} listNode;

 

// list 雙端連結串列
typedef struct list { // 在c語言中,用結構體的方式來模擬物件是一種常見的手法

    // 表頭節點
    listNode *head;

    // 表尾節點
    listNode *tail;

    // 節點值複製函式
    void *(*dup)(void *ptr);

    // 節點值釋放函式
    void(*free)(void *ptr);

    // 節點值對比函式
    int(*match)(void *ptr, void *key);

    // 連結串列所包含的節點數量
    unsigned long len;

} list;

 

/* 作為巨集實現的函式 */
//獲取長度
#define listLength(l) ((l)->len)
//獲取頭節點
#define listFirst(l) ((l)->head)
//獲取尾結點
#define listLast(l) ((l)->tail)
//獲取前一個結點
#define listPrevNode(n) ((n)->prev)
//獲取後一個結點
#define listNextNode(n) ((n)->next)
//獲取結點的值 是一個void型別指標
#define listNodeValue(n) ((n)->value)

/* 下面3個函式主要用來設定list結構中3個函式指標,引數m為method的意思 */
#define listSetDupMethod(l,m) ((l)->dup = (m))
#define listSetFreeMethod(l,m) ((l)->free = (m))
#define listSetMatchMethod(l,m) ((l)->match = (m))

/* 下面3個函式主要用來獲取list結構的單個函式指標 */
#define listGetDupMethod(l) ((l)->dup)
#define listGetFree(l) ((l)->free)
#define listGetMatchMethod(l) ((l)->match)

3.API實現

listCreate函式:建立一個不包含任何結點的新連結串列

/*
 * listCreate 建立一個新的連結串列
 *
 * 建立成功返回連結串列,失敗返回 NULL 。
 *
 * T = O(1)
 */
list *listCreate(void)
{
    struct list *list;

    // 分配記憶體
    if ((list = zmalloc(sizeof(*list))) == NULL)
        return NULL;//記憶體分配失敗則返回NULL

    // 初始化屬性
    list->head = list->tail = NULL;//空連結串列
    list->len = 0;
    list->dup = NULL;
    list->free = NULL;
    list->match = NULL;

    return list;
}

listAddNodeHead函式:將一個包含給定值的新結點新增到給定連結串列的表頭

/*
 * listAddNodeHead 將一個包含有給定值指標 value 的新節點新增到連結串列的表頭
 *
 * 如果為新節點分配記憶體出錯,那麼不執行任何動作,僅返回 NULL
 *
 * 如果執行成功,返回傳入的連結串列指標
 *
 * T = O(1)
 */
list *listAddNodeHead(list *list, void *value)
{
    listNode *node;

    // 為節點分配記憶體
    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;

    // 儲存值指標
    node->value = value;

    // 新增節點到空連結串列
    if (list->len == 0) {
        list->head = list->tail = node;
        //該結點的前驅和後繼都為NULL
        node->prev = node->next = NULL;
    }
    else { // 新增節點到非空連結串列
        node->prev = NULL;
        node->next = list->head;
        list->head->prev = node;
        list->head = node;
    }

    // 更新連結串列節點數
    list->len++;

    return list;
}

listAddNodeTail函式:將一個包含給定值的新結點插入到給定連結串列的表尾

/*
 * listAddNodeTail 將一個包含有給定值指標 value 的新節點新增到連結串列的表尾
 *
 * 如果為新節點分配記憶體出錯,那麼不執行任何動作,僅返回 NULL
 *
 * 如果執行成功,返回傳入的連結串列指標
 *
 * T = O(1)
 */
list *listAddNodeTail(list *list, void *value)
{
    listNode *node;

    // 為新節點分配記憶體
    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;

    // 儲存值指標
    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;
    }

    // 更新連結串列節點數
    list->len++;

    return list;
}

listInsertNode函式:將一個給定值的新結點插入到給定結點之前或者之後

/*
 * listInsertNode 建立一個包含值 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;
}

listDelNode函式:從指定的list中刪除給定的結點

/*
 * listDelNode 從連結串列 list 中刪除給定節點 node
 *
 * 對節點私有值(private value of the node)的釋放工作由呼叫者進行。該函式一定會成功.
 *
 * T = O(1)
 */
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;

    // 釋放值
    if (list->free) list->free(node->value);

    // 釋放節點
    zfree(node);

    // 連結串列數減一
    list->len--;
}

listRelease函式:釋放給定連結串列以及連結串列中所有結點

 

/*
 * listRelease 釋放整個連結串列,以及連結串列中所有節點, 這個函式不可能會失敗.
 *
 * T = O(N)
 */
void listRelease(list *list)
{
    unsigned long len;
    listNode *current, *next;

    // 指向頭指標
    current = list->head;
    // 遍歷整個連結串列
    len = list->len;
    while (len--) {
        next = current->next;

        // 如果有設定值釋放函式,那麼呼叫它
        if (list->free) list->free(current->value);

        // 釋放節點結構
        zfree(current);

        current = next;
    }

    // 釋放連結串列結構
    zfree(list);
}

 

該函式不僅釋放了表結點的記憶體還釋放了表結構的記憶體

 listGetIterator函式:為給定連結串列建立一個迭代器

在講這個函式之前,我們應該先看看連結串列迭代器的結構:

// listIter 雙端連結串列迭代器
typedef struct listIter {

    // 當前迭代到的節點
    listNode *next;

    // 迭代的方向
    int direction;

} listIter;

迭起器只有兩個重要的屬性:當前迭代到的結點,迭代的方向

下面再看看連結串列的迭代器建立函式

/*
 * listGetIterator 為給定連結串列建立一個迭代器,
 * 之後每次對這個迭代器呼叫 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;
}

listReleaseIterator函式:釋放指定的迭代器

/*
 * listReleaseIterator 釋放迭代器
 *
 * T = O(1)
 */
void listReleaseIterator(listIter *iter) {
    zfree(iter);
}

listRewind函式和listRewindTail函式:迭代器重新指向表頭或者表尾的函式

 

/*
 * 將迭代器的方向設定為 AL_START_HEAD,
 * 並將迭代指標重新指向表頭節點。
 *
 * T = O(1)
 */
void listRewind(list *list, listIter *li) {
    li->next = list->head;
    li->direction = AL_START_HEAD;
}

/*
 * 將迭代器的方向設定為 AL_START_TAIL,
 * 並將迭代指標重新指向表尾節點。
 *
 * T = O(1)
 */
void listRewindTail(list *list, listIter *li) {
    li->next = list->tail;
    li->direction = AL_START_TAIL;
}

listNext函式:返回當前迭代器指向的結點

 

/*
 * 返回迭代器當前所指向的節點。
 *
 * 刪除當前節點是允許的,但不能修改連結串列裡的其他節點。
 *
 * 函式要麼返回一個節點,要麼返回 NULL,常見的用法是:
 *
 * iter = listGetIterator(list,<direction>);
 * while ((node = listNext(iter)) != NULL) {
 *     doSomethingWith(listNodeValue(node));
 * }
 *
 * T = O(1)
 */
listNode *listNext(listIter *iter)
{
    listNode *current = iter->next;

    if (current != NULL) {
        // 根據方向選擇下一個節點
        if (iter->direction == AL_START_HEAD)
            // 儲存下一個節點,防止當前節點被刪除而造成指標丟失
            iter->next = current->next;
        else
            // 儲存下一個節點,防止當前節點被刪除而造成指標丟失
            iter->next = current->prev;
    }

    return current;
}

 

 

 

該函式保持了當前結點的下一個結點,避免了當前結點被刪除而迭代器無法繼續迭代的尷尬情況

 listDup函式:複製整個連結串列,返回副本

 

/*
 * 複製整個連結串列。
 *
 * 複製成功返回輸入連結串列的副本,
 * 如果因為記憶體不足而造成複製失敗,返回 NULL 。
 *
 * 如果連結串列有設定值複製函式 dup ,那麼對值的複製將使用複製函式進行,
 * 否則,新節點將和舊節點共享同一個指標。
 *
 * 無論複製是成功還是失敗,輸入節點都不會修改。
 *
 * T = O(N)
 */
list *listDup(list *orig)
{
    list *copy;//連結串列副本
    listIter *iter;//連結串列迭代器
    listNode *node;//連結串列結點

    // 建立新的空連結串列
    if ((copy = listCreate()) == NULL)
        return NULL;//建立空的連結串列失敗則返回NULL

    // 設定副本連結串列的節點值處理函式
    copy->dup = orig->dup;
    copy->free = orig->free;
    copy->match = orig->match;

    //獲取輸入連結串列的迭代器
    iter = listGetIterator(orig, AL_START_HEAD);
    
    //遍歷整個輸入連結串列進行復制
    while ((node = listNext(iter)) != NULL) {
        
        //副本結點值
        void *value;

        // 存在複製函式
        if (copy->dup) {
            
            //呼叫複製函式複製
            value = copy->dup(node->value);
         
            //複製結果為空,說明覆制失敗
            if (value == NULL) {
                
                //複製失敗則釋放副本連結串列和迭代器,避免記憶體洩漏
                listRelease(copy);
                listReleaseIterator(iter);
            
                return NULL;
            }
        }
        //不存在複製函式 則直接指標指向
        else
            value = node->value;

        // 將節點新增到副本連結串列 
        if (listAddNodeTail(copy, value) == NULL) {
                
            //如果不能成功新增,則釋放副本連結串列和迭代器,避免記憶體洩漏
            listRelease(copy);
            listReleaseIterator(iter);
        
            return NULL;
        }
    }

    // 釋放迭代器
    listReleaseIterator(iter);

    // 返回副本
    return copy;
}

 

如果複製失敗則要注意釋放副本連結串列和迭代器,避免記憶體洩漏

 listSearchKey函式:查詢list中值和key匹配的結點

 
/*
 * 查詢連結串列 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;
}

listIndex函式:返回連結串列在給定索引上的值

 

/*
 * 返回連結串列在給定索引上的值。
 *
 * 索引以 0 為起始,也可以是負數, -1 表示連結串列最後一個節點,諸如此類。
 *
 * 如果索引超出範圍(out of range),返回 NULL 。
 *
 * T = O(N)
 */
listNode *listIndex(list *list, long index) {
    
    listNode *n;//連結串列結點

    
    /* n不用設定成NULL的原因:
    如果索引超出範圍,
    那肯定是找到表頭或者表尾沒有找到,
    表頭的前驅和表尾的後繼都是NULL,
    所以這裡n不用設定為NULL,直接設定也可以*/
    
    // 如果索引為負數,從表尾開始查詢
    if (index < 0) {
        
        //變成正數,方便索引
        index = (-index) - 1;
    
        //從尾部開始找
        n = list->tail;
        
        //尋找 因為從尾部開始找,所以是前驅
        while (index-- && n) n = n->prev;
        
    }
    
    // 如果索引為正數,從表頭開始查詢
    else {
        
        //從頭部開始找
        n = list->head;
    
        //尋找 因為從頭部開始找,所以是後繼
        while (index-- && n) n = n->next;
    }

    return n;
}

listRotate函式:取出連結串列的表尾結點放到表頭,成為新的表頭結點

/*
 * 取出連結串列的表尾節點,並將它移動到表頭,成為新的表頭節點。
 *
 * T = O(1)
 */
void listRotate(list *list) {
    
    //表尾結點
    listNode *tail = list->tail;

    //如果連結串列中只有一個元素,那麼表頭就是表尾,可以直接返回
    if (listLength(list) <= 1) return;

    // 重新設定表尾節點
    list->tail = tail->prev;
    list->tail->next = NULL;

    // 插入到表頭
    list->head->prev = tail;
    tail->prev = NULL;
    tail->next = list->head;
    list->head = tail;
}

相關文章