標籤:
跳躍表是一種插入、查詢、刪除的平均時間複雜度為O(nlogn)的資料結構,在最差情況下是O(n),當然這幾乎很難出現。
和紅/黑樹狀結構相比較最差時間複雜度要差很多,紅/黑樹狀結構是O(nlogn),而跳躍表是O(n)平均時間複雜度是一樣的實現要簡單很多
https://en.wikipedia.org/wiki/Skip_list維基的跳躍表例子
跳躍表的結構如
跳躍表的實現還是一個鏈表,是一個有序的鏈表,在遍曆的時候基於比較,但普通鏈表只能遍曆,跳躍表加入了一個層的概念,層數越高的元素越少,每次先從高層尋找,再逐漸降層,直到找到合適的位置。可以看到高層的節點遠遠少於底層的節點數,從而實現了跳躍式尋找。
redis中的定義/* * 跳躍表 */typedef struct zskiplist { // 表前端節點和表尾節點 struct zskiplistNode *header, *tail; // 表中節點的數量 unsigned long length; // 表中層數最大的節點的層數 int level;} zskiplist;跳躍表的節點/* * 跳躍表節點 */typedef struct zskiplistNode { // 成員對象 robj *obj; // 分值 double score; // 後退指標 struct zskiplistNode *backward; // 層 struct zskiplistLevel { // 前進指標 struct zskiplistNode *forward; // 跨度 unsigned int span; } level[];} zskiplistNode;跳躍表是一個空間換時間的資料結構,和雙鏈表相比,額外的空間開銷就是zskiplistNode中的level數組元素,冗餘儲存了每一層的forward指標。
redis跳躍表實現的一些方法zslCreateNodezslCreatezslFreeNodezslFreezslRandomLevelzslInsertzslDeleteNodezslDelete還有其他一些
重點關注幾個方法/* * 建立並返回一個新的跳躍表 * * T = O(1) */zskiplist *zslCreate(void) { int j; zskiplist *zsl; // 分配空間 zsl = zmalloc(sizeof(*zsl)); // 設定高度和起始層數 zsl->level = 1; zsl->length = 0; // 初始化表前端節點 //表頭一定具有最高的level // T = O(1) zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) { zsl->header->level[j].forward = NULL; zsl->header->level[j].span = 0; } zsl->header->backward = NULL; // 設定表尾 zsl->tail = NULL; return zsl;}//返回一個隨機值,作為新跳躍表節點的層次//層次的合理分布是跳躍表的效率所在int zslRandomLevel(void) { int level = 1; while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;}/* * 建立一個成員為 obj ,分值為 score 的新節點, * 並將這個新節點插入到跳躍表 zsl 中。 * * 函數的傳回值為新節點。 * * T_wrost = O(N^2), T_avg = O(N log N) */zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) { zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; unsigned int rank[ZSKIPLIST_MAXLEVEL]; int i, level; redisAssert(!isnan(score)); // 在各個層尋找節點的插入位置 // T_wrost = O(N^2), T_avg = O(N log N) x = zsl->header; for (i = zsl->level-1; i >= 0; i--) { /* store rank that is crossed to reach the insert position */ // 如果 i 不是 zsl->level-1 層 // 那麼 i 層的起始 rank 值為 i+1 層的 rank 值 // 各個層的 rank 值一層層累積 // 最終 rank[0] 的值加一就是新節點的前置節點的排位 // rank[0] 會在後面成為計算 span 值和 rank 值的基礎 rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; // 沿著前進指標遍曆跳躍表 // T_wrost = O(N^2), T_avg = O(N log N) while (x->level[i].forward && (x->level[i].forward->score < score || // 比對分值 (x->level[i].forward->score == score && // 比對成員, T = O(N) compareStringObjects(x->level[i].forward->obj,obj) < 0))) { // 記錄沿途跨越了多少個節點 rank[i] += x->level[i].span; // 移動至下一指標 x = x->level[i].forward; } // 記錄將要和新節點相串連的節點 update[i] = x; } /* we assume the key is not already inside, since we allow duplicated * scores, and the re-insertion of score and redis object should never * happen since the caller of zslInsert() should test in the hash table * if the element is already inside or not. * * zslInsert() 的調用者會確保同分值且同成員的元素不會出現, * 所以這裡不需要進一步進行檢查,可以直接建立新元素。 */ // 擷取一個隨機值作為新節點的層數 // T = O(N) level = zslRandomLevel(); // 如果新節點的層數比表中其他節點的層數都要大 // 那麼初始化表前端節點中未使用的層,並將它們記錄到 update 數組中 // 將來也指向新節點 if (level > zsl->level) { // 初始化未使用層 // T = O(1) for (i = zsl->level; i < level; i++) { rank[i] = 0; update[i] = zsl->header; update[i]->level[i].span = zsl->length; } // 更新表中節點最大層數 zsl->level = level; } // 建立新節點 x = zslCreateNode(level,score,obj); // 將前面記錄的指標指向新節點,並做相應的設定 // T = O(1) for (i = 0; i < level; i++) { // 設定新節點的 forward 指標 x->level[i].forward = update[i]->level[i].forward; // 將沿途記錄的各個節點的 forward 指標指向新節點 update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */ // 計算新節點跨越的節點數量 x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); // 更新新節點插入之後,沿途節點的 span 值 // 其中的 +1 計算的是新節點 update[i]->level[i].span = (rank[0] - rank[i]) + 1; } /* increment span for untouched levels */ // 未接觸的節點的 span 值也需要增一,這些節點直接從表頭指向新節點 // T = O(1) for (i = level; i < zsl->level; i++) { update[i]->level[i].span++; } // 設定新節點的後退指標 x->backward = (update[0] == zsl->header) ? NULL : update[0]; if (x->level[0].forward) x->level[0].forward->backward = x; else zsl->tail = x; // 跳躍表的節點計數增一 zsl->length++; return x;}
跳躍表尋找節點的過程(以插入元素為例,刪除、尋找的過程是一樣的)1.從head開始,根據forward指標向前尋找,如果前一個元素大於待尋找的元素或者遇到tail指標,下移層次繼續尋找;如果下一個元素不大於待尋找的元素,forward向前推進一個節點,繼續比較。2.重複1步驟,直到level1遇到的前一個節點的值大於待尋找的值最終總是能找到比待尋找節點的值大的前一個位置,在這個位置插入元素。
redis的跳躍表