跳錶SkipList

原文地址

1.聊一聊跳錶作者的其人其事
2. 言歸正傳,跳錶簡介
3. 跳錶數據存儲模型
4. 跳錶的代碼實現分析
5. 論文,代碼下載及參考資料


<1>. 聊一聊作者的其人其事

跳錶是由William Pugh發明。他在 Communications of the ACM June 1990, 33(6) 668-676 發表了Skip lists: a probabilistic alternative to balanced trees,在該論文中詳細解釋了跳錶的數據結構和插入刪除操作。
William Pugh同時還是FindBug(沒有使用過,這是一款java的靜態代碼分析工具,直接對java 的字節碼進行分析,能夠找出java字節碼中潛在很多錯誤。)作者之一。現在是University of Maryland, College Park(馬里蘭大學伯克分校,位於馬里蘭州,全美大學排名在五六十名左右的樣子)大學的一名教授。他和他的學生所作的研究深入的影響了java語言中內存池實現。
又是一個計算機的天才!

<2>. 言歸正傳,跳錶簡介

這是跳錶的作者,上面介紹的William Pugh給出的解釋:
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.
跳錶是平衡樹的一種替代的數據結構,但是和紅黑樹不相同的是,跳錶對於樹的平衡的實現是基於一種隨機化的算法的,這樣也就是說跳錶的插入和刪除的工作是比較簡單的。
下面來研究一下跳錶的核心思想:
先從鏈表開始,如果是一個簡單的鏈表,那麼我們知道在鏈表中查找一個元素I的話,需要將整個鏈表遍歷一次。

這裏寫圖片描述

如果是說鏈表是排序的,並且節點中還存儲了指向前面第二個節點的指針的話,那麼在查找一個節點時,僅僅需要遍歷N/2個節點即可。

這裏寫圖片描述

這基本上就是跳錶的核心思想,其實也是一種通過“空間來換取時間”的一個算法,通過在每個節點中增加了向前的指針,從而提升查找的效率。

<3>.跳錶的數據存儲模型

我們定義:
如果一個基點存在k個向前的指針的話,那麼陳該節點是k層的節點。
一個跳錶的層MaxLevel義爲跳錶中所有節點中最大的層數。
下面給出一個完整的跳錶的圖示:

這裏寫圖片描述

那麼我們該如何將該數據結構使用二進制存儲呢?通過上面的跳錶的很容易設計這樣的數據結構:
定義每個節點類型:

// 這裏僅僅是一個指針
typedef struct nodeStructure *node;
typedef struct nodeStructure
{
    keyType key;    // key值
    valueType value;    // value值
    // 向前指針數組,根據該節點層數的
    // 不同指向不同大小的數組
    node forward[1];    
};

這裏寫圖片描述
上面的每個結構體對應着圖中的每個節點,如果一個節點是一層的節點的話(如7,12等節點),那麼對應的forward將指向一個只含一個元素的數組,以此類推。
定義跳錶數據類型:

typedef struct listStructure{
   int level;     /* Maximum level of the list 
            (1 more than the number of levels in the list) */
   struct nodeStructure * header; /* pointer to header */
   } * list; 

跳錶數據類型中包含了維護跳錶的必要信息,level表明跳錶的層數,header如下所示:

這裏寫圖片描述

定義輔助變量:
定義上圖中的NIL變量:node NIL;

#define MaxNumberOfLevels 16
#define MaxLevel (MaxNumberOfLevels-1) 

定義輔助方法:

#define newNodeOfLevel(l) (node)malloc(sizeof(struct nodeStructure)+(l)*sizeof(node *))

好的基本的數據結構定義已經完成,接下來來分析對於跳錶的一個操作。
<4>. 跳錶的代碼實現分析
4.1 初始化
初始化的過程很簡單,僅僅是生成下圖中紅線區域內的部分,也就是跳錶的基礎結構:

這裏寫圖片描述

list newList()
{
  list l;
  int i;
  // 申請list類型大小的內存
  l = (list)malloc(sizeof(struct listStructure));
  // 設置跳錶的層level,初始的層爲0層(數組從0開始)
  l->level = 0;

  // 生成header部分
  l->header = newNodeOfLevel(MaxNumberOfLevels);
  // 將header的forward數組清空
  for(i=0;i<MaxNumberOfLevels;i++) l->header->forward[i] = NIL;
  return(l);
};  

4.2 插入操作
由於跳錶數據結構整體上是有序的,所以在插入時,需要首先查找到合適的位置,然後就是修改指針(和鏈表中操作類似),然後更新跳錶的level變量。

這裏寫圖片描述

boolean insert(l,key,value) 
    register list l;
    register keyType key;
    register valueType value;
{
  register int k;
  // 使用了update數組
  node update[MaxNumberOfLevels];
  register node p,q;

  p = l->header;
  k = l->level;
  /*******************1步*********************/
  do {
        // 查找插入位置
        while (q = p->forward[k], q->key < key)
            p = q;

        // 設置update數組
        update[k] = p;
    } while(--k>=0);    // 對於每一層進行遍歷

    // 這裏已經查找到了合適的位置,並且update數組已經
    // 填充好了元素
   if (q->key == key)
   {
     q->value = value;
     return(false);
   };

   // 隨機生成一個層數
   k = randomLevel();  
  if (k>l->level) 
  {
    // 如果新生成的層數比跳錶的層數大的話
    // 增加整個跳錶的層數
    k = ++l->level;
    // 在update數組中將新添加的層指向l->header
    update[k] = l->header;
  };

/**************2步****************/

  // 生成層數個節點數目
  q = newNodeOfLevel(k);
  q->key = key;
  q->value = value;

  // 更新兩個指針域
  do 
  {
        p = update[k];
        q->forward[k] = p->forward[k];
        p->forward[k] = q;
    } while(--k>=0);

    // 如果程序運行到這裏,程序已經插入了該節點

  return(true);
} 

4.3 刪除某個節點

和插入是相同的,首先查找需要刪除的節點,如果找到了該節點的話,那麼只需要更新指針域,如果跳錶的level需要更新的話,進行更新。

這裏寫圖片描述

boolean delete(l,key) 
register list l;
register keyType key;
{
  register int k,m;
  // 生成一個輔助數組update
  node update[MaxNumberOfLevels];
  register node p,q;

  p = l->header;
  k = m = l->level;
  // 這裏和查找部分類似,最終update中包含的是:
  // 指向該節點對應層的前驅節點
  do 
  {
        while (q = p->forward[k], q->key < key) 
            p = q;
            update[k] = p;
    } while(--k>=0);

    // 如果找到了該節點,才進行刪除的動作
  if (q->key == key) 
  {
    // 指針運算
        for(k=0; k<=m && (p=update[k])->forward[k] == q; k++) 
            // 這裏可能修改l->header->forward數組的值的 
          p->forward[k] = q->forward[k];
        // 釋放實際內存
        free(q);

        // 如果刪除的是最大層的節點,那麼需要重新維護跳錶的
        // 層數level
    while( l->header->forward[m] == NIL && m > 0 )
         m--;
        l->level = m;
        return(true);
    }
  else
    // 沒有找到該節點,不進行刪除動作 
    return(false);
} 

4.4 查找
查找操作其實已經在插入和刪除過程中包含,比較簡單,可以參考源代碼。
<5>. 論文,代碼下載及參考資料
SkipList論文
/Files/xuqiang/skipLists.rar

//——————————————————————————–
增加跳錶c#實現代碼 2011-5-29下午
上面給出的數據結構的模型是直接按照跳錶的模型得到的,另外還有一種數據結構的模型:
跳錶節點類型,每個跳錶類型中僅僅存儲了左側的節點和下面的節點:

這裏寫圖片描述

我們現在來看對於這種模型的操作代碼:

這裏寫圖片描述

初始化完成了如下的操作:

  • 插入操作:和上面介紹的插入操作是類似的,首先查找到插入的位置,生成update數組,然後隨機生成一個level,然後修改指針。
  • 刪除操作:和上面介紹的刪除操作是類似的,查找到需要刪除的節點,如果查找不到,拋出異常,如果查找到的需要刪除的節點的話,修改指針,釋放刪除節點的內存。

代碼下載:
/Files/xuqiang/skiplist_csharp.rar

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章