文章導航-readme
前言
Redis鏈表為雙向無環鏈表!
圖解Redis之數據結構篇——簡單動態字符串SDS提到Redis使用了簡單動態字符串,鏈表,字典(散列表),跳躍表,整數集合,壓縮列表這些數據結構來操作內存,並且簡單介紹了Redis簡單動態字符串。本篇文章我們繼續來分析鏈表。
鏈表是一種非常常見的數據結構,在Redis中使用非常廣泛,列表對象的底層實現之一就是鏈表。其它如慢查詢,發布訂閱,監視器等功能也用到了鏈表。
系列文章
一、復習鏈表
1.1 數組與鏈表
數組需要一塊連續的內存來存儲,這個特性有利也有弊。好處是其支持根據索引下標"隨機訪問"(時間復雜度為O(1)),但是其插入與刪除操作為了保證在內存中的連續性將會變得非常低效(時間復雜度為O(N)),並且其一經聲明就要占用整塊連續內存空間,如果聲明過大,系統可能內存不足,聲明過小又可能導致不夠用,而當數組的空間不足的時候需要對其進行擴容(申請一個更大的空間,將原數組拷貝過去)。
而鏈表恰恰相反,其不需要一塊連續的內存空間,其通過"指針"將一組零散的內存連接起來使用。其優點在於本身沒有大小限制,天然支持擴容,插入刪除操作高效(時間復雜度為O(1)),但缺點是隨機訪問低效(時間復雜度為O(N))。並且由於需要額外的空間存儲指針。
鏈表的實現方式有很多種,常見的主要有三個,單向鏈表、雙向鏈表、循環鏈表。
1.2 單鏈表
單鏈表中每個節點除了包含數據之外還包含一個指針,叫后繼指針,因此需要額外的空間來存儲后繼節點的地址。有兩個特殊的節點,頭結點和尾節點,其中頭節點用來記錄鏈表的基地址,有了它就可以遍歷整個鏈表,尾節點的后繼指針不是指向下一個節點,而是指向一個空地址NULL表示這是鏈表上最后一個節點。與數組一樣,單鏈表也支持數據的查找、插入和刪除操作,其中插入和刪除操作只需要考慮相鄰節點指針的變化,因此為常數級時間復雜度O(1)。要想隨機訪問第 k 個元素,就沒有數組那么高效了。因為鏈表中的數據並非連續存儲的,所以無法像數組那樣,根據首地址和下標,通過尋址公式就能直接計算出對應的內存地址,而是需要根據指針一個結點一個結點地依次遍歷,直到找到相應的結點,因此時間復雜度為O(N)。
1.3 雙向鏈表
雙向鏈表和單鏈表不同的是多了一個前驅指針,雙向鏈表需要額外的兩個空間來存儲后繼結點和前驅結點的地址。因此存儲同樣多的數據,雙向鏈表占用比單鏈表更多的空間。但其優點在於支持雙向遍歷,體現在以下兩個方面。
- 在有序鏈表中查找某個元素,單鏈表由於只有后繼指針,因此只能從前往后遍歷查找時間復雜度為O(N),而雙向鏈表可以雙向遍歷。
- 刪除給定指針指向的結點。假設已經找到要刪除的節點,要刪除就必須知道其前驅節點和后繼節點,單鏈表想要知道其前驅節點只能從頭開始遍歷,時間復雜度為0(n),而雙向鏈表由於保存了其前驅節點的地址,因此時間復雜度為0(1)。
1.4 循環鏈表
顧名思義。循環鏈表與單、雙鏈表不同的是其呈環狀,單循環鏈表中其尾節點並非指向NULL而是指向頭結點。雙循環鏈表中其頭節點的前驅指針指向尾節點,尾節點的后繼指針指向頭結點。循環鏈表的優勢在於鏈尾到鏈頭,鏈頭到鏈尾比較方便適合處理的數據具有環型結構特點。
二、Redis鏈表
2.1 雙向無環鏈表
Redis鏈表使用雙向無環鏈表。
如圖所示,Redis使用一個listNode結構來表示。
typedef struct listNode
{
// 前置節點
struct listNode *prev;
// 后置節點
struct listNode *next;
// 節點的值
void *value;
} listNode;
2.2 list結構
同時Redis為了方便的操作鏈表,提供了一個list結構來持有鏈表。如下圖所示
typedef struct list{
//表頭節點
listNode *head;
//表尾節點
listNode *tail;
//鏈表所包含的節點數量
unsigned long len;
//節點值復制函數
void *(*dup)(void *ptr);
//節點值釋放函數
void *(*free)(void *ptr);
//節點值對比函數
int (*match)(void *ptr,void *key);
}list;
Redis鏈表結構其主要特性如下:
- 雙向:鏈表節點帶有前驅、后繼指針獲取某個節點的前驅、后繼節點的時間復雜度為0(1)。
- 無環: 鏈表為非循環鏈表表頭節點的前驅指針和表尾節點的后繼指針都指向NULL,對鏈表的訪問以NULL為終點。
- 帶表頭指針和表尾指針:通過list結構中的head和tail指針,獲取表頭和表尾節點的時間復雜度都為O(1)。
- 帶鏈表長度計數器:通過list結構的len屬性獲取節點數量的時間復雜度為O(1)。
- 多態:鏈表節點使用void*指針保存節點的值,並且可以通過list結構的dup、free、match三個屬性為節點值設置類型特定函數,所以鏈表可以用來保存各種不同類型的值。
2.3 雙向無環鏈表在Redis中的使用
鏈表在Redis中的應用非常廣泛,列表對象的底層實現之一就是鏈表。此外如發布訂閱、慢查詢、監視器等功能也用到了鏈表。我們現在簡單想一想Redis為什么要使用雙向無環鏈表這種數據結構,而不是使用數組、單向鏈表等。既然列表對象的底層實現之一是鏈表,那么我們通過一個表格來分析列表對象的常用操作命令。如果分別使用數組、單鏈表和雙向鏈表實現列表對象的時間復雜度對照如下:
操作\時間復雜度 | 數組 | 單鏈表 | 雙向鏈表 |
---|---|---|---|
rpush(從右邊添加元素) | O(1) | O(1) | O(1) |
lpush(從左邊添加元素) | 0(N) | O(1) | O(1) |
lpop (從右邊刪除元素) | O(1) | O(1) | O(1) |
rpop (從左邊刪除元素) | O(N) | O(1) | O(1) |
lindex(獲取指定索引下標的元素) | O(1) | O(N) | O(N) |
len (獲取長度) | O(N) | O(N) | O(1) |
linsert(向某個元素前或后插入元素) | O(N) | O(N) | O(1) |
lrem (刪除指定元素) | O(N) | O(N) | O(N) |
lset (修改指定索引下標元素) | O(N) | O(N) | O(N) |
我們可以看到在列表對象常用的操作中雙向鏈表的優勢所在。但雙向鏈表因為使用兩個額外的空間存儲前驅和后繼指針,因此在數據量較小的情況下會造成空間上的浪費(因為數據量小的時候速度上的差別不大,但空間上的差別很大)。這是一個時間換空間還是空間換時間的思想問題,Redis在列表對象中小數據量的時候使用壓縮列表作為底層實現,而大數據量的時候才會使用雙向無環鏈表。(關於列表對象后續會有文章繼續介紹可訪問我的個人博客持續關注www.kxamm.com)
小結
鏈表作為一種非常常用的數據結構,內置在許多編程語言里面,更是找工作過程中經常問的面試題之一。本篇文章簡單復習了鏈表這種數據結構常見的幾種形式,並且簡單分析了Redis中鏈表的使用。下篇文章將繼續分享Redis中用到的數據結構Hash。敬請關注!
參考
《Redis設計與實現》
《Redis開發與運維》
《Redis官方文檔》