說明
數組的查詢速度很快O(1)
, 但是插入的速度比較慢O(N)
, 鏈表的插入速度快O(1)
, 但是鏈表的查詢速度比較慢O(N)
。 而跳表的平均查找和插入時間復雜度都是O(logN)
,空間復雜度O(N)
流程
題目中規定了節點值的數據范圍
[0,20000]
假設要加入的數據依次是:
[5, 43, 6, 1, 3, 8, 5]
現在要把這些數據組織成跳表
首先,設計跳表節點的數據結構,為了便於跳表節點可以上下左右尋找到對應節點,我們設計跳表節點的結構如下:
首先,我們需要先准備一個頭節點,這個頭節點是整個跳表的最小值,可以根據節點值的范圍來確定,因為節點值都大於等於0,所以,頭節點的值我准備一個-1即可。
准備好頭節點以后,我們開始放入第一個元素:5
放入元素之前,我們需要做兩件事:
第一件事: 找到目前小於等於5的最大的元素,假設是a,把5這個元素連到a的后面,如果a的下一個元素是c,那么連好后應該是
a -> 5 -> c
第二件事: 通過擲色子的方式(簡單理解成隨機生成一個整數值),隨機給5這個元素增加層數,並逐層連好。
上述兩點說的比較抽象,如果用圖示來表達的話,在現在跳表只有-1這個節點的情況下,
經歷了第一件事之后,跳表結構應該是這樣的:
經歷了第二件事以后(假設生成的隨機層數是3),跳表的結構會變成:
此時,第一個元素5,就安排好了,接下來安排第二個元素:43,假設43生成的隨機層數是:2,那么做完上述兩件事后,跳表的樣子會變成如下:
注:此時,跳表的最高層數是3,我們有一個規定:頭節點鏈表的長度一定和最大層數要保持一致
接下來處理6這個元素,假設6對應獲取到的層數是:4
4已經超過了目前的最大層數3,所以,要擴充頭節點鏈表,且6必須插在5和43之間,所以,6這個元素加入到跳表后,跳表的樣子如下:
同理,假設1,3,8對應的層數分別是2,4,1,那么經歷過上述處理后,跳表變成了這個樣子:
最后一個元素5,假設對應的層數是1,因為題目說需要支持重復值,那么把這個5放到原有跳表的下一個位置即可(如下圖中綠色的點),最后生成的跳表格式為:
如圖可知,最底層有所有的元素值,每次查詢的時候,是從頭節點的最高層開始,比如,我們要查詢8這個元素,那么整個跳表會歷經如下藍色的點:
這樣的查詢,每次高層走一步,其實底層會直接過濾掉一批節點,提高了查詢的效率。
如果要刪除一個節點,其實我們只需要在還有這個節點的每一層做單鏈表的刪除元素操作即可,比如,我們需要刪除a這個元素,我們需要做兩件事:
-
找到a這個節點最底層的位置,如果找不到,直接不需要操作刪除。
-
找到a這個節點在的底層位置,依次往上(node = node.up),刪掉每一個元素,直到不能往上為止。
假設,我們要刪除3這個元素。
我們首先找到了3這個元素,所在的最底層位置,如下圖:
然后刪掉這個元素,並來到3所在的第二層位置:
繼續刪除
最后,刪除所有的3以后,跳表如下:
細節
通過如上流程,我們需要實現跳表的初始化,增加元素,刪除元素,至少需要實現如下的一些方法:
隨機生成層數
如下代碼
// < 0.5 : 增加一層,
// >= 0.5 :不再增加
public boolean roll() {
return Math.random() < 0.5d;
}
獲取跳表中小於或某個元素的最右位置的元素
public Node getLessOrEqual(int target) {
// 獲取頭節點的最頂端的位置的那個元素
Node cur = heads.get(heads.size() - 1);
while (cur != null) {
// 如果比目標值小,就一直向右邊移動(在右邊不為空的情況下),如果右邊為空了,繼續向下移動,直到移動到最底端。
if (cur.right == null || cur.right.val > target) {
if (cur.val <= target) {
if (cur.down != null) {
cur = cur.down;
} else {
break;
}
}
} else {
cur = cur.right;
}
}
return cur;
}
有了這兩個功能函數,其他的實現,就是利用這兩個函數做一些雙向鏈表的節點操作,只不過這個雙向鏈表的節點還保存了其上一層位置的節點指針和下一層位置的節點指針。
完整代碼
class Skiplist {
private final ArrayList<Node> heads;
private final static double POSSIBLE = 0.5d;
public Skiplist() {
heads = new ArrayList<>();
heads.add(new Node(-1));
}
public static class Node {
private int val;
private Node up, down, left, right;
public Node(int val) {
this.val = val;
}
}
public Node getLessOrEqual(int target) {
Node cur = heads.get(heads.size() - 1);
while (cur != null) {
if (cur.right == null || cur.right.val > target) {
if (cur.val <= target) {
if (cur.down != null) {
cur = cur.down;
} else {
break;
}
}
} else {
cur = cur.right;
}
}
return cur;
}
public boolean search(int target) {
return getLessOrEqual(target).val == target;
}
public boolean roll() {
return Math.random() < POSSIBLE;
}
public void add(int num) {
// 如果節點存在則不增加
Node lessOrEqual = getLessOrEqual(num);
// 支持重復數據插入,所以這里不能直接判斷存在就退出
/* if (lessOrEqual.val == num) {
return;
}*/
// 到這里說明節點不存在,先建出節點
Node newNode = new Node(num);
// 無論如何,最底層都要連起來的
Node right = lessOrEqual.right;
lessOrEqual.right = newNode;
newNode.left = lessOrEqual;
if (right != null) {
// lessOrEqual不是最后一個節點
newNode.right = right;
right.left = newNode;
}
// 擲骰子隨機確定其他的層數
Node pre = lessOrEqual;
Node cur = newNode;
while (roll()) {
while (pre.left != null && pre.up == null) {
pre = pre.left;
}
if (pre.left == null) {
// 到達heads節點
final Node head = new Node(-1);
pre.up = head;
head.down = pre;
heads.add(head);
}
pre = pre.up;
Node toInsert = new Node(num);
cur.up = toInsert;
toInsert.down = cur;
cur = cur.up;
pre.right = toInsert;
cur.left = pre;
}
}
public boolean erase(int num) {
Node lessOrEqual = getLessOrEqual(num);
if (lessOrEqual.val != num) {
return false;
}
Node cur = lessOrEqual;
while (cur != null) {
Node pre = cur.left;
Node after = cur.right;
pre.right = after;
if (after != null) {
after.left = pre;
}
cur = cur.up;
}
return true;
}
}
跳表的應用
Redis底層使用了跳表,為什么選跳表而不是AVL樹或者SBT紅黑樹來實現,因為跳表方便序列化。