1. 程式人生 > >skiplist及Java實現

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:

  • If the key k is found in the Skip ListfindEntry(k) will return the reference to the entry containg the key k  //找到的準備返回
  • If the key k is not found in the Skip ListfindEntry(k) will return the reference to the floorEntry(k) entry containg a key that is smaller than k //找不到則返回底層比k小的

    Example: findEntry(42) will return the reference to 39

插入:實現put方法: 如果put的key值在跳躍表中存在,則進行修改操作; 如果put的key值在跳躍表中不存在,則需要進行新增節點的操作,並且需要由random隨機數決定新加入的節點的高度(最大level); 當新新增的節點高度達到跳躍表的最大level,需要新增一個空白層(除了-oo和+oo沒有別的節點)

上面是個插入的動圖,下面分佈把圖展示出來:

插入之前:

1,查詢適合插入的位子

  • p = findEntry(k)

 

在查詢到的p節點後面插入新增的節點q  insert q after p:

3 Now make a column of random heightrepeat 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 entryright 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;
    }

還有需要說明的一點是:跳躍表每次執行的結果是不一樣的,這就是為什麼說跳躍表是屬於隨機化資料結構。

參考: