skipList

如何在大量元素中去查找某个元素。例如在下面的“大量元素”的数组中


对于上面的情况无论是查找3还是查找8,只有一种办法,那就是遍历。时间复杂度是O(N)

但是举一个生活中的例子,我们在查新华字典的时候,没有人会一页一页的翻吧,肯定是翻到中间看看,然后再决定往左边查,还是往右边查。

显然,可以把元素排序放在一个数组中,这样就可以利用二分查找了。查字典也算是二分查找的一个实际例子。二分查找的时间复杂度是O(logN)


利用二分查找是有前提限制的:

1:元素已排序

2:元素可以随机访问


二分查找要求元素可以随机访问,所以决定了需要把元素存储在连续内存。这样查找确实很快,但是插入和删除元素的时候,为了保证元素的有序性,就需要大量的移动元素了。(当然如果删除操作较少的话,也可以用懒惰删除,被删除的元素做个标记,但并不实际从内存中清楚,所以也不用移动元素。但是对于插入还是没有办法)


所以,现在我们需要的是一个能够进行二分查找,又能快速添加和删除元素的数据结构。这就非(平衡)二叉查找树莫属了。

此时,我们可以利用树这个模型,对1,2,3,4,5,6这6个元素够造成一个二叉树,如下:


显然,上面这棵树太坑爹了。把自己的头右转45°或者把显示器左转45°,我们会发现,这TM就是一个链表。这棵树不符合快速查找元素的要求,原因就是,每个节点的左右子树之差>1,也就是不平衡。不平衡就导致了树的高度过高,所以没有办法根据一个节点筛选掉一半的子结点(左右子结点个数不相等)

然后,带有高度平衡条件的AVL树,是下面这样的。


对于这样一棵树,查找元素就是变相的二分查找。(插入,删除也有了保证,直觉上只需修改几个指针)

但是,由于AVL树要求高度平衡,不论是插入还是查找,被操作节点到根节点的整条路径上的所有节点都要判断,是否打破了平衡条件,从而LL, LR, RR, RL旋转。(当然了,实现一颗AVL树也不困难,但是相对于skiplist还是复杂些)

RB树是一种对平衡要求相对略低的AVL树。(类似于2-3树)

但是,无论是高度平衡的AVL还是RB树,自己编写起来难度都比较大。虽然说已经有了现成的set/map(基于红黑树实现)。


通过上面的几个图,我们可以发现,二分查找和AVL树为什么那么高效,原因就是:每比较一次,就能筛掉一半元素。

就像我们查字典一样,没有人会傻傻的从第一页开始一页一页的查,肯定是先翻到字典中间看看,然后决定继续查字典的前半部分还是后半部分。

接下来,我们就来看看跳跃表。

之所以成为跳跃表,就是因为每个节点会额外保存几个指针,间隔的指向后继的节点。



具体的查找,删除,插入原理。这里已经讲了。点击打开链接

我自己实现一个最高为12层的跳跃表,并与红黑树的效率做了对比。


可以看到,数据量很大时,跳跃表的优势显著(当然skiplist很浪费内存这个劣势也很明显)。


http://blog.youkuaiyun.com/qq575787460/article/details/16371287上一篇引入了跳跃表的介绍,并且测试了跳跃表的插入和查找效率。可以看到在大量数据中效率明显高于红黑树。(当然带来的空间的巨大浪费)。


那么跳跃表为什么会快呢?下来看一个生活中的例子。


如图,A--G分别代表250路公交车经过的车站。有特快,快和慢3条线路。

特快的只在A,E,G站牌停车。

快线只在A,C,E,H,G站牌停车。

慢线则在所有的站牌停车。

如果我想A站坐到I站,那么最快的乘车方案就是

A站牌---特快线---E站牌---换成快线---H站牌---换成慢线---I站牌。

是不是很像跳跃表?


并且在上一篇博客中测试情况看来,跳跃表与红黑树的查找效率差别不大,而跳跃表的插入效率明显高于红黑树。究其原因,跳跃表在插入的时候,只需要记下来插入一个节点需要更新哪一层的哪个节点就行了,而红黑树则需要回溯的对整个插入路径调整。


在插入节点时,我们是随机的给新节点一个层次高度,其实并不是随机。

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int get_rand_lvl()  
  2. {  
  3.     int lvl = 1;  
  4.     while ((rand() & 0xffff) < (0xffff >> 2))  
  5.          lvl++;  
  6.     return lvl;  
  7. }  
关键是要理解while循环中的那一句是什么意思?

rand() & 0xffff 意思是随机一个0---0xffff之间的整数。

0xffff >> 2 意思是0xffff的四分之一

可以把while循环理解为产生一个0--1之间的小数,如果这个小数小于0.25,那么把层数+1,并且进行下一次while循环。

是不是感觉很诡异,为什么不直接随机一个1---MAX_LVL之间的数呢?

我想了好几天,终于知道了,这涉及到了层次分布的问题。

通过while循环来产生层次,那么层次越大,几率越低。

例如,lvl=1的概率为0.75(第一次while就为false)

lvl=2的概率为0.25

lvl=3的概率为0.25*0.25

lvl=4的概率为0.25*0.25*0.25

所以,如果数据量非常大,那么整个跳跃表看起来是波浪线非常明显的。

而如果直接随机出来一个高度,那么高度的分布均匀了,反而不利于查找。


跳跃表的源码点击打开链接


skip_list.h

#ifndef SKIP_LIST_H_
#define SKIP_LIST_H_

struct skip_list_node;

class skip_list
{
        public:
                explicit skip_list(int max_level = 12);
                ~skip_list();


                int insert(int data);
                bool find(int data) const;
                int remove(int data);
                int size() const;


        private:
                int rand_level();


        private:
                skip_list_node *header_;
                int size_;
                int max_level_;


        private:
                skip_list(const skip_list&);
                skip_list& operator= (const skip_list&);
};

#endif


skip_list.cpp


#include "skip_list.h"

#include <stdlib.h>
#include <time.h>

struct skip_list_node
{
        int data_;
        skip_list_node **forward_;
};

skip_list::skip_list(int max_level /*=12*/)
        : header_(NULL), size_(0), max_level_(max_level)
{
        this->header_ = new skip_list_node;
        this->header_->forward_ = new skip_list_node*[this->max_level_];
        for (int level = 0; level < this->max_level_; ++level)
                this->header_->forward_[level] = NULL;
}

skip_list::~skip_list()
{
        while (this->header_)
        {
                skip_list_node *tmp_node = this->header_;
                this->header_ = this->header_->forward_[0];
                delete []tmp_node->forward_;
                delete tmp_node;
        }
}

int skip_list::insert(int data)
{
        skip_list_node **update = new skip_list_node*[this->max_level_];
        skip_list_node *cur = this->header_;
        for (int k = this->max_level_ - 1; k >= 0; --k)
        {
                skip_list_node *next = NULL;
                while ((next = cur->forward_[k]) && (next->data_ < data))
                        cur = next;
                update[k] = cur;
        }

        bool b_find = (update[0]->forward_[0]
                        && update[0]->forward_[0]->data_ == data);
        if (!b_find)
        {
                skip_list_node *node = new skip_list_node;
                node->data_ = data;
                int new_node_level = this->rand_level();
                if (new_node_level > this->max_level_)
                        new_node_level = this->max_level_;
                node->forward_ = new skip_list_node*[new_node_level];
                for (int k = 0; k < new_node_level; ++k)
                {
                        node->forward_[k] = update[k]->forward_[k];
                        update[k]->forward_[k] = node;
                }
                ++this->size_;
        }

        delete []update;

        return b_find ? -1 : 0;
}
bool skip_list::find(int data) const
{
        bool b_find = false;
        skip_list_node *cur = this->header_;
        for (int k = this->max_level_ - 1; k >= 0; --k)
        {
                skip_list_node *next = NULL;
                while ((next = cur->forward_[k]) && next->data_ < data)
                        cur = next;
                if (next && next->data_ == data)
                {
                        b_find = true;
                        break;
                }
        }

        return b_find;
}

int skip_list::remove(int data)
{
        skip_list_node **update = new skip_list_node*[this->max_level_];
        for (int k = this->max_level_ - 1; k >= 0; --k)
        {
                skip_list_node *cur = this->header_;
                skip_list_node *next = NULL;
                while ((next = cur->forward_[k]) && next->data_ < data)
                        cur = next;
                update[k] = cur;
        }
        bool b_find = (update[0]->forward_[0]
                                                                && update[0]->forward_[0]->data_ == data);
        if (b_find)
        {
                skip_list_node *find_node = update[0]->forward_[0];
                for (int k = 0; k < this->max_level_; ++k)
                {
                        if (update[k]->forward_[k] == find_node)
                                update[k]->forward_[k] = find_node->forward_[k];
                        else
                                break;
                }
                delete []find_node->forward_;
                delete find_node;
                --this->size_;
        }

        return b_find ? 0 : -1;
}


int skip_list::size() const
{
        return this->size_;
}


int skip_list::rand_level()
{
        int level = 1;
        while ((::rand() & 0xffff) < (0xffff >> 2))
                ++level;

        return level;
}


test.cpp


#include "skip_list.h"


#include <sys/time.h>
#include <stdint.h>
#include <stdlib.h>
#include <iostream>
#include <set>


static int64_t current_ms();
int main(int argc, char *argv[])
{
        if (argc != 2)
                return 0;


        const int num = ::atoi(argv[1]);


        int64_t t1 = current_ms();
        skip_list *sl = new skip_list;
        for (int i = 0; i < num; ++i)
                if (sl->insert(i) != 0)
                        std::cout << "skip_list insert " << i << " failed." << std::endl;
        int64_t t2 = current_ms();
        std::cout << "skip_list insert used " << t2 - t1 << " ms" << std::endl;
        int64_t t3 = current_ms();
        for (int i = 0; i < num; ++i)
                if (!sl->find(i))
                        std::cout << "skip_list not found " << i << std::endl;
        int64_t t4 = current_ms();
        std::cout << "skip_list find used " << t4 - t3 << " ms" << std::endl;
        delete sl;


        std::set<int> si;
        int64_t t5 = current_ms();
        for (int i = 0; i < num; ++i)
                si.insert(i);
        int64_t t6 = current_ms();
        std::cout << "rb_tree insert used " << t6 - t5 << " ms" << std::endl;
        int64_t t7 = current_ms();
        for (int i = 0; i < num; ++i)
                if (si.find(i) == si.end())
                        std::cout << "rb_tree not found " << i << std::endl;
        int64_t t8 =current_ms();
        std::cout << "rb_tree find used " << t8 - t7 << " ms" << std::endl;
        return 0;
}


static int64_t current_ms()
{
        struct timeval tv;
        ::gettimeofday(&tv, NULL);
        return int64_t(tv.tv_sec) * 1000 + tv.tv_usec / 1000;

}

### Skiplist 数据结构概述 Skiplist 是一种基于概率的数据结构,用于高效地实现有序集合的操作。它通过构建多层链表的方式,在保持数据有序的同时支持快速的插入、删除和查找操作。每层链表中的节点数量逐级减少,从而形成类似于二分查找的效果。 在 Redis 中,Skiplist 被用来实现 `zset`(sorted set),即有序集合对象的一部分[^1]。具体来说,一个 `zset` 同时包含一个字典和一个跳跃表,其中字典负责映射成员与其分数的关系,而跳跃表则维护按分数排序的成员序列。 以下是关于如何实现或使用 Skiplist 的详细介绍: --- ### Skiplist 的基本组成 #### 1. **节点定义** 每个节点通常包含以下几个字段: - 成员值(member) - 分数值(score),表示成员的权重 - 下一层指向相同位置的指针数组(level) ```c typedef struct skiplistNode { char *member; // 成员名称 double score; // 成员对应的分数 struct skiplistLevel { struct skiplistNode *forward; // 指向下一层的指针 unsigned int span; // 当前跨度(可选优化项) } level[]; } skiplistNode; ``` #### 2. **头结点与层数控制** 为了方便管理整个跳跃表,还需要定义一个头部节点以及一些元信息来记录当前的最大层数和其他属性。 ```c typedef struct skiplist { struct skiplistNode *header; // 头部节点 unsigned long length; // 总节点数 int level; // 最大层数 } skiplist; ``` --- ### Skiplist 的核心算法 #### 1. **随机化层数** 每次创建新节点时,都需要为其分配合适的层数。这一步骤决定了性能的关键特性——时间复杂度接近于 O(log n)。Redis 使用如下方法计算随机层数: ```c #include <stdlib.h> #define MAX_LEVEL 32 // 假设最大可能层数为32 int randomLevel(void) { int level = 1; while ((rand() & 0xFFFF) < (REDIS_SKIPLIST_P << 16)) { // REDIS_SKIPLIST_P 表示提升的概率,默认为0.25 level += 1; } return (level < MAX_LEVEL) ? level : MAX_LEVEL; } ``` #### 2. **插入操作** 插入过程分为两步:先定位合适的位置并更新路径上的所有前置节点;再根据随机化的层数调整各级链接关系。 ```c void insert(skiplist *sl, double score, char *member) { skiplistNode *update[MAX_LEVEL]; // 记录各层需修改的前驱节点 skiplistNode *x; x = sl->header; for (int i = sl->level - 1; i >= 0; i--) { while (x->level[i].forward && compare(x->level[i].forward, member, score) < 0) { x = x->level[i].forward; } update[i] = x; // 更新第i层的前驱节点 } int new_level = randomLevel(); // 随机决定新增节点的高度 if (new_level > sl->level) { for (int i = sl->level; i < new_level; i++) { update[i] = sl->header; } sl->level = new_level; } x = createNode(new_level, score, member); // 创建新的节点 for (int i = 0; i < new_level; i++) { x->level[i].forward = update[i]->level[i].forward; update[i]->level[i].forward = x; } sl->length++; } ``` #### 3. **查找操作** 利用跳跃表的特点,从最高层开始逐步缩小范围直至找到目标元素为止。 ```c skiplistNode* find(skiplist *sl, double score, char *member) { skiplistNode *x = sl->header; for (int i = sl->level - 1; i >= 0; i--) { while (x->level[i].forward && compare(x->level[i].forward, member, score) < 0) { x = x->level[i].forward; } } x = x->level[0].forward; if (x != NULL && equal(x, member, score)) { return x; } return NULL; } ``` #### 4. **删除操作** 删除逻辑较为简单,只需沿着之前保存的路径逐一断开对应连接即可。 ```c void remove(skiplist *sl, double score, char *member) { skiplistNode *update[MAX_LEVEL]; skiplistNode *x = sl->header; for (int i = sl->level - 1; i >= 0; i--) { while (x->level[i].forward && compare(x->level[i].forward, member, score) < 0) { x = x->level[i].forward; } update[i] = x; } x = x->level[0].forward; if (x != NULL && equal(x, member, score)) { for (int i = 0; i < sl->level; i++) { if (update[i]->level[i].forward == x) { update[i]->level[i].forward = x->level[i].forward; } } freeNode(x); sl->length--; while (sl->level > 1 && sl->header->level[sl->level - 1].forward == NULL) { sl->level--; } } } ``` --- ### Skiplist 的实际应用 除了 Redis 中的 `zset` 实现外,Skiplist 还可以广泛应用于其他场景,例如分布式系统的路由表设计、日志文件的时间戳索引等。其主要优势在于能够以较低的空间代价换取较高的访问速度。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值