跳表的原理
跳表也叫跳躍表,是一種動態的數據結構。如果我們需要在有序鏈表中進行查找某個值,需要遍歷整個鏈表,二分查找對鏈表不支持,二分查找的底層要求為數組,遍歷整個鏈表的時間復雜度為O(n)。我們可以把鏈表改造成B樹、紅黑樹、AVL樹等數據結構來提升查詢效率,但是B樹、紅黑樹、AVL樹這些數據結構實現起來非常復雜,里面的細節也比較多。跳表就是為了提升有序鏈表的查詢速度產生的一種動態數據結構,跳表相對B樹、紅黑樹、AVL樹這些數據結構實現起來比較簡單,但時間復雜度與B樹、紅黑樹、AVL樹這些數據結構不相上下,時間復雜度能夠達到O(logn)。
如何理解跳表?

要理解跳表,我們先從有序鏈表開始,對於上圖中的有序鏈表,我們要查找值為150的節點,我們需要遍歷到鏈表的最后一個元素才能找到,用大O表示時間復雜度就為O(n)。時間復雜度比較高,有沒有優化空間呢?答案是肯定的,我們采取空間換時間的概念,對鏈表抽取出索引層,比如每兩個元素抽取一個元素構成新的有序鏈表,這樣索引層的有序鏈表相對底層的鏈表來說長度更短、間距更大,改造之后的鏈表入下圖所示:

我們在新的鏈表中查詢150,先從最上層Level3開始查找,遍歷Level3有序鏈表,遍歷兩次就查詢到了150這個節點。相對於初始鏈表來說,改造后的鏈表對查詢效率有了不少的提升。
改造后的鏈表就是跳表,跳表采用的是以空間換時間的概念,將有序鏈表隨機抽出好幾層有序鏈表,查找的時候先從最上層開始,然后一直下沉到最底層鏈表。
現在我們對跳表有了一定的認識,總結一下跳表的性質:
- 由很多層結構組成
- 每一層都是一個有序的鏈表
- 最底層(Level 1)的鏈表包含所有元素
- 如果一個元素出現在 Level i 的鏈表中,則它在 Level i 之下的鏈表也都會出現。
跳表的實現
跳表一般使用單鏈表來實現,這樣比較節約空間。我使用雙向鏈表來實現跳表,因為雙向鏈表相對單向鏈表來說比較容易理解跳表的實現。
鏈表定義
// 頭節點
private final static byte HEAD_NODE = (byte) -1;
// 數據節點
private final static byte DATA_NODE = (byte) 0;
// 尾節點
private final static byte TAIL_NODE = (byte) -1;
private static class Node{
private Integer value;
// 上下左右節點
private Node up,down,left,right;
private byte bit;
public Node(Integer value,byte bit){
this.value = value;
this.bit = bit;
}
public Node(Integer value){
this(value,DATA_NODE);
}
}
跳表的搜索
跳表的搜索從最上層開始查詢,最開始的節點p指向最頂層的head節點,如果p.right.value小於等於帶查詢元素,則p指向p.right,否則指向p.down。直到p.down為空,返回該節點。
跳表查找示意圖:

1)p=head,head.right.value=15,15<30,繼續往右邊查找,p=head.right
2)p.right.value=150,150 > 30,往下一層查找,p=p.down
3)p.right.value=30,30=30,繼續往右邊查找,p=p.right
4)p.right.value=150,150 > 30,往下一層查找,p=p.down
5)p.right.value=50,50 > 30,由於p.down為空,所以就返回該節點
代碼實現
/**
* 通用查找
* 查找出最后一個小於該元素或者等於元素的節點
* @param element 待查找的數
* @return
*/
private Node find(Integer element){
Node current = head;
for (;;){
while (current.right.bit !=TAIL_NODE && current.right.value <= element){
current = current.right;
}
if (current.down !=null){// 找到最底層節點
current = current.down;
}else{
break;
}
}
return current;// current.value <= element <current.right.value
}
/**
* 獲取某個元素
* @param element
* @return
*/
public Integer get(Integer element){
Node node = this.find(element);
return (node.value==element)?node.value:null;
}
跳表的插入
理解了跳表的查找后,在來看跳表的插入就相對來說比較簡單,我們先找出最后一個小於等於插入值的節點,我們將值插入到該節點的右邊。除了最底層鏈表上的插入外,每個新插入的節點都會有一個隨機的層高,我們還需要維護層高之間的節點關系,因為新節點的左邊第一個元素不一定有層高,所以我們要往左邊查找到第一個node.up不為空的節點,維護節點之間的關系,然后將節點作為新的節點,繼續往上加層。
我們以值55,層高為2的新節點為例,演示索引建立的過程。

1)我們先找出最后一個小於或者等於插入值的節點,即圖中的node節點,然后將新節點插入到node的右邊,即newnode節點。
2)node=50時,node.up為空,所以node = node.left
3)node=30時,node.up不為空,此時node=node.up
4)此時node已經移到level2的30節點上,在node的右邊建立indexNode節點,作為新插入值的lebel2層的索引
5)將indexNode節點作為新的newnode節點繼續向上添加索引,由於我們新插入節點的層高為2,所以將結束循環,新增元素完畢。
代碼實現
/**
* 添加元素
* @param element
*/
public void add(Integer element){
//查找出前一個節點
Node node = this.find(element);
// 新建節點
Node newNode = new Node(element);
// 新節點與前后兩節點建立關系
newNode.left = node;
newNode.right = node.right;
node.right.left = newNode;
node.right = newNode;
int currentLevel = 1;
// 建立索引層,隨機建立層高
while (random.nextDouble() < 0.5d){
// 索引大於當前索引層
if (currentLevel >= level){
level++;
// 最頂層索引的頭指針
Node topHead = new Node(null,HEAD_NODE);
// 最頂層索引的尾指針
Node topTail = new Node(null,TAIL_NODE);
topHead.right = topTail;
topHead.down = head;
head.up = topHead;
topTail.left = topHead;
topTail.down = tail;
tail.up = topTail;
head = topHead;
tail = topTail;
}
// 一直往左邊找到索引層
while (node !=null && node.up == null){
node = node.left;
}
node = node.up;
// 新建索引節點,與當前左邊節點建立關系
Node indexNode = new Node(element);
indexNode.left = node;
indexNode.right = node.right;
indexNode.down = newNode;
node.right.left = indexNode;
node.right = indexNode;
newNode.up = indexNode;
// 將索引節點作為新的節點,繼續往上建立索引(如果有索引的話)
newNode = indexNode;
currentLevel++;
// 當前層高大於最高層高時,跳出循環
if (currentLevel > MAX_LEVEL) break;
}
size ++;
}
跳表的刪除
跳表的刪除就比較簡單,找到該節點,從最底層開始刪除,直到node.up為空。
代碼實現
/**
* 刪除跳表
* @param element 待刪除的元素
*/
public void delete(Integer element){
Node node = this.find(element);
// 沒有找到該元素,直接返回
if (node.value != element) return;
// 刪除元素,將元素的左右鏈表建立關系
node.left.right = node.right;
node.right.left = node.left;
// 判斷是否有索引層,
while (node.up !=null){
node.up.left.right = node.up.right;
node.up.right.left = node.up.left;
node = node.up;
}
}
跳表的時間復雜度
對於有序鏈表來說,時間復雜度為O(n),我們將鏈表改造成跳表之后,時間復雜度為多少呢?首先來分析對於有n個節點的鏈表,需要建立多少級索引?如果我們每兩個節點會提取一個節點作為一個索引節點,那么第一級索引節點的個數為n/2,第二級索引節點的個數為n/4,依此類推,則第K級的索引節點的個數為n/(2^k)。
假設索引有h級,且第h級的索引節點個數為2,如下圖所示。則我們可以得出n/(2^h)=2,這樣可以得到h=logn-1(這里的log是指以2為底),加上鏈表本身的一層,則整個跳表的高度為logn。我們在跳表中查詢某個數據時,如果每一層都需要遍歷m個節點,那么在跳表中查詢某個數的時間復雜度為O(m*log(n))。
跳表的空間復雜度
比起單純的單鏈表,跳表需要存儲多級索引,肯定要消耗更多的存儲空間。我們來看看跳表的空間復雜度,假設原始鏈表大小為 n,每兩個元素直接提取一個索引,那第一級索引大約有 n/2 個結點,第二級索引大約有 n/4 個結點,以此類推,每上升一級就減少一半,直到剩下 2 個結點。如果我們把每層索引的結點數寫出來,就是一個等比數列。這幾級索引的結點總和就是 n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空間復雜度是 O(n)。也就是說,如果將包含 n 個結點的單鏈表構造成跳表,我們需要額外再用接近 n 個結點的存儲空間。
以上就是跳表相關的知識,我使用雙向鏈表來實現跳表,相對來說能夠更好的理解跳表的思想,在使用是用單鏈表實現就可以了,單鏈表比雙鏈表節約空間。
示例代碼:GitHub
如果您發現文中錯誤,還請多多指教。歡迎關注個人公眾號,一起交流學習。
最后
打個小廣告,金九銀十跳槽季,平頭哥給大家整理了一份較全面的 Java 學習資料,歡迎掃碼關注微信公眾號:「平頭哥的技術博文」領取,祝各位升職加薪。

