日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Redis源码剖析(十一)跳表

發布時間:2024/4/19 数据库 57 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis源码剖析(十一)跳表 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在樹形結構中,常見的平衡樹有AVL樹和紅黑樹,但是由于AVL樹過于平衡,導致維護平衡所需的代價過大,使用的不多,不過其中幾種旋轉算法還是值得學習的。取而代之的是較為平衡的紅黑樹,STL中的map和set都是采用紅黑樹實現的,插入和查找效率為O(logN)。

而跳表也是一種較為平衡的數據結構,與紅黑樹不同的是,它是鏈狀結構而非樹形結構,不過,跳表的插入查找效率也為O(logN),和紅黑樹有一拼,而且最重要的是,跳轉在實現上比紅黑樹簡單的多

跳表結構

同數組相比,鏈表的插入刪除效率是O(1),但是如果想要在鏈表中查找某個元素,就糟糕了,復雜度會是O(N),為了提高查找效率,就有了跳表的概念。所謂跳表,就是可以跳躍的鏈表,回想二分查找算法,每次的查找都是跳躍性的,這才使得二分法效率這么高,跳表的設計同樣也借鑒了二分法的策略,實現跳躍查找,當然,需要跳表中的元素有序

普通的鏈表每個節點僅僅保存了指向下一個節點的指針,只能移動到下一個相鄰節點,也就是跳一步。而跳表為了可以一次跳很多步,保存了很多指針,指向該節點后面的不同節點

在server.h頭文件中,可以找到跳表節點的定義和跳表的定義

/* 跳表節點 */ typedef struct zskiplistNode {robj *obj; /* 數據 */double score; /* 分數 */struct zskiplistNode *backward; //前一個節點指針struct zskiplistLevel {struct zskiplistNode *forward; //后面某個節點,也就是next指針unsigned int span; //跨度} level[]; /* 跳表中保存了多個指向下一個節點的指針 */ } zskiplistNode;

跳表實際上也是保存鍵值對的結構,其中obj保存實際的數據而score用于排序使用,這保證了跳表內部是有序的。此外,level數組記錄了多個指向后面節點的指針,同時也記錄了兩個節點之間的跨度

為了方便,接下來將跳表節點中指向下一個節點的指針稱為next指針,而level數組稱為next數組

/* 跳表 */ typedef struct zskiplist {struct zskiplistNode *header, *tail; //表頭表尾unsigned long length; /* 跳表中節點個數 */int level; //跳表總層數 } zskiplist;

跳表定義中記錄了表頭表尾,level記錄了當前跳表的總層數

下面是跳表的一個例子,可以看到,每個節點都有若干個next指針,通過這些指針,可以直線跳躍移動,而不再是只能移動到相鄰節點

圖片轉自https://zcheng.ren/2016/12/06/TheAnnotatedRedisSourceZskiplist/

跳表操作

創建跳表

創建跳表由zslCreate函數實現,函數中需要調用zslCreateNode創建跳表節點,主要就是申請內存,設置初值

//t_zset.c /*** 創建一個跳表節點* level : 節點包含的層數,即節點next數組的大小,每一層有一個next指針 * score : 該節點的分值,用于使跳表數據有序* obj : 跳表保存的數據**/ zskiplistNode *zslCreateNode(int level, double score, robj *obj) {/* 申請跳表節點內存 */zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));/* 設置初值 */zn->score = score;zn->obj = obj;return zn; }/* 創建一個跳表 */ zskiplist *zslCreate(void) {int j;zskiplist *zsl;/* 申請跳表內存, 初始總層數為1 */zsl = zmalloc(sizeof(*zsl));zsl->level = 1;zsl->length = 0;/* 申請表頭節點,默認有32層 */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; }

值得注意的是,表頭節點是在創建跳表就申請好的,不屬于跳表數據的一部分。

搜索操作

跳表的多數操作都是基于搜索的,考慮這樣一個需求,就是在跳表中查找某個節點,如果該節點不存在,找到它應該插入的位置。雖然Redis中沒有實現搜索功能,但是后面會看到,插入刪除等函數都是基于搜索的

假設此時跳表結構如下,需要從跳表中查找分值為23的節點

查找操作的步驟如下

  • 從最高層開始嘗試移動,比較next節點的分值和待查找分值大小
  • 如果next節點的分值小于待查找分值,則移動到next節點
  • 如果next節點的分值大于等于待查找分值,則降層,如果此時為第0層不能繼續降層,當前節點位置就是待查找節點的前一個節點
  • 查找完成后,找到的節點的下一個節點不是待查找節點,就是分值23應該插入的位置

每次搜索操作,不管是找到還是沒找到,返回的節點都是目標位置的前一個節點,所以,需要判斷返回節點的下一個節點的分值與給定分值的關系從而得知要查找的節點是否在跳表中。當然也可以獲得要插入的位置,比如查找分值為21的節點,返回的同樣是指向20的節點,該節點的下一個位置就是分值為21應該插入的位置

插入操作

插入操作實際上就是執行了一遍搜索功能,由于插入一個節點,會破壞某些節點的next數組。所以需要在搜索過程中記錄每一層的前一個節點,以插入22為例,每一層的前一個節點分別是7,20,20三個節點(實際上是兩個)

//t_zset.c /* 在跳表中插入節點,值為score數據為obj */ /* 因為插入節點會破壞原跳表的結構,所以需要先找到會被破壞的那些節點 * 被破壞的節點是每一層插入位置的前一個節點,因為它的next數組需要更改 */ zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {/* update保存每一層插入位置的前一個節點 */zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;unsigned int rank[ZSKIPLIST_MAXLEVEL];int i, level;serverAssert(!isnan(score));x = zsl->header;/* 尋找每一層插入位置的前一個節點 */for (i = zsl->level-1; i >= 0; i--) {rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];/* 實際上就是跳表的查找規則* 如果當前層上的next指針指向的節點分值大于要查找的分值,則在同層移動* 如果當前層上的next指針指向的節點分值小于要查找的分值,則降層,不移動 *//* 而如果要查找每一層插入位置的上一個節點,那么降層時的節點就要要找的節點 */while (x->level[i].forward &&(x->level[i].forward->score < score ||(x->level[i].forward->score == score &&compareStringObjects(x->level[i].forward->obj,obj) < 0))) {rank[i] += x->level[i].span;/* 同層移動 */x = x->level[i].forward;}/* 如果一旦降層,當前節點就是要查找的節點 */update[i] = x;}/* 為新節點隨機生成一個層數 */level = zslRandomLevel();/* 如果隨機出的層數大于跳表總層數,那么將跳表擴層 */if (level > zsl->level) {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);/* 因為新節點只存在[0 : level]層,所以對于高于level層的那些節點沒有影響 */for (i = 0; i < level; i++) {/* 每一層都相當于鏈表插入 */x->level[i].forward = update[i]->level[i].forward;update[i]->level[i].forward = x;/* 更新跨度span */x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);update[i]->level[i].span = (rank[0] - rank[i]) + 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;elsezsl->tail = x;/* 節點個數增加 */zsl->length++;return x; }

刪除操作

刪除操作首先在跳表中查找需要刪除的節點,如果找到,則將其刪除。需要注意,刪除和插入一樣,都會破壞某些節點的next指針,所以需要更新

zslDelete函數用于找到匹配的節點,zslDeleteNode函數用于將節點從跳表中刪除

//t_zset.c /* 從跳表中刪除節點 */ void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {int i;/* 對于每一層,改變其next數組和跨度 */for (i = 0; i < zsl->level; i++) {/* 如果當前節點的當前層的next節點是要刪除的節點,改變其next指針和跨度 */if (update[i]->level[i].forward == x) {update[i]->level[i].span += x->level[i].span - 1;update[i]->level[i].forward = x->level[i].forward;} else {/* 否則,只需要改變跨度 */update[i]->level[i].span -= 1;}}/* 刪除節點后面的后繼節點的前驅指針也需要改變 */if (x->level[0].forward) {x->level[0].forward->backward = x->backward;} else {zsl->tail = x->backward;}/* 如果刪除的是最高層節點,同時刪除后最高層為空,就將跳表層數降低 */while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)zsl->level--;/* 節點個數減少 */zsl->length--; }/* 刪除分值score和數據obj匹配的節點 */ int zslDelete(zskiplist *zsl, double score, robj *obj) {zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;int i;x = zsl->header;/* 尋找待刪除節點的前一個節點,和插入操作的查找相同 */for (i = zsl->level-1; i >= 0; i--) {while (x->level[i].forward &&(x->level[i].forward->score < score ||(x->level[i].forward->score == score &&compareStringObjects(x->level[i].forward->obj,obj) < 0)))x = x->level[i].forward;update[i] = x;}/* 判斷是否存在要刪除的節點,x是插入位置前的節點,那么它的next指針就是需要刪除的節點 */x = x->level[0].forward;if (x && score == x->score && equalStringObjects(x->obj,obj)) {/* 如果是,則調用刪除節點操作 */zslDeleteNode(zsl, x, update);zslFreeNode(x);return 1;}return 0; /* not found */ }

計算某個節點的排名

Redis跳表可以計算某個數據在跳表中的排名,由zslGetRank函數完成,函數仍然使用查找方法

//t_zset.c /* 計算數據o在跳表中的排名 */ unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) {zskiplistNode *x;unsigned long rank = 0;int i;/* 和插入刪除相同的查找操作 */x = zsl->header;for (i = zsl->level-1; i >= 0; i--) {while (x->level[i].forward &&(x->level[i].forward->score < score ||(x->level[i].forward->score == score &&compareStringObjects(x->level[i].forward->obj,o) <= 0))) {/* 跨度代表當前節點到下一個節點跳過了幾個節點,所以排名需要增加跨度個 */rank += x->level[i].span;x = x->level[i].forward;}/* 當降層后,說明當前層的下一個節點的值已經大于score了,需要降低一層繼續查找* 當然也有可能已經找到,所以需要判斷是否匹配 */if (x->obj && equalStringObjects(x->obj,o)) {return rank;}}return 0; }

對象系統中的跳表

跳表是作為有序集合的底層實現存在的,在object.c文件中,可以看到創建有序集合時將編碼設置為跳表

//object.c /* 創建有序集合對象,底層使用跳表實現 */ robj *createZsetObject(void) {/* 申請有序集合內存 */zset *zs = zmalloc(sizeof(*zs));robj *o;/* 為有序集合創建字典 */zs->dict = dictCreate(&zsetDictType,NULL);/* 創建有序集合中的跳表 */zs->zsl = zslCreate();o = createObject(OBJ_ZSET,zs);/* 設置編碼格式為跳表 */o->encoding = OBJ_ENCODING_SKIPLIST;return o; }

小結

跳表是較平衡的數據結構,實現簡單,插入刪除等都是建立在搜索上的。可以發現,搜索是先嘗試在高層上移動,因為移動的跨度大,可以快速的達到目的地,當不滿足在高層移動的條件時,再降層移動,直到降到最低層。

總結

以上是生活随笔為你收集整理的Redis源码剖析(十一)跳表的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。