什么是跳躍表
Skip list(跳表)是一種可以代替平衡樹的數據結構,默認是按照Key值升序的。Skip list讓已排序的數據分布在多層鏈表中,以0-1隨機數決定一個數據的向上攀升與否,通過“空間來換取時間”的一個算法,在每個節點中增加了向前的指針,在插入、刪除、查找時可以忽略一些不可能涉及到的結點,從而提高了效率。
在Java的API中已經有了實現:分別是
ConcurrentSkipListMap(在功能上對應HashTable、HashMap、TreeMap) ;
ConcurrentSkipListSet(在功能上對應HashSet)
跳躍表以有序的方式在層次化的鏈表中保存元素, 效率和AVL樹媲美 —— 查找、刪除、添加等操作都可以在O(LogN)時間下完成(最壞情況下時間復雜性O(n)。相比在一個有序數組或鏈表中進行插入/刪除操作的時間為O(n),最壞情況下為O(n)), 並且比起二叉搜索樹來說, 跳躍表的實現要簡單直觀得多。
結構圖如下:
可以看到跳躍表主要由以下部分構成:
- 表頭(head):負責維護跳躍表的節點指針。
- 跳躍表節點:保存着元素值,以及多個層。
- 層:保存着指向其他元素的指針。高層的指針越過的元素數量大於等於低層的指針,為了提高查找的效率,程序總是從高層先開始訪問,然后隨着元素值范圍的縮小,慢慢降低層次。
- 表尾:全部由 NULL 組成,表示跳躍表的末尾
原理
跳表的原理非常簡單,跳表其實就是一種可以進行二分查找的有序鏈表。跳表的數據結構模型如圖1:
可以看到,跳表在原有的有序鏈表上面增加了多級索引,通過索引來實現快速查找。首先在最高級索引上查找最后一個小於當前查找元素的位置,然后再跳到次高級索引繼續查找,直到跳到最底層為止,這時候以及十分接近要查找的元素的位置了(如果查找元素存在的話)。由於根據索引可以一次跳過多個元素,所以跳查找的查找速度也就變快了。
級的分配
在級基本的分配過程中,可以觀察到,在一般跳表結構中,i-1級鏈中的元素屬於i級鏈的概率為p。假設有一隨機數產生器值域為[0,RANDMAX]。則下一次所產生的隨機數≤CutOff=p*RANDMAX的概率為p。因此,若下一隨機數≤CutOff,則新元素應在1級鏈上。現在繼續確定新元素是否在2級鏈上,這由下一個隨機數來決定。若新的隨機數≤CutOff,則該元素也屬於2級鏈。重復這個過程,直到得到一隨機數>CutOff為止。故可以用下面的代碼為要插入的元素分配級。
intlevel = 0;
while(rand() <= CutOff) level++;
這種方法潛在的缺點是可能為某些元素分配特別大的級,從而導致一些元素的級遠遠超過log1/pN,其中N為字典中預期的最大數目。為避免這種情況,可以設定一個上限lev。在有N個元素的跳表中,級MaxLevel的最大值為
可以采用此值作為上限。
另一個缺點是即使采用上面所給出的上限,但還可能存在下面的情況,如在插入一個新元素前有三條鏈,而在插入之后就有了10條鏈。這時,新插入元素的為9級,盡管在前面插入中沒有出現3到8級的元素。也就是說,在此插入前並未插入3,4,⋯,8級元素。既然這些空級沒有直接的好處,那么可以把新元素的級調整為3。
構造一個跳躍表
一個跳表,應該具有以下特征:
- 一個跳表應該有幾個層(level)組成;
- 跳表的第一層包含所有的元素;
- 每一層都是一個有序的鏈表;
- 如果元素x出現在第i層,則所有比i小的層都包含x;
- 第i層的元素通過一個down指針指向下一層擁有相同值的元素;
- 在每一層中,-1和1兩個元素都出現(分別表示INT_MIN和INT_MAX);
- Top指針指向最高層的第一個元素。
以下面的鏈表為例演示如何構造一個跳躍表:
構造一個3層的跳躍表:
Skip List構造步驟:
1、給定一個有序的鏈表。
2、選擇連表中最大和最小的元素,然后從其他元素中按照一定算法(隨機)隨即選出一些元素,將這些元素組成有序鏈表。這個新的鏈表稱為一層,原鏈表稱為其下一層。
3、為剛選出的每個元素添加一個指針域,這個指針指向下一層中值同自己相等的元素。Top指針指向該層首元素
4、重復2、3步,直到不再能選擇出除最大最小元素以外的元素。
查詢
跳躍表只需要從最上層開始遍歷,由於每一層的鏈表都是有序的,因此當查找的“鍵”不存在於某一層中的時候,只需要在比查找目標的“鍵”要大的結點向下一次跳躍即可,重復操作,直至跳躍到最底層的鏈表。
1、先從頂層開始遍歷,與16進行對比小,進入下一層。
2、與4進行比較,比4大,當前結點置為4結點,與16進行比較,進入下一層。
3、 與8進行比較,沒有比8大,切換為當前結點4。
4、將節點4的下一個節點8和當前值進行比較,相同,取出。
插入
1、函數實現向跳躍表中插入一個“鍵”為 key,“值”為 value 的結點。由於我們進行插入操作時,插入結點的層數先要確定因此需要進行拋硬幣實驗確定占有層數。
2、由於新結點根據占有的層數不同,它的后繼可能有多個結點,因此需要用一個指針通過“鍵”進行試探,找到對應的“鍵”的所有后繼結點,在創建結點之后依次修改結點每一層的后繼,不要忘了給結點判空。在插入操作時,“鍵”可能已經存在,此時可以直接覆蓋“值”就行了,也可以讓用戶決定,可以適當發揮。
尋找節點的位置,獲取到插入節點的前一個節點,
3、與鏈表的操作執行相同的節點操作,地址替換。
模擬插入操作
首先我們需要用一個試探指針找到需要插入的結點的前驅,即用紅色的框框出來的結點。需要注意的是,由於當前的跳躍表只有 2 層,而新結點被 3 層占有,因此新結點在第 3 層的前驅就是頭結點。
接下來的操作與單鏈表相同,只是需要同時對每一層都操作。如圖所示,紅色箭頭表示結點之間需要切斷的邏輯聯系,藍色的箭頭表示插入操作新建立的聯系。
插入的最終效果應該是如圖所示的。
刪除
由於需要刪除的結點在每一層的前驅的后繼都會因刪除操作而改變,所以和插入操作相同,需要一個試探指針找到刪除結點在每一層的前驅的后繼,並拷貝。接着需要修改刪除結點在每一層的前驅的后繼為刪除結點在每一層的后繼,保證跳躍表的每一層的邏輯順序仍然是能夠正確描述。
1、根據刪除的值找到當前值在跳表中的前驅結點 head 4
2、判斷結點4的后驅結點的值是否為8,不是,直接跳出。當前值在跳表中不存在。
3、循環遍歷每一層,執行地址變更。當前結點可能在其他層不存在結點,因此在變更的時候要判斷是當前層是否存在該結點。
代碼
// 跳表中存儲的是正整數,並且存儲的數據是不重復的
public class SkipListTest {
//最大索引層數
private static int MAX_LEVEL =16;
//頭節點
private Node head;
//索引的層級數,默認為1
private int levelCount =1;
private Random random;
class Node{
//結點值
private int value;
//當前節點的所有后驅節點。1-maxlevel 層。
private Node[]nodes =new Node[MAX_LEVEL];
//當前節點的層數
private int maxLevel;
public Node(int value,int maxLevel) {
this.value = value;
this.maxLevel = maxLevel;
}
}
public Node get(int value){
//1、從最高層開始遍歷
Node cur =head;
for (int i =levelCount-1; i >=0 ; i--) {
//找到比該值小的那個結點
while (cur.nodes[i]!=null && cur.nodes[i].value < value){
cur = cur.nodes[i];
}
//開始尋找下一層,直到找到最后一層
}
if(cur.nodes[0]!=null&&cur.nodes[0].value == value){
return cur.nodes[0];
}
return null;
}
public void insert(int number){
//1、獲取要插入的索引層數量
int level = randomLevel();
//2、創建新節點
Node newNode =new Node(number,level);
//3、獲取每一層的前驅結點
Node update[] =new Node[level];
//遍歷索引層
Node c =head;
for (int i =level-1; i >=0 ; i--) {
while (c.nodes[i]!=null&&c.nodes[i].value){
c = c.nodes[i];
}
update[i] = c;
}
//4、更新每一層的索引結構
for (int i =0; i // 缺失
//當前結點的后驅結點
newNode.nodes[i] =update[i].nodes[i];
//當前結點的前驅
update[i].nodes[i] =newNode.nodes[i];
}
//5、更新索引層
if(levelCount // 缺失
levelCount =level;
}
}
public void delete(int value){
//1、獲取每一層比當前值小的前一個結點
Node[]update =new Node[levelCount];
Node p =head;
for(int i =levelCount -1; i >=0; --i){
while(p.nodes[i] !=null && p.nodes[i].value < value){
p = p.nodes[i];
}
update[i] = p;
}
//2、如果最后一層的結點的與當前值相同,進入變更指針操作。
if(p.nodes[0] !=null && p.nodes[0].value == value){
for(int i =levelCount -1; i >=0; --i){
//從最高層開始變更,如果值相等才進行變更
if(update[i].nodes[i] !=null &&update[i].nodes[i].value == value){
update[i].nodes[i] =update[i].nodes[i].nodes[i];
}
}
}
}
// 隨機函數
private int randomLevel(){
int level =1;
for(int i =1; i // 缺失
if(random.nextInt() %2 ==1){
level++;
}
}
return level;
}
}