详解Redis数据结构之跳跃表

详解Redis数据结构之跳跃表

什么是跳跃表

跳跃表(Skiplist)是Redis中用于实现有序集合(sorted set)的底层数据结构之一。它是一种可以替换平衡树的数据结构,具有插入、删除、查找等操作的时间复杂度都为O(log N),并且实现起来比平衡树要简单。

跳跃表的实现原理

跳跃表由若干个节点组成,其中第一个节点为表头,最后一个节点为表尾,每个节点中保存一个分值以及指向下一个节点的指针。

除了每个节点的指针指向下一个节点,还会额外的保存一个指向当前节点“顶层”的指针,这个指针可以使跳跃表更快地进行搜索、插入和删除操作。

跳跃表通过使用不同层次(level)的指针来进行节点之间的连结,每个节点的level可以通过随机算法生成,用户可以通过调整跳跃表的节点level的个数来平衡搜索效率和空间占用。

在跳跃表中,节点按照分值大小从小到大排序,可以快速找到指定范围内的节点。

跳跃表的基本操作

  • 插入节点

在Redis中,插入节点操作通过zslInsert函数实现,其基本的流程为:

  1. 从表头开始遍历跳跃表的每一层,找到前一个节点的位置
  2. 生成一个随机值作为新节点的level
  3. 根据新节点的level,创建新节点
  4. 将新节点插入到每一层的前一个节点位置

示例代码:

void zslInsert(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        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 &&
                  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 element 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. */
    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);
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].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]);
        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;
    else
        zsl->tail = x;
    zsl->length++;
}
  • 删除节点

在Redis中,删除节点操作通过zslDeleteNode函数实现,其基本的流程为:

  1. 从表头开始遍历跳跃表的每一层,找到待删除节点的前一个节点的位置
  2. 通过前一个节点找到待删除的节点
  3. 从每一层上删除此节点

示例代码:

int zslDelete(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i;

    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 &&
                  compareStringObjects(x->level[i].forward->obj,obj)<0))) {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /* We may have multiple elements with the same score, what we need
     * is to find end of the duplicates so we can remove the nodes between
     * start and end. */
    x = x->level[0].forward;
    if (x && score == x->score && equalStringObjects(x->obj,obj)) {
        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;
            }
        }
        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--;
        zslFreeNode(x);
        return 1;
    } else {
        return 0;
    }
}

跳跃表的示例应用

跳跃表在Redis中被广泛应用于实现排序、排名、排行榜等功能,下面我们以排行榜功能为例,进一步示范跳跃表的应用。

示例1:实现排行榜功能

我们假设有一个在线游戏网站,我们需要实现一个排行榜功能,排行榜根据用户的游戏得分进行排名。

首先,我们可以通过跳跃表实现快速地插入、删除、查找操作,用户游戏得分越高,则其在跳跃表中节点的分值越高。

示例代码:

typedef struct player {
    robj *name;
    double score;
} player;

/* 插入用户的游戏得分 */
void playerAdd(zskiplist *zsl, double score, robj *name) {
    player *p = malloc(sizeof(*p));
    p->score = score;
    p->name = name;
    zslInsert(zsl, score, p);
}

/* 删除用户 */
void playerRemove(zskiplist *zsl, player *p) {
    zslDelete(zsl, p->score, p->name);
    free(p);
}

/* 根据排名获取用户 */
player *playerAtRank(zskiplist *zsl, int rank) {
    zskiplistNode *node = zslGetElementByRank(zsl, rank);
    if (node == NULL) {
        return NULL;
    } else {
        return node->obj;
    }
}

/* 获取用户的排名 */
int playerRank(zskiplist *zsl, player *p) {
    return zslGetRank(zsl, p->score, p->name);
}

示例2:实现商品推荐功能

我们尝试通过跳跃表实现商品推荐功能,商品被推荐的概率与其在跳跃表中的分值大小成正比,分值越高的商品,则其被推荐的概率越大。

我们可以通过在跳跃表中插入商品,以商品价格为分值,来实现快速地抽取推荐商品。

下面是示例代码:

typedef struct item {
    robj *name;
    double price;
} item;

/* 插入商品到跳跃表中 */
void itemAdd(zskiplist *zsl, double price, robj *name) {
    item *p = malloc(sizeof(*p));
    p->price = price;
    p->name = name;
    zslInsert(zsl, price, p);
}

/* 从跳跃表中抽取推荐商品 */
item *itemPick(zskiplist *zsl) {
    double prob, score = (double) rand() / RAND_MAX;
    zskiplistNode *node;

    /* 产生随机数score,以此判断要被抽中的商品 */
    node = zsl->header->level[0].forward;
    while (node) {
        /* 获取抽中该商品的概率 */
        prob = (node->score / zsl->length) * node->obj->price / 100;
        if (score < prob) {
            return node->obj;
        }
        score -= prob;
        node = node->level[0].forward;
    }
    /* 如果一直没有找到符合要求的商品,返回最后一个商品 */
    return node == NULL ? zsl->tail->obj : node->obj;
}

总结

跳跃表是一种高效的数据结构,它可以用于实现有序集合、排行榜、商品推荐等操作,其底层实现简单、操作高效,被广泛应用于Redis中。了解跳跃表的实现原理和基本操作可以帮助我们更好地使用Redis,提高编程效率。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:详解Redis数据结构之跳跃表 - Python技术站

(0)
上一篇 2023年6月27日
下一篇 2023年6月27日

相关文章

  • mybatis中文网

    当然,我很乐意为您提供有关“MyBatis中文网”的完整攻略。以下是详细的步骤和两个示例: 1 MyBatis中文网 MyBatis中文网是一个提供MyBatis框架学习资源的网站,包括文档、示例、教程、API等。以下是使用MyBatis中文网的步骤: 1.1 访问MyBatis中文网 首先,您需要访问MyBatis中文网。您可以在浏览器中输入“https:…

    other 2023年5月6日
    00
  • Lua编程中使用嵌套循环的使用教程

    Lua编程中使用嵌套循环的使用教程 在Lua编程中,嵌套循环是一种强大的工具,可以用于处理复杂的问题。嵌套循环允许我们在循环内部再次使用循环,以便多次执行某个操作。本教程将详细介绍如何在Lua中使用嵌套循环,并提供两个示例说明。 基本语法 嵌套循环的基本语法如下: for 初始值1, 终止值1, 步长1 do — 外层循环代码 for 初始值2, 终止值2…

    other 2023年7月28日
    00
  • CorelDraw x6 (Cdr x6) 官方简体中文破解版(32位)安装图文教程、破解注册方法

    CorelDraw x6 (Cdr x6) 官方简体中文破解版(32位)安装图文教程、破解注册方法 简介 CorelDraw x6是一款功能强大的图形设计软件,但官方版本需要付费购买。本攻略将详细介绍如何安装和破解CorelDraw x6的官方简体中文破解版(32位),以便您免费使用该软件。 步骤1:下载软件 首先,您需要下载CorelDraw x6的官方简…

    other 2023年7月28日
    00
  • PHP利用超级全局变量$_POST来接收表单数据的实例

    PHP利用超级全局变量$_POST来接收表单数据的实例攻略 在PHP中,可以使用超级全局变量$_POST来接收通过表单提交的数据。$_POST是一个关联数组,其中的键值对对应着表单中的输入字段名和用户输入的值。 以下是使用$_POST接收表单数据的完整攻略: 步骤1:创建HTML表单 首先,需要创建一个HTML表单,以便用户输入数据。可以使用<form…

    other 2023年7月29日
    00
  • 易语言将指定的主机名与IP地址转换功能

    易语言将指定的主机名与IP地址转换功能攻略 简介 易语言是一种面向中文编程的高级编程语言,它提供了一些方便的网络编程功能,包括将主机名与IP地址进行转换的功能。这个功能可以帮助我们在网络编程中快速获取主机名对应的IP地址,或者获取IP地址对应的主机名。 步骤 步骤一:导入网络编程模块 首先,我们需要导入易语言的网络编程模块,以便使用其中的函数和方法。在易语言…

    other 2023年7月30日
    00
  • js实现右键菜单栏功能

    实现网页右键菜单栏功能一般需要用到 Javascript,可以通过两种方式来实现:自定义菜单和浏览器默认菜单。 自定义菜单 自定义菜单可以通过 JavaScript 代码,动态生成菜单结构,并设置菜单项的点击事件。具体实现过程如下: 给需要添加右键菜单的元素绑定 contextmenu 事件,该事件会在用户在元素上右键点击时触发。例如,在以下 HTML 代码…

    other 2023年6月27日
    00
  • 华为mate50开发者模式在哪?华为mate50关闭开发者模式的方法

    华为Mate50是一款功能强大的智能手机,它集成了许多方便开发人员的功能,其中包括开发者模式。本文将详细讲解华为Mate50开发者模式的位置以及如何关闭该模式。 华为Mate50开发者模式在哪 要使用华为Mate50的开发者模式,首先需要找到该模式的位置。以下是如何找到华为Mate50开发者模式的方法: 打开“设置”应用程序。 滚动到底部并找到“系统”部分。…

    other 2023年6月26日
    00
  • php+jQuery递归调用POST循环请求示例

    下面我就给你详细讲解一下 “php+jQuery递归调用POST循环请求示例” 的完整攻略。 前言 在讲解 “php+jQuery递归调用POST循环请求示例” 之前,我们先了解一下本文中用到的一些基础概念和工具: PHP: PHP 是 Server端的开发语言,常用于编写 Web 应用程序。本文中PHP的版本为 PHP 7.0; jQuery: jQuer…

    other 2023年6月27日
    00
合作推广
合作推广
分享本页
返回顶部