關注公眾號,一起交流,微信搜一搜: 潛行前行
什麼是跳躍連結串列
開發時經常使用的平衡資料結構有B數、紅黑數,AVL數。但是如果讓你實現其中一種,很難,實現起來費時間。而跳躍連結串列一種基於連結串列陣列實現的快速查詢資料結構,目前開源軟體 Redis 和 LevelDB 都有用到它。它的效率和紅黑樹以及 AVL 樹不相上下
跳躍連結串列結構
結構
public class SkipList<T> {
//跳躍表的頭尾
private SkipListNode<T> head;
//跳躍表含的元素長度
private int length;
//跳錶的層數 的歷史最大層數
public int maxLevel;
public SecureRandom random;
private static final int MAX_LEVEL = 31;
public SkipList() {
//初始化頭尾節點及兩者的關係
head = new SkipListNode<>(SkipListNode.HEAD_SCORE, null, MAX_LEVEL);
//初始化大小,層,隨機
length = 0;
maxLevel = 0; // 層數從零開始計算
random = new SecureRandom();
}
...
- header:指向跳躍表的頭節點
- maxLevel:記錄目前跳躍表,層數最大節點的層數
- length:連結串列存在的元素長度
節點
跳躍連結串列節點的組成:前節點、後節點、分值(map的key值)、及儲存物件 value
public class SkipListNode<T> {
//在跳錶中排序的 分數值
public double score;
public T value;
public int level;
// 前後節點
public SkipListNode<T> next,pre;
//上下節點形成的層
public SkipListNode<T>[] levelNode;
private SkipListNode(double score, int level){
this.score = score;
this.level = level;
}
public SkipListNode(double score, T value, int level) {
this.score = score;
this.value = value;
this.level = level;
this.levelNode = new SkipListNode[level+1];
//初始化 SkipListNode 及 每一層的 node
for (int i = level; i > 0; --i) {
levelNode[i] = new SkipListNode<T>(score, level);
levelNode[i].levelNode = levelNode;
}
this.levelNode[0] = this;
}
@Override
public String toString() { return "Node[score=" + score + ", value=" + value + "]"; }
}
跳錶是用空間來換時間
- 在我實現的跳躍連結串列節點,包括一個 levelNode 成員屬性。它就是節點層。跳躍連結串列能實現快速訪問的關鍵點就是它
- 平時訪問一個陣列,我們是順序遍歷的,而跳躍連結串列效率比陣列連結串列高,是因為它使用節點層儲存多級索引,形成一個稀疏索引,所以需要的更多的記憶體空間
跳躍連結串列有多快
- 如果一個連結串列有 n 個結點,每兩個結點抽取出一個結點建立索引的話,那麼第一層索引的結點數大約就是 n/2,第二層索引的結點數大約為 n/4,以此類推第 m 層索引的節點數大約為 n/(2^m)
- 訪問資料時可以從 m 層索引查詢定位到 m-1 層索引資料。而 m-1 大約是 m 層的1/2。也就是說最優的時間複雜度為O(log/N)
- 最差情況。在實際實現中,每一層索引是無法每次以資料數量對摺一次實現一層索引。因此折中的方式,每一層的索引是隨機用全量資料建一條。也就是說最差情況時間複雜度為O(N),但最優時間複雜度不變
查詢
- 查詢一開始是遍歷最高層 maxLevel 的索引 m。按照以下步驟查詢出等於 score 或者最接近 score 的左節點
- 1:如果同層索引的 next 節點分值小於查詢分值,則跳到 next 節點。cur = next
- 2:如果 next 為空。或者next節點分值大於查詢分值。則跳到下一層 m-1 索引,迴圈 2
- 迴圈 1、2 步驟直到訪問到節點分值和查詢分值一致,或者索引層為零
// SkipList
private SkipListNode<T> findNearestNode(double score) {
int curLevel = maxLevel;
SkipListNode<T> cur = head.levelNode[curLevel];
SkipListNode<T> next = cur.next;
// 和當前節點分數相同 或者 next 為 null
while (score != cur.score && curLevel > 0) {
// 1 向右 next 遍歷
if (next != null && score >= next.levelNode[0].score) {
cur = next;
}
next = cur.levelNode[curLevel].next;
// 2 向下遍歷,層數減1
while ((next == null || score < next.levelNode[0].score) && curLevel > 0) {
next = cur.levelNode[--curLevel].next;
}
}
// 最底層的 node。
return cur.levelNode[0];
}
public SkipListNode<T> get(double score) {
//返回跳錶最底層中,最接近這個 score 的node
SkipListNode<T> p = findNearestNode(score);
//score 相同,返回這個node
return p.score == score ? p : null;
}
插入
- 如果分值存在則替換 value
- 如果分值對應節點不存在,則隨機一個索引層數 level (取值 0~31)。然後依靠節點屬性 levelNode 加入 0 到 level 層的索引
//SkipList
public T put(double score, T value) {
//首先得到跳錶最底層中,最接近這個key的node
SkipListNode<T> p = findNearestNode(score);
if (p.score == score) {
// 在跳錶中,只有最底層的node才有真正的value,只需修改最底層的value就行
T old = p.value;
p.value = value;
return old;
}
// nowNode 為新建的最底層的node。索引層數 0 到 31
int nodeLevel = (int) Math.round(random.nextDouble() * 32);
SkipListNode<T> nowNode = new SkipListNode<T>(score, value, nodeLevel);
//初始化每一層,並連線每一層前後節點
int level = 0;
while (nodeLevel >= p.level) {
for (; level <= p.level; level++) {
insertNodeHorizontally(p.levelNode[level], nowNode.levelNode[level]);
}
p = p.pre;
}
// 此時 p 的層數大於 nowNode 的層數才進入迴圈
for (; level <= nodeLevel; level++) {
insertNodeHorizontally(p.levelNode[level], nowNode.levelNode[level]);
}
this.length ++ ;
if (this.maxLevel < nodeLevel) {
maxLevel = nodeLevel;
}
return value;
}
private void insertNodeHorizontally(SkipListNode<T> pre, SkipListNode<T> now) {
//先考慮now
now.next = pre.next;
now.pre = pre;
//再考慮pre的next節點
if (pre.next != null) {
pre.next.pre = now;
}
//最後考慮pre
pre.next = now;
}
刪除
- 使用 get 方法找到元素,然後解除節點屬性 levelNode 在每一層索引的前後引用關係即可
//SkipList
public T remove(double score){
//在底層找到對應這個key的節點
SkipListNode<T> now = get(score);
if (now == null) {
return null;
}
SkipListNode<T> curNode, next;
//解除節點屬性 levelNode 在每一層索引的前後引用關係
for (int i = 0; i <= now.level; i++){
curNode = now.levelNode[i];
next = curNode.next;
if (next != null) {
next.pre = curNode.pre;
}
curNode.pre.next = curNode.next;
}
this.length--; //更新size,返回舊值
return now.value;
}
使用示例
public static void main(String[] args) {
SkipList<String> list=new SkipList<>();
list.printSkipList();
list.put(1, "csc");
list.printSkipList();
list.put(3, "lwl");
list.printSkipList();
list.put(2, "hello world!");
list.printSkipList();
System.out.println(list.get(2));
System.out.println(list.get(4));
list.remove(2);
list.printSkipList();
}
歡迎指正文中錯誤
參考文章
- redis設計與實現
- 跳錶(跳躍表,skipList)總結-java版
- 資料結構與演算法——跳錶