對於一個有序陣列,如果要查詢其中的一個數,我們可以使用二分查詢(Binary Search)演算法,將它的時間複雜度降低為O(logn).那查詢一個有序連結串列,有沒有辦法將其時間複雜度也降低為O(logn)呢?
跳錶(skip list),全稱為跳躍連結串列,實質上就是一種可以進行二分查詢的有序連結串列,它允許快速查詢、插入和刪除有序連結串列。
跳錶使用的前提是連結串列有序,就像二分查詢也要求有序陣列
怎麼理解跳錶
要查詢其中值為20的元素,之前都是採取按順序進行遍歷的方法,但這樣做時間複雜度就變成了O(n).怎樣才能提高效率呢?我們可以通過對連結串列建立一級索引,查詢的時候先遍歷索引,通過索引找到原始層繼續遍歷。索引如下圖所示
那麼查詢20的過程就變成了先使用索引遍歷 2 -> 7 -> 12 -> 20,然後順著索引連結串列的結點向下找到原始連結串列的結點20.之前需要遍歷7次,現在需要遍歷5次。在資料量小的時候跳錶效能優化並不明顯,但當有序連結串列包含大量資料時,結點的訪問次數大致會減少一半。
現在我們新增兩層索引,基於第一層的索引再新增一層,如下圖所示
要查詢20,先在第二層索引上遍歷 2 -> 12 ,然後向下轉到第一層索引遍歷 12 - > 20,最後向下找到原始連結串列的結點20.
這個例子中,原始有序連結串列的結點數量很少,當結點數量很多時,可以抽出更多的索引層級,每一層索引結點的數量都是低層索引的一半。
跳錶複雜度分析
時間複雜度
演算法的執行效率可以通過時間複雜度來衡量,跳錶的時間複雜度是多少呢?我們來分析一下。
前面我們每兩個結點抽一個結點作為上一級索引的結點,那麼假設原始連結串列的長度為n,第一層索引的結點個數為n/2,第二層索引的個數為n/4,第k級的索引結點個數就是n/(2k)。假設索引有 h 級,最高階的索引有 2 個結點。通過上面的公式,我們可以得到 n/(2h)=2,從而求得 h=log2n-1。如果包含原始連結串列這一層,整個跳錶的高度就是 log2n。我們在跳錶中查詢某個資料的時候,如果每一層都要遍歷 m 個結點,那在跳錶中查詢一個資料的時間複雜度就是 O(m*logn)。
m的值怎麼計算呢?在上面的例子中,每一層最多隻需要遍歷三個元素,因此m=3,根據
空間複雜度
每兩個結點中抽一個結點作為上級索引,很明顯,它的空間複雜度為O(n)
.
?♂這是一個典型的空間換時間操作。原始連結串列中儲存的有可能是很大的物件,而索引結點只需要儲存關鍵值和幾個指標,並不需要儲存物件,所以當物件比索引結點大很多時,索引佔用的額外空間就可以忽略了。
高效的插入和刪除
插入操作
向連結串列插入資料的時間複雜度是O(1),但為了保持連結串列資料有序,需要先找到插入結點的前置結點,然後插入資料到前置結點後面,其時間複雜度為O(logn)
。假設我們要插入10,需要先找到前置結點9,然後插入10。
刪除操作
刪除的話也是需要先找到要刪除的結點,如果該結點在索引中也有出現的話,索引中的也需要刪除。因為單連結串列中的刪除操作需要拿到要刪除結點的前驅結點,然後通過指標操作完成刪除。所以在查詢要刪除的結點的時候,一定要獲取前驅結點。
動態更新索引
當我們一直往跳錶中插入資料時,兩個索引結點之間的資料可能會變得非常多,在極端情況下,跳錶還會退化成單連結串列,這樣的話跳錶的優勢也就沒有了。
因此我們需要用一些方法來維護索引和原始連結串列之間的平衡,也就是在增加原始連結串列中結點內容的時候適當增加索引的大小。為了維護平衡,跳錶的設計者採用了一種有趣的方法:“拋硬幣”,也就是隨機決定新結點是否建立索引,兩個結點建立一個索引的話,每層的概率為50%。
Java實現跳錶
下面是王爭老師
package skiplist;
/**
* 跳錶的一種實現方法。
* 跳錶中儲存的是正整數,並且儲存的是不重複的。
*
* Author:ZHENG
*/
public class SkipList {
private static final float SKIPLIST_P = 0.5f;
private static final int MAX_LEVEL = 16;
private int levelCount = 1;
private Node head = new Node(); // 帶頭連結串列
public Node find(int value) {
Node p = head;
for (int i = levelCount - 1; i >= 0; --i) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
}
if (p.forwards[0] != null && p.forwards[0].data == value) {
return p.forwards[0];
} else {
return null;
}
}
public void insert(int value) {
int level = randomLevel();
Node newNode = new Node();
newNode.data = value;
newNode.maxLevel = level;
Node update[] = new Node[level];
for (int i = 0; i < level; ++i) {
update[i] = head;
}
// record every level largest value which smaller than insert value in update[]
Node p = head;
for (int i = level - 1; i >= 0; --i) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
update[i] = p;// use update save node in search path
}
// in search path node next node become new node forwords(next)
for (int i = 0; i < level; ++i) {
newNode.forwards[i] = update[i].forwards[i];
update[i].forwards[i] = newNode;
}
// update node hight
if (levelCount < level) levelCount = level;
}
public void delete(int value) {
Node[] update = new Node[levelCount];
Node p = head;
for (int i = levelCount - 1; i >= 0; --i) {
while (p.forwards[i] != null && p.forwards[i].data < value) {
p = p.forwards[i];
}
update[i] = p;
}
if (p.forwards[0] != null && p.forwards[0].data == value) {
for (int i = levelCount - 1; i >= 0; --i) {
if (update[i].forwards[i] != null && update[i].forwards[i].data == value) {
update[i].forwards[i] = update[i].forwards[i].forwards[i];
}
}
}
while (levelCount>1&&head.forwards[levelCount]==null){
levelCount--;
}
}
// 理論來講,一級索引中元素個數應該佔原始資料的 50%,二級索引中元素個數佔 25%,三級索引12.5% ,一直到最頂層。
// 因為這裡每一層的晉升概率是 50%。對於每一個新插入的節點,都需要呼叫 randomLevel 生成一個合理的層數。
// 該 randomLevel 方法會隨機生成 1~MAX_LEVEL 之間的數,且 :
// 50%的概率返回 1
// 25%的概率返回 2
// 12.5%的概率返回 3 ...
private int randomLevel() {
int level = 1;
while (Math.random() < SKIPLIST_P && level < MAX_LEVEL)
level += 1;
return level;
}
public void printAll() {
Node p = head;
while (p.forwards[0] != null) {
System.out.print(p.forwards[0] + " ");
p = p.forwards[0];
}
System.out.println();
}
public class Node {
private int data = -1;
private Node forwards[] = new Node[MAX_LEVEL];
private int maxLevel = 0;
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{ data: ");
builder.append(data);
builder.append("; levels: ");
builder.append(maxLevel);
builder.append(" }");
return builder.toString();
}
}
}
總結