c++如何实现跳表(skiplist)
作者:evenleo 发布时间:2022-02-10 18:05:58
标签:c++,跳表,skiplist
引言
二分查找底层依赖的是数组随机访问的特性,所以只能用数组来实现。如果数据存储在链表中,就真的没法用二分查找算法了吗?实际上,只需要对链表稍加改造,就可以支持类似“二分”的查找算法。改造之后的数据结构叫作跳表。
定义
跳表是一个随机化的数据结构。它允许快速查询一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(log n),优于普通队列的O(n)。性能上和红黑树,AVL树不相上下,但跳表的原理非常简单,目前Redis和LevelDB中都有用到。
跳表是一种可以替代平衡树的数据结构。跳表追求的是概率性平衡,而不是严格平衡。因此,跟平衡二叉树相比,跳表的插入和删除操作要简单得多,执行也更快。
C++简单实现
下面实现过程主要是简单实现跳表的过程,不是多线程安全的,LevelDB实现的跳表支持多线程安全,用了std::atomic原子操作,本文主要是为了理解跳表的原理,所以采用最简单的实现。
#ifndef SKIPLIST_H
#define SKIPLIST_H
#include <ctime>
#include <initializer_list>
#include <iostream>
#include <random>
template <typename Key>
class Skiplist {
public:
struct Node {
Node(Key k) : key(k) {}
Key key;
Node* next[1]; // C语言中的柔性数组技巧
};
private:
int maxLevel;
Node* head;
enum { kMaxLevel = 12 };
public:
Skiplist() : maxLevel(1)
{
head = newNode(0, kMaxLevel);
}
Skiplist(std::initializer_list<Key> init) : Skiplist()
{
for (const Key& k : init)
{
insert(k);
}
}
~Skiplist()
{
Node* pNode = head;
Node* delNode;
while (nullptr != pNode)
{
delNode = pNode;
pNode = pNode->next[0];
free(delNode); // 对应malloc
}
}
// 禁止拷贝构造和赋值
Skiplist(const Skiplist&) = delete;
Skiplist& operator=(const Skiplist&) = delete;
Skiplist& operator=(Skiplist&&) = delete;
private:
Node* newNode(const Key& key, int level)
{
/*
* 开辟sizeof(Node) + sizeof(Node*) * (level - 1)大小的空间
* sizeof(Node*) * (level - 1)大小的空间是给Node.next[1]指针数组用的
* 为什么是level-1而不是level,因为sizeof(Node)已包含一个Node*指针的空间
*/
void* node_memory = malloc(sizeof(Node) + sizeof(Node*) * (level - 1));
Node* node = new (node_memory) Node(key);
for (int i = 0; i < level; ++i)
node->next[i] = nullptr;
return node;
}
/*
* 随机函数,范围[1, kMaxLevel],越小概率越大
*/
static int randomLevel()
{
int level = 1;
while (rand() % 2 && level < kMaxLevel)
level++;
return level;
}
public:
Node* find(const Key& key)
{
// 从最高层开始查找,每层查找最后一个小于key的前继节点,不断缩小范围
Node* pNode = head;
for (int i = maxLevel - 1; i >= 0; --i)
{
while (pNode->next[i] != nullptr && pNode->next[i]->key < key)
{
pNode = pNode->next[i];
}
}
// 如果第一层的pNode[0]->key == key,则返回pNode->next[0],即找到key
if (nullptr != pNode->next[0] && pNode->next[0]->key == key)
return pNode->next[0];
return nullptr;
}
void insert(const Key& key)
{
int level = randomLevel();
Node* new_node = newNode(key, level);
Node* prev[kMaxLevel];
Node* pNode = head;
// 从最高层开始查找,每层查找最后一个小于key的前继节点
for (int i = level - 1; i >= 0; --i)
{
while (pNode->next[i] != nullptr && pNode->next[i]->key < key)
{
pNode = pNode->next[i];
}
prev[i] = pNode;
}
// 然后每层将新节点插入到前继节点后面
for (int i = 0; i < level; ++i)
{
new_node->next[i] = prev[i]->next[i];
prev[i]->next[i] = new_node;
}
if (maxLevel < level) // 层数大于最大层数,更新最大层数
maxLevel = level;
}
void erase(const Key& key)
{
Node* prev[maxLevel];
Node* pNode = head;
// 从最高层开始查找,每层查找最后一个小于key的前继节点
for (int i = maxLevel - 1; i >= 0; --i)
{
while (pNode->next[i] != nullptr && pNode->next[i]->key < key)
pNode = pNode->next[i];
prev[i] = pNode;
}
// 如果找到key,
if (pNode->next[0] != nullptr && pNode->next[0]->key == key)
{
Node *delNode = pNode->next[0];
// 从最高层开始,如果当前层的next节点的值等于key,则删除next节点
for (int i = maxLevel - 1; i >= 0; --i)
{
if (prev[i]->next[i] != nullptr && key == prev[i]->next[i]->key)
prev[i]->next[i] = prev[i]->next[i]->next[i];
}
free(delNode); // 最后销毁pNode->next[0]节点
}
// 如果max_level>1且头结点的next指针为空,则该层已无数据,max_level减一
while (maxLevel > 1 && head->next[maxLevel] == nullptr)
{
maxLevel--;
}
}
};
#endif
Redis和LevelDB选用跳表而弃用红黑树的原因
Skiplist的复杂度和红黑树一样,而且实现起来更简单。
在并发环境下Skiplist有另外一个优势,红黑树在插入和删除的时候可能需要做一些rebalance的操作,这样的操作可能会涉及到整个树的其他部分,而skiplist的操作显然更加局部性一些,锁需要盯住的节点更少,因此在这样的情况下性能好一些。
来源:https://www.cnblogs.com/evenleee/p/13355499.html


猜你喜欢
- 1:普通实现99乘法表太简单,是个程序员都会,实现如下:package test.ms;public class Test99 {publi
- 这两天因为要做一个随机的地图生成系统,所以一直在研究随机迷宫生成算法,好吧,算是有一点小小的成果。随机迷宫生成我自己的理解简而言之分为以下几
- 前言最近在学习安卓开发的时候遇到了一个问题,使用Android Studio在为Button设置背景颜色的时候发现设置好后却在运行 * 上失
- java连续执行多条cmd命令命令之间用&连接例如:Process p = Runtime.getRuntime().exec(&q
- springcloud集成nacos遇到的问题1.获取不到配置文件信息有时候新建了配置文件后浏览器访问发现获取不到里面的值,原来spring
- 说明:以下的代码基于httpclient4.5.2实现。我们要使用java的HttpClient实现get请求抓取网页是一件比较容易实现的工
- Mybatis Criteria条件查询CriterionCriterion是最基本,最底层的Where条件,用于字段级的筛选。Criter
- 在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的。但是在通过了Ap
- 问题一次面试遇到的一个问题,其实也是实际开发中很容易遇到的问题,特此记录一下。当请求某个接口的时候,我们会在请求的header中携带toke
- 本文实例为大家分享了Android检测手机多点触摸点数的具体代码,供大家参考,具体内容如下说明:手指每点击一个地方,在那个地方就画一个圆第一
- 在android 中可以广泛看到的template<typename T> class Sp 句柄类实际上是android 为实
- 本章,会对synchronized关键字进行介绍。涉及到的内容包括:1. synchronized原理2. synchronized基本规则
- 疑问,确实像往常一样在service上添加了注解 @Transactional,为什么查询数据库时还是发现有数据不一致的情况,想想肯定是事务
- 最近有个需求,需要统计APP的在线人数,其实以前也统计过,采取的是上线发送一个请求$this->cache->incr()加1,
- 面向对象思想之封装或许大家都听说过java是纯面向对象语言,面向对象思想也就是我们常说的OOP,我们听说最多的思想就是继承,封装,多态,今天
- 现工作中有需求要进行批量新增和修改实现了以下几种方式代码中foreach insert/update多线程foreach insert/up
- @Param注解导致分页失效—分页 * 问题描述在使用mybatis分页时,使用@Param注解传入了两个对象,分页失效,查询出的总是全部的
- C语言运算符及其优先级汇总表口诀圆下箭头一顿号非凡增减富强针地长三乘除,四加减,五移位千万别把鱼忘记,它在盛饭的厨子里小灯大灯灯灯不等爸喂鱼
- 1. 编写索引内容节点解释:settings:配置信息"number_of_replicas": 0 不需要备份(单节点
- 本文实例为大家分享了android水平循环滚动控件的具体代码,供大家参考,具体内容如下CycleScrollView.javapackage