雙向連結串列都不懂,還說懂Redis?
目錄
redis原始碼分析系列文章
前言
API使用
lpush左側插入資料
rpush右側插入資料
刪除某個資料
修改某個資料
具體邏輯圖
雙向連結串列的定義
節點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的,所以需要記憶體。(這段話是不是聽的耳朵都起繭子啦