作用/目的
跳表作為一種資料結構通常用於取代平衡樹。
起因平衡樹可以用於表示抽象的資料型別如字典和有序鍊錶,它通過樹旋轉(tree rotation)操作強制使樹結構保持平衡來保證節點搜尋的效率。在資料為隨機插入的情況下,平衡樹效能表現良好;但資料為順序插入或者需要刪除節點的情況下,平衡樹的效能就會有些糟糕。
跳表可以作為平衡樹的一種替代選擇。它使用隨機的平衡策略取代平衡樹嚴格的強制的樹平衡策略。因此它具有更簡單有效的插入/刪除方法以及更快的搜尋速度。
原理假設有乙個鍊錶,我們要查詢某個節點,則我們需要逐個的查詢鍊錶的每個節點
如果鍊錶是有序的,並且每隔乙個節點都有乙個指向其前面2個位置節點的指標,那我們只需要最多查詢⌈n/2⌉個節點(n為鍊錶長度)
如果再每隔3個節點就有指向其前面4個位置節點的指標,那麼我們就只需要查詢不超過⌈n/4⌉+2個節點
也即如果每個(2^i)位置的節點都有指向其前面2^i個位置節點的指標,則查詢某個節點的次數可以下降到⌈log2^n⌉次(只是指標數會變為之前的雙倍)
這種資料結構可以用於快速的查詢,只是插入和刪除不太容易實現。
如果不再依照節點的位置,而是採取一種隨機的策略來決定節點是否具有額外的指向前面節點的指標呢?
假設擁有k個前向指標的節點我們稱之為k等級節點,在節點被分配出來的時候,我們通過隨機策略(按照一定的概率)來決定節點的等級(也即有幾個前向指標),節點的第i個指標也不再指向其前面2^i個位置的節點,而是指向等級i的下個節點。這樣,插入和刪除節點都只需要做很少的改動,其整體的效果卻和上面所描述的類似。
由於這種資料結構是乙個鍊錶帶有額外的指標,在鍊錶的節點間跳躍,因此,原作者稱其為跳躍鍊錶(skip lists)。
實現/演算法
節點等級
隨機生成節點等級的演算法有很多種,這裡介紹原作者採用的演算法:
首先確定乙個概率p(1/2、1/4等),用於確定節點是否需要有下乙個等級。
就跟投骰子一樣,節點有1/2或1/4的概率獲得下乙個等級,如果是,則節點的等級k=k+1,如果不是,則節點的等級為k,至此結束。
如此重複迴圈。
但這裡會有乙個問題,某些節點的等級k可能會很大(一直獲得下乙個等級,雖然概率極低),這在演算法的原理上沒有問題(除了有極少的效能損耗),但在工程的實現上會相當麻煩,因此,在實際的實現當中,通常會設定乙個最高等級(max_level),並且還會有乙個當前鍊錶最大等級,搜尋的時候從當前最大等級開始。
關於p和max_level取值,原作者推薦的p值是1/4或1/2,max_level可根據所選的p及鍊錶所含的最多元素個數n通過公式logp^n所得。
初始化初始化的時候,我們會分配乙個nil節點(最終節點)並將其key值設為最大int值,還會分配乙個鍊錶初始節點,其header擁有max_level個前向指標,所有的前向指標都初始化成指向nil節點(表明鍊錶中暫無節點)。
搜尋演算法
插入和刪除節點只需要在搜尋的基礎上再進行簡單的插入和刪除操作,只是需要注意兩個操作當中前向指標關係的處理,以及增加和減少鍊錶等級後及時更新當前最大等級的值。插入的過程可見如下示意圖:
c實現**:
#include #include #include #define max_num_of_level 16
#define max_level (max_num_of_level-1)
#define max_int 0x7fffffff /* max integer */
#define max_bits 32 /* 32-bit integers */
typedef struct _node node;
typedef struct _skiplist skiplist;
node *nil;
int random_bits;
int bits_left;
int random()
node *alloc_node_of_level(int level)
init()
int random_level()
} while (!b);
return (level > max_level ? max_level : level);
}
skiplist *new_list()
void free_list(skiplist *l)
free(l);
}int insert(skiplist *l, int key, int value)
if (q->key == key)
/* insert new node */
k = random_level();
if (k > l->level)
q = alloc_node_of_level(k);
q->key = key;
q->value = value;
while (k >= 0)
return 0;
}
int delete(skiplist *l, int key)
if (q->key != key)
k = 0;
while (k <= m && (p = update[k])->forward[k] == q)
free(q);
while (l->header->forward[m] == nil && m > 0)
m--;
l->level = m;
return 0;
}int search(skiplist *l, int key, int &value)
if (q->key != key)
*value = value;
return 0;
}/* tests */
int main(int argc, char *argv)
for (i = 0; i < 4; i++)
for(k = 0; k < 65536; k++)
}free_list(l);
return 0;
}
參考資料
[1] skip lists: a probabilistic alternative to balanced trees, william pugh.
[2]
跳表的原理與實現 Golang 版
跳表的原理與實現 golang 版 有時候,我們會說,在計算機世界裡,其實只有兩種資料結構,乙個是陣列乙個是鍊錶。原因是其他的資料結構都是基於這兩種資料結構做的擴充套件。陣列和鍊錶的優缺點實在是非常的明顯。陣列可以高效查詢,按照下標索引,但是很難進行高效的刪除和擴容。鍊錶的優缺點正好相反。很多時候,...
資料結構與演算法 跳表的實現
我們知道redis leveldb 都是著名的 key value 資料庫,redis中 的 sortedset以及leveldb 中的 memtable 都用到了跳表,那麼什麼是跳表呢?跳表又是如何實現的呢?說跳表之前,先說說有序鍊錶 乙個有序鍊錶搜尋 新增 刪除的平均時間複雜度是都是o n 有序...
跳表的原理及其實現
作用 目的 跳表作為一種資料結構通常用於取代平衡樹。起因平衡樹可以用於表示抽象的資料型別如字典和有序鍊錶,它通過樹旋轉 tree rotation 操作強制使樹結構保持平衡來保證節點搜尋的效率。在資料為隨機插入的情況下,平衡樹效能表現良好 但資料為順序插入或者需要刪除節點的情況下,平衡樹的效能就會有...