redis 鏈表結構相對簡單一些 我們從各種鏈表的區別,到源碼,最後到一次redis命令的底層操作來介紹它。
一、鏈表
1.1 介紹
關於鏈表的介紹,自己理解後組織的語言或者各種博客的介紹總覺得差點意思,所以直接引用維基百科的鏈表介紹。
鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,但是並不會按線性的順序存儲數據,而是在每一個節點裏存到下一個節點的指針(Pointer)。由於不必須按順序存儲,鏈表在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查找一個節點或者訪問特定編號的節點則需要O(n)的時間,而順序表相應的時間複雜度分別是O(logn)和O(1)。
使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大。
在計算機科學中,鏈表作爲一種基礎的數據結構可以用來生成其它類型的數據結構。鏈表通常由一連串節點組成,每個節點包含任意的實例數據(data fields)和一或兩個用來指向上一個/或下一個節點的位置的鏈接("links")。鏈表最明顯的好處就是,常規數組排列關聯項目的方式可能不同於這些數據項目在記憶體或磁盤上順序,數據的訪問往往要在不同的排列順序中轉換。而鏈表是一種自我指示數據類型,因爲它包含指向另一個相同類型的數據的指針(鏈接)。鏈表允許插入和移除表上任意位置上的節點,但是不允許隨機存取。鏈表有很多種不同的類型:單向鏈表,雙向鏈表以及循環鏈表。
1.2 圖解
下面三圖很好解釋了介紹中三種鏈表的結構。文中GIF圖使用mac自帶keynote的導出gif功能製作。
1.2.1 單鏈表
鏈表中最簡單的一種是單向鏈表,它包含兩個域,一個信息域和一個指針域。這個鏈接指向列表中的下一個節點,而最後一個節點則指向一個空值。
一個單向鏈表的節點被分成兩個部分。第一個部分保存或者顯示關於節點的信息,第二個部分存儲下一個節點的地址。單向鏈表只可向一個方向遍歷。
鏈表最基本的結構是在每個節點保存數據和到下一個節點的地址,在最後一個節點保存一個特殊的結束標記,另外在一個固定的位置保存指向第一個節點的指針,有的時候也會同時儲存指向最後一個節點的指針。一般查找一個節點的時候需要從第一個節點開始每次訪問下一個節點,一直訪問到需要的位置。但是也可以提前把一個節點的位置另外保存起來,然後直接訪問。當然如果只是訪問數據就沒必要了,不如在鏈表上儲存指向實際數據的指針。這樣一般是爲了訪問鏈表中的下一個或者前一個(需要儲存反向的指針,見下面的雙向鏈表)節點。
相對於下面的雙向鏈表,這種普通的,每個節點只有一個指針的鏈表也叫單向鏈表,或者單鏈表,通常用在每次都只會按順序遍歷這個鏈表的時候(例如圖的鄰接表,通常都是按固定順序訪問的)。
1.2.2 雙向鏈表
每個節點有兩個連接:一個指向前一個節點,(當此“連接”爲第一個“連接”時,指向空值或者空列表);而另一個指向下一個節點,(當此“連接”爲最後一個“連接”時,指向空值或者空列表)。Redis中的List結構也是基於雙向鏈表實現的。
1.2.3 循環鏈表
在一個 循環鏈表中, 首節點和末節點被連接在一起。這種方式在單向和雙向鏈表中皆可實現。要轉換一個循環鏈表,你開始於任意一個節點然後沿着列表的任一方向直到返回開始的節點。再來看另一種方法,循環鏈表可以被視爲“無頭無尾”。這種列表很利於節約數據存儲緩存, 假定你在一個列表中有一個對象並且希望所有其他對象迭代在一個非特殊的排列下。圖就不搞了,偷個懶。
1.2.4 擴展
對於非線性的鏈表,可以參見相關的其他數據結構,例如樹、圖。另外有一種基於多個線性鏈表的數據結構:跳錶,插入、刪除和查找等基本操作的速度可以達到O(nlogn),和平衡樹一樣。
二、Redis中的List結構
2.1 爲什麼是雙向鏈表
Redis爲什麼選擇雙向鏈表來實現List結構?幾種鏈表的區別優缺點搜索引擎都詳細的告訴了我們,先從需求點說起吧!我們需要List結構做什麼?
-
堆棧,雙向列表的前後指針支持實現先進先出和先進後出,而單鏈表只可以先進先出。
-
.插入,單鏈表只可以尾部插入,雙鏈表可以頭尾插入。
-
.查找,單鏈表只可以通過遍歷查找,雙鏈表可以隨機從任意節點進行前後查找或者二分查找。
-
刪除,雖然兩種鏈表的刪除都是O(1),但是我們需要先查找到需要刪除的節點,同上。而且雙鏈表支持刪除尾部節點而不需要遍歷。
當然也是有缺點的,不過對於追求性能的Redis來說,空間換時間已經是家常便飯。
2.2 如何實現
我們已經知道爲什麼Redis選擇雙向鏈表,好在redis的鏈表相關源碼可讀性相當奈斯,無需過多的文字解釋下面我們從源碼來看一下它在Redis中的運用。
2.2.1 List結構體定義
源碼文件在 redis/src/adlist.h
2.2.2 方法及接口定義
源碼文件在 redis/src/adlist.h
2.3 具體實現
2.3.1 創建列表
2.3.2 釋放列表
2.3.3 添加頭尾
2.3.4 插入節點
2.3.5 刪除節點
2.3.6 迭代
2.3.7 尋找元素
2.3.8 通過index獲取元素
2.3.9 連接列表
2.3.10 鏈表在Redis中的運用
如發佈訂閱、慢查詢、監視器,日誌隊列,客戶端信息,從服務器列表,慢日誌記錄都是使用list結構來儲存。我們來搜索一下,很多地方都用到了奧
三、總結
Redis鏈表結構其主要特性如下:(摘自Redis設計與實現)
-
雙向:鏈表節點帶有前驅、後繼指針獲取某個節點的前驅、後繼節點的時間複雜度爲0(1)。
-
無環: 鏈表爲非循環鏈表表頭節點的前驅指針和表尾節點的後繼指針都指向NULL,對鏈表的訪問以NULL爲終點。
-
帶表頭指針和表尾指針:通過list結構中的head和tail指針,獲取表頭和表尾節點的時間複雜度都爲O(1)。
-
帶鏈表長度計數器:通過list結構的len屬性獲取節點數量的時間複雜度爲O(1)。
-
多態:鏈表節點使用void*指針保存節點的值,並且可以通過list結構的dup、free、match三個屬性爲節點值設置類型特定函數,所以鏈表可以用來保存各種不同類型的值。
分別使用數組、單鏈表和雙向鏈表實現列表對象的時間複雜度對照如下:
操作\時間複雜度 | 數組 | 單鏈表 | 雙向鏈表 |
---|---|---|---|
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) |