0
点赞
收藏
分享

微信扫一扫

Chromium127编译指南 Linux篇 - 编译前的准备工作(二)

殇感故事 2024-11-11 阅读 8

跳表skiplist

1 跳表的原理

要了解跳表的原理,可以先了解一下有序链表:

在这里插入图片描述

上面是一个递增的有序链表,要查找一个值需要从头节点开始一步一步向后查询,事件复杂度为O(n),而插入和删除的时间复杂度为O(1)。

因此我们可以将有序链表分层,如下图所示,越高的层结点数量越少,查找时优先从高层开始,当某个结点的next结点值大于key或者为空时,就从下层开始继续查找。当数据量较大时,这样可以显著提高效率。

在这里插入图片描述

综上可得,跳表的基本思想为:

通过将有序集合的部分节点分层,由最上层开始依次向后查找,如果本层的next节点大于要查找的值或next节点为NULL,则从本节点开始,降低一层继续向后查找,依次类推,如果找到则返回节点;否则返回NULL。

这个跳表的思想和顺序表的二分查找思想是异曲同工的,都通过最大程度排除待查找的元素来提高效率。跳表的每个结点都维护了多个指向其他结点的指针(对应不同的层),所以其在查找、插入、删除操作时可以跳过一些结点,快速找到操作需要的结点。

在这里插入图片描述

2 跳表的优势

  • 效率高,查找的时间复杂度为O(logn),插入、删除时间复杂度为O(1)。
  • 实现简单,与红黑树相比,两者性能相差不大,但是前者实现简单。

3 跳表的数据结构

本文以redis中的跳表为例讲解跳表的实现。

跳表结点的结构

typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;
  • ele: 是一个动态长度的字符串,键值对中的“值”
  • score:关键字值
  • backword:指向前一个结点
  • level:是一个柔性数组,代表不同的层次
  • forward:指向下一个结点
  • span:本结点到下一个结点中间的结点个数,即结点间的跨度

span字段的作用

跳表的结构

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist
  • header:指向跳表的头节点。头节点是跳表的一个特殊结点,它的level数组元素个数为64,它不指向一个实际的结点,因此ele为NULL,score为0。
  • tail:指向跳表的尾结点
  • length:跳表长度
  • level:跳表的高度

4 跳表的基本操作

4.1 创建跳表

#define ZSKIPLIST_MAXLEVEL 64	// 跳表最大高度为64

Redis通过zsIRandomLevel函数随机生成一个1~64的值,作为新建节点的高度,越大的值出现的概率越低,生成随机层高的代码如下:

#define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */
int zslRandomLevel(void) {
    int level = 1;
    // 这里循环次数越多,总的概率越小
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

然后再拿到score和member,就可以创建跳表节点了

zskiplistNode *zslCreateNode(int level, double score, sds ele)
{
    zskiplistNode *zn =
    	zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}

创建头节点

for (int j = 0; j < ZSKIPLIST_MAXLEVEL; ++j)
{
    zsl->header->level[j].forward = NULL;
    zsl->header->level[j].span = 0;
}

创建跳跃表

zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl));
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
zsl->header->backward = NULL;
zsl->level = 1;
zsl->length = 0;
zsl->tail = NULL;

4.2 插入节点

首先要找到要插入的位置,根据前面原理部分的讲解,可以写出查找一个节点的代码。

查找要插入的位置

为了找到要更新的节点,我们需要以下两个长度为64的数组来辅助操作。

  1. update[]:插入节点时,需要更新被插入节点每层的前一个节点。由于每层更新的节点不一样,所以将每层需要更新的节点记录在update[i]中。

  2. rank[]:记录当前层从header节点到update[i]节点所经历的步长,在更新update[i]的span和设置新插入节点的span时用到。

x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
    rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
    while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                sdscmp(x->level[i].forward->ele,ele) < 0)))
    {
        rank[i] += x->level[i].span;
        x = x->level[i].forward;
    }
    update[i] = x;
}

调整跳跃表高度

由上文可知,插入节点的高度是随机的,假设要插入节点的高度为3,大于跳跃表的高度2,所以我们需要调整跳跃表的高度。代码如下:

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插入update[i]和update[i]->forward之间,需要更新x和update[i]的forward和span值。

x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) {
    x->level[i].forward = update[i]->level[i].forward;
    update[i]->level[i].forward = x;
    x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
    update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
image-20241107233403984

调整backward

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;

4.3 删除节点

查找到要被删除的节点

这一步与掺入节点时的查询操作基本相同。

设置span和update

假设x指向当前要被删除的节点,如果update[i]第i层的forward不为x,说明update[i]的层高大于x的层高,即update[i]第i层指向了指向了x的后续节点或指向NULL。由于删除了一个节点,所以update[i]的leve[i]的span需要减1。

否则,如果update[i]第i层的forward就是x,那么删除x节点后的update[i]的第i层的span值就是update[i]->level[i].span + x->level[i].span - 1。

代码如下:

void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    for (i = 0; i < zsl->level; i++) {
        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;
        }
    }
}

调整backward、跳表高度、长度

代码如下:

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--;

4.4 删除跳表

获取到跳跃表对象之后,从头节点的第0层开始,通过forward指针逐步向后遍历,每遇到一个节点便将释放其内存。当所有节点的内存都被释放之后,释放跳跃表对象,即完成了跳跃表的删除操作。代码如下:

void zslFree(zskiplist *zsl) {
    zskiplistNode *node = zsl->header->level[0].forward, *next;

    zfree(zsl->header);
    while(node) {
        next = node->level[0].forward;
        zslFreeNode(node);
        node = next;
    }
    zfree(zsl);
}

学习参考

学习更多相关知识请参考零声 github。

举报

相关推荐

0 条评论