skiplist及Java實現
一 序
在看《深入分散式快取》的第7章,介紹redis的set的實現時候,提到了跳錶skiplist.對應的整理下,主要分兩篇吧,本篇先整理跳錶及Java實現。後面在看Java的實現ConcurrentSkipListSet跟ConcurrentSkipListMap。
二 skiplist
2.1 名詞
本節主要從wiki摘取:跳錶由William Pugh 1989年發明。他在論文《Skip lists: a probabilistic alternative to balanced trees》中詳細介紹了跳錶的資料結構和插入刪除等操作。論文是這麼介紹跳錶的:
Skip lists are a data structure that can be used in place of balanced trees. Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees.
Skip list是一個“概率型”的資料結構,可以在很多應用場景中替代平衡樹。Skip list演算法與平衡樹相比,有相似的漸進期望時間邊界,但是它更簡單,更快,使用更少的空間。 Skip list是一個分層結構多級連結串列,最下層是原始的連結串列,每個層級都是下一個層級的“高速跑道”。
如果對於上面說的不好理解,可以跟常見的結構做個比較。
有序陣列。優點:是支援資料的隨機訪問,並且可以採用二分查詢演算法降低查詢操作的複雜度。缺點:插入和刪除資料時,為了保持元素的有序性,需要進行大量的移動資料的操作。
二叉查詢樹。 優點:既支援高效的二分查詢演算法,又能快速的進行插入和刪除操作的資料結構。缺點:是在某些極端情況下,二叉查詢樹有可能變成一個線性連結串列。
平衡二叉樹。對二叉樹的缺點進行改進,引入了平衡的概念。根據平衡演算法的不同,具體實現有AVL樹 / B樹(B-Tree) / B+樹(B+Tree) / 紅黑樹 等等。但是平衡二叉樹的實現多數比較複雜,較難理解。我自己有切身體會,平時業務搬磚頭,拿出個白紙,來寫寫紅黑樹的實現,真寫不出來。
所以對於跳錶,效能接近,還是採用了空間換時間的思路。
Algorithm | Average | Worst case |
---|---|---|
Space | O(n) | O(n log n)[1] |
Search | O(log n) | O(n)[1] |
Insert | O(log n |
O(n) |
Delete | O(log n) | O(n) |
2.2 特性
考慮一個有序表:
從該有序表中搜索元素 < 23, 43, 59 > ,需要比較的次數分別為 < 2, 4, 6 >,總共比較的次數
為 2 + 4 + 6 = 12 次。有沒有優化的演算法嗎? 連結串列是有序的,但不能使用二分查詢。類似二叉
搜尋樹,我們把一些節點提取出來,作為索引。得到如下結構:
這裡我們把 < 14, 34, 50, 72 > 提取出來作為一級索引,這樣搜尋的時候就可以減少比較次數了。
我們還可以再從一級索引提取一些元素出來,作為二級索引,變成如下結構:
這裡元素不多,體現不出優勢,如果元素足夠多,這種索引結構就能體現出優勢來了。
下圖是一個跳錶的例子
跳錶具有如下性質:
(1) 由很多層結構組成
(2) 每一層都是一個有序的連結串列
(3) 最底層(Level 1)的連結串列包含所有元素
(4) 如果一個元素出現在 Level i 的連結串列中,則它在 Level i 之下的連結串列也都會出現。
(5) 每個節點包含兩個指標,一個指向同一連結串列中的下一個元素,一個指向下面一層的元素。
2.3 原理
看了上面的,應該理解了跳錶的由來及特性。本節就是來看對應的原理。
Skip List主要思想是將連結串列與二分查詢相結合,它維護了一個多層級的連結串列結構(就是用空間換取時間)。常見的操作有:搜尋、插入、刪除。
對與一個目標元素的搜尋:會從頂層連結串列的頭部元素開始,然後遍歷該連結串列,直到找到元素大於或等於目標元素的節點,如果當前元素正好等於目標,那麼就直接返回它。如果當前元素小於目標元素,那麼就垂直下降到下一層繼續搜尋,如果當前元素大於目標或到達連結串列尾部,則移動到前一個節點的位置,然後垂直下降到下一層。正因為Skip List的搜尋過程會不斷地從一層跳躍到下一層的,所以被稱為跳躍表。
對於插入:
新節點和各層索引節點逐一比較,確定原連結串列的插入位置。O(logN) 把索引插入到原連結串列。O(1) 利用拋硬幣的隨機方式,決定新節點是否提升為上一級索引。是的話付繼續上面的步驟。
跳錶的設計者用“拋硬幣”的方法選取節點是否提拔,也就是隨機的方式,每個節點有50%概率會提拔。這樣雖然不會讓索引絕對均勻分佈,但也會大體上是均勻的。
刪除:
自上而下,查詢第一次出現節點的索引,並逐層找到每一層對應的節點。O(logN) 刪除每一層查詢到的節點,如果該層只剩下1個節點,刪除整個一層。
三 Java實現
本節主要參考emory大學的課程,搜了下還是美國的名校呢。估計下面的圖大家看了很熟悉,但是轉來轉去的沒標明出處。
3.1 資料節點結構
data 就是具體的儲存資料key,value 。 至於四個指標left,right,up,down,很好理解就是分別節點為了實現跳錶的連結關係。
class SkipListEntry {
Integer key;
Integer value;
SkipListEntry right;
SkipListEntry left;
SkipListEntry down;
SkipListEntry up;
public SkipListEntry(Integer key, Integer value) {
this.key = key;
this.value = value;
}
public String toString()
{
return "(" + key + "," + value + ")";
}
public int pos;//與資料結構無關,只為輸出方便
}
3.2 跳錶結構
public class SkipList<T> {
// number of entries in the Skip List
public int n;
// height
public int h;
// 表頭
private SkipListEntry head;
// 表尾
private SkipListEntry tail;
// 生成randomLevel用到的概率值
private Random r;
list 有頭尾的指標,還需要跳錶的高度h,長度 n,隨機數是模擬拋硬幣隨機高度的。
public SkipList() {
head = new SkipListEntry(Integer.MIN_VALUE, Integer.MIN_VALUE);
tail = new SkipListEntry(Integer.MAX_VALUE, Integer.MAX_VALUE);
head.right =tail;
tail.left = head;
n = 0;
h = 0;
r = new Random();
}
圖上邊界是“-∞”“∞”,有的為了演示方便,把key設定為String型別。這裡就是integer的min,max來代替邊界範圍。
初始化兩個首尾節點,並且連結指向。
3.3 實現map的基本操作
-
get(String key) : 根據key值查詢某個元素
-
put(String key, Integer value) :插入一個新的元素,元素已存在時為修改操作
-
remove(String key): 根據key值刪除某個元素
Notice that each basic operation must first find (search) the appropriate entry (using a key) before the operation can be completed.So we must learn how to search a Skip List for a given key first...就是上面的操作,都依賴於查詢.所以先看查詢實現方法。
查詢:
上面的圖示使用紫色的箭頭畫出了在一個SkipList中查詢key值50的過程。過程如下:
從head出發,因為head指向最頂層(top level)連結串列的開始節點,相當於從頂層開始查詢;
移動到當前節點的右指標(right)指向的節點,直到右節點的key值大於要查詢的key值時停止;
如果還有更低層次的連結串列,則移動到當前節點的下一層節點(down),如果已經處於最底層,則退出;
重複第2步 和 第3步,直到查詢到key值所在的節點,或者不存在而退出查詢; java 實現程式碼如下:
/**
* 查詢
* @param searchKey
* @return
*/
public SkipListEntry findEntry(Integer key)
{
SkipListEntry p;
/* -----------------
Start at "head"
----------------- */
p = head;
while ( true )
{
/* --------------------------------------------
Search RIGHT until you find a LARGER entry
E.g.: k = 34
10 ---> 20 ---> 30 ---> 40
^
|
p stops here
p.right.key = 40
-------------------------------------------- */
while ( p.right.key != Integer.MAX_VALUE && p.right.key< key )
{
p = p.right;
// System.out.println(">>>> " + p.key);
}
/* ---------------------------------
Go down one level if you can...
--------------------------------- */
if ( p.down != null )
{
p = p.down;
// System.out.println("vvvv " + p.key);
}
else
break; // We reached the LOWEST level... Exit...
}
return(p); // p.key <= k
}
public Integer get(int key) {
SkipListEntry p;
p = findEntry(key);
if(p.key ==key) {
return p.value;
} else {
return null;
}
}
note:
|
插入:實現put方法: 如果put的key值在跳躍表中存在,則進行修改操作; 如果put的key值在跳躍表中不存在,則需要進行新增節點的操作,並且需要由random隨機數決定新加入的節點的高度(最大level); 當新新增的節點高度達到跳躍表的最大level,需要新增一個空白層(除了-oo和+oo沒有別的節點)
上面是個插入的動圖,下面分佈把圖展示出來:
插入之前:
1,查詢適合插入的位子
- p = findEntry(k)
2 在查詢到的p節點後面插入新增的節點q insert q after p:
3 Now make a column of random height: repeat these steps a random number of times
3.1 使用隨機數決定新增節點的高度
Starting at p, (using p to) scan left and find the first entry that has an up-entry: 向左找到第一個up不為空的節點
Make p point to the up-element 把p指向 向上的節點
建立一個新的節點。(根插入節點key一樣,value為空)
Insert the newly created entry: right of p and up from q: 插入新建立的節點。注意左右連結跟指向向下的節點。
Make q point to the newly inserted entry 把q指向新插入的節點
repeat the steps and show the effect of building a "tower":只要隨機數滿足條件,key=42的節點就會一直向上攀升,直到它的level等於跳躍表的高度(height)。這個時候我們需要在跳躍表的最頂層新增一個空白層,同時跳躍表的height+1,以滿足下一次新增節點的操作。
Java 實現程式碼如下:
public Integer insert(int key, int value) {
SkipListEntry p, q;
int i = 0;
// 查詢適合插入的位子
p = findEntry(key);
// 如果跳躍表中存在含有key值的節點,則進行value的修改操作即可完成
if(p.key ==key) {
Integer oldValue = p.value;
p.value = value;
return oldValue;
}
// 如果跳躍表中不存在含有key值的節點,則進行新增操作
q = new SkipListEntry(key, value);
/* --------------------------------------------------------------
Insert q into the lowest level after SkipListEntry p:
p put q here p q
| | | |
V V V V V
Lower level: [ ] <------> [ ] ==> [ ] <--> [ ] <--> [ ]
--------------------------------------------------------------- */
q.left = p;
q.right = p.right;
p.right.left = q;
p.right = q;
//本層操作完畢,看更高層操作
//拋硬幣隨機決定是否上層插入
while ( r.nextDouble() < 0.5 /* Coin toss */ )
{
if ( i >= h ) // We reached the top level !!!
{
//Create a new empty TOP layer
addEmptyLevel();
}
/* ------------------------------------
Find first element with an UP-link
------------------------------------ */
while ( p.up == null )
{
p = p.left;
}
/* --------------------------------
Make p point to this UP element
-------------------------------- */
p = p.up;
/* ---------------------------------------------------
Add one more (k,*) to the column
Schema for making the linkage:
p <--> e(k,*) <--> p.right
^
|
v
q
---------------------------------------------------- */
SkipListEntry e;
// 這裡需要注意的是除底層節點之外的節點物件是不需要value值的
e = new SkipListEntry(key, null);
/* ---------------------------------------
Initialize links of e
--------------------------------------- */
e.left = p;
e.right = p.right;
e.down = q;
/* ---------------------------------------
Change the neighboring links..
--------------------------------------- */
p.right.left = e;
p.right = e;
q.up = e;
//把q執行新插入的節點:
q = e;
// level增加
i = i + 1;
}
n = n+1; //更新連結串列長度
return null;
}
private void addEmptyLevel() {
SkipListEntry p1, p2;
p1 = new SkipListEntry(Integer.MIN_VALUE, null);
p2 = new SkipListEntry(Integer.MAX_VALUE, null);
p1.right = p2;
p1.down = head;
p2.left = p1;
p2.down = tail;
head.up = p1;
tail.up = p2;
head = p1;
tail = p2;
h = h + 1;
}
刪除 Deleting an entry from a Skip List
刪除25
刪除節點的操作相對put就比較簡單了,首先查詢到包含key值的節點,將節點從連結串列中移除,接著如果有更高level的節點,則repeat這個操作即可。
public Integer remove(int key) {
SkipListEntry p, q;
p = findEntry(key);
if(!p.key.equals(key)) {
return null;
}
Integer oldValue = p.value;
while(p != null) {
q = p.up;
p.left.right = p.right;
p.right.left = p.left;
p = q;
}
return oldValue;
}
還有需要說明的一點是:跳躍表每次執行的結果是不一樣的,這就是為什麼說跳躍表是屬於隨機化資料結構。
參考: