圖解Redis之數據結構篇——鏈表

文章導航-readme

前言

    Redis鏈表爲雙向無環鏈表!html

    圖解Redis之數據結構篇——簡單動態字符串SDS提到Redis使用了簡單動態字符串,鏈表,字典(散列表),跳躍表,整數集合,壓縮列表這些數據結構來操做內存,而且簡單介紹了Redis簡單動態字符串。本篇文章咱們繼續來分析鏈表。面試

    鏈表是一種很是常見的數據結構,在Redis中使用很是普遍,列表對象的底層實現之一就是鏈表。其它如慢查詢,發佈訂閱,監視器等功能也用到了鏈表。redis

Redis對象結構

系列文章

1、複習鏈表

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而是指向頭結點。雙循環鏈表中其頭節點的前驅指針指向尾節點,尾節點的後繼指針指向頭結點。循環鏈表的優點在於鏈尾到鏈頭,鏈頭到鏈尾比較方便適合處理的數據具備環型結構特色。函數

2、Redis鏈表

2.1 雙向無環鏈表

    Redis鏈表使用雙向無環鏈表。大數據

Redis雙向無環鏈表

如圖所示,Redis使用一個listNode結構來表示。

typedef struct listNode
{ 
    // 前置節點 
    struct listNode *prev; 
    // 後置節點 
    struct listNode *next; 
    // 節點的值 
    void *value; 
} listNode;

2.2 list結構

    同時Redis爲了方便的操做鏈表,提供了一個list結構來持有鏈表。以下圖所示

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官方文檔》

相關文章
相關標籤/搜索