一、什么是跳表?
為一個值有序的鏈表建立多級索引,比如每2個節點提取一個節點到上一級,我們把抽出來的那一級叫做索引或索引層。如下圖所示,其中down表示down指針,指向下一級節點。以此類推,對於節點數為n的鏈表,大約可以建立log2n-1級索引。像這種為鏈表建立多級索引的數據結構就稱為跳表。
二、跳表的時間復雜度?
1.計算跳表的高度
如果鏈表有n個節點,每2個節點抽取抽出一個節點作為上一級索引的節點,那第1級索引的節點個數大約是n/2,第2級索引的節點個數大約是n/4,依次類推,第k級索引的節點個數就是n/(2k)**。假設索引有h級別,最高級的索引有2個節點,則有**n/(2h)=2,得出h=log2n-1,包含原始鏈表這一層,整個跳表的高度就是log2n。
2.計算跳表的時間復雜度
假設我們在跳表中查詢某個數據的時候,如果每一層都遍歷m個節點,那在跳表中查詢一個數據的時間復雜度就是O(m*logn)。那這個m是多少呢?如下圖所示,假設我們要查找的數據是x,在第k級索引中,我們遍歷到y節點之后,發現x大於y,小於后面的節點z,所以我們通過y的down指針,從第k級下降到第k-1級索引。在第k-1級索引中,y和z之間只有3個節點(包含y和z),所以,我們在k-1級索引中最多只需要遍歷3個節點,以此類推,每一級索引都最多只需要遍歷3個節點。所以m=3。因此在跳表中查詢某個數據的時間復雜度就是O(logn)。
三、跳表的空間復雜度及如何優化?
1.計算索引的節點總數
如果鏈表有n個節點,每2個節點抽取抽出一個節點作為上一級索引的節點,那每一級索引的節點數分別為:n/2,n/4,n/8,…,8,4,2,等比數列求和n-1,所以跳表的空間復雜度為O(n)。
2.如何優化時間復雜度
如果鏈表有n個節點,每3或5個節點抽取抽出一個節點作為上一級索引的節點,那每一級索引的節點數分別為(以3為例):n/3,n/9,n/27,…,27,9,3,1,等比數列求和n/2,所以跳表的空間復雜度為O(n),和每2個節點抽取一次相比,時間復雜度要低不少呢。
四、高效的動態插入和刪除?
跳表本質上就是鏈表,所以僅插作,插入和刪除操時間復雜度就為O(1),但在實際情況中,要插入或刪除某個節點,需要先查找到指定位置,而這個查找操作比較費時,但在跳表中這個查找操作的時間復雜度是O(logn),所以,跳表的插入和刪除操作的是時間復雜度也是O(logn)。
五、跳表索引動態更新?
當往跳表中插入數據的時候,可以選擇同時將這個數據插入到部分索引層中,那么如何選擇這個索引層呢?可以通過隨機函數來決定將這個節點插入到哪幾級索引中,比如隨機函數生成了值K,那就可以把這個節點添加到第1級到第K級索引中。
Redis中的有序集合是通過跳表來實現的,嚴格點講,其實還用到了散列表。不過散列表我們后面才會講到,所以我們現在暫且忽略這部分。如果你去查看 Redis的開發手冊,就會發現,**Redis **的有序集合支持的核心操作主要有下面這幾個:
-
插入一個數據;
-
刪除一個數據;
-
查找一個數據;
-
按照區間查找數據(比如查找值在[100, 356]之間的數據);
-
迭代輸出有序序列。
其中,插入、刪除、查找以及迭代輸出有序序列這幾個操作,紅黑樹也可以完成,時間復雜度跟跳表是一樣的。但是,按照區間來查找數據這個操作,紅黑樹的效率沒有跳表高。
對於按照區間查找數據這個操作,跳表可以做到O(logn)的時間復雜度定位區間的起點,然后在原始鏈表中順序往后遍歷就可以了。這樣做非常高效。
當然,Redis之所以用跳表來實現有序集合,還有其他原因,比如,跳表更容易代碼實現。雖然跳表的實現也不簡單,但比起紅黑樹來說還是好懂、好寫多了,而簡單就意味着可讀性好,不容易出錯。還有,跳表更加靈活,它可以通過改變索引構建策略,有效平衡執行效率和內存消耗。不過,跳表也不能完全替代紅黑樹。因為紅黑樹比跳表的出現要早一些,很多編程語言中的Map類型都是通過紅黑樹來實現的。我們做業務開發的時候,直接拿來用就可以了,不用費勁自己去實現一個紅黑樹,但是跳表並沒有一個現成的實現,所以在開發中,如果你想使用跳表,必須要自己實現。