Redis实现skipList(跳表) 代码有详解

news2024/11/24 18:47:07

Redis实现skipList(跳表)

项目介绍

非关系型数据库redis,以及levedb,rockdb其核心存储引擎的数据结构就是跳表。

本项目就是基于跳表实现的轻量级键值型存储引擎,使用C++实现。插入数据、删除数据、查询数据、数据展示、数据落盘、文件加载数据,以及数据库大小显示。

函数提供接口

int insert_element(K,V); (插入数据)
void display_list(); (展示跳表中数据)
bool search_element(K); (搜索数据)
void delete_element(K); (删除数据)
void dump_file(); (读取数据)
void load_file(); (存放数据)
int size(); (元素数量)

跳表原理解释

什么是跳表

单链表是是一种各性能比较优秀的动态数据结构,可以支持快速的插入、删除、查找操作。
即便在有序的单链表中,插入、删除操作仍然时间复杂度为O(N),那么有没有更好的优化方法呢?
跳表是在单链表的基础上进行对数据结构的优化,将插入、删除、查找时间复杂度都控制在O(log N)。我们这主要解释跳表原理和怎么讲单链表的插入、删除操作时间复杂度降低到O(log N)

我们可以按照元素的个数(n)分成好多层,每一层都有对应元素的索引。例如第一层就是原始单链表,第一层索引个数就是n,然而到了第二层,每个元素都有50%的几率上升到第二层。(解释:咱们要求插入、删除操作都控制在O(log N),每个元素都是随机,这个时间复杂度是O(1),如果隔一元素上升一层的话,要加判定条件,就需要时间复杂度为O(N)
在这里插入图片描述
下列是按照最优操作建的图
在这里插入图片描述
在这里插入图片描述
查找元素时,就是从顶层索引开始,对于每一层,大于当前索引对应的值,小于下层对应的值,开始执行下一层的操作。

当查找元素8时,按照二级索引,找到区间[ 7-12 ],执行下一层,执行[ 7-9 ]区间。继续执行下一层,找到元素8,只需要四次操作!
在这里插入图片描述

但是现在发现如果按照单链表查找的8话也只需要5次操作,根本没有表示出跳表的强大魅力!!!
那接下来咱们试试更多元素的查找
在这里插入图片描述原始链表有64个元素,查找第60个元素,如果按照单链表查找需要60次操作,如果跳表操作的话就可以6次操作即可查找出来第60个元素,符合时间复杂度O(log N)。
这次是不是就可以体现跳表的强大能力啦!!!

分析跳表插入、删除、查找时间复杂度

插入、删除、查找在跳表中都是按照索引的方式查找,可以说三种方式几乎一样。以查找为例:
单链表元素个数为16,按照每元素50%的几率上升一层
0级(原始链表)索引数量 : 16
1级索引数量:8
2级索引数量:4
3级索引数量:2
基本可以确定索引数量按照层数以log次方递减。

分析跳表的空间复杂度

跳表的时间复杂度为O(log N),空间复杂度是拿空间换时间的操作,索引个数随着层数减半,成等比数列,空间复杂度为O(N)。

跳表索引更新

从上面插入元素8的过程中发现,我们插入8时没有更新索引,会出现 2 个索引结点之间数据非常多的情况,若频繁的插入数据,但不更新索引,最终会退化成单链表的数据结构,会导致查找数据效率变低。

跳表作为一个动态的数据结构,需要动态的维护索引与原始链表中的大小。若原始链表插入的结点变多了,那么相应的索引结点也需要增加,避免查找、删除、插入的性能下降。
其实是通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值rand,那就将这个结点添加到第一级到第rand级这rand级索引中。

代码源码

skipList.h包括函数的源码及详解

#include<iostream>
#include<cmath>
#include<cstring>
#include<mutex>
#include<fstream>

#define STORE_FILE "store/dumpFile"

std::mutex mtx;  //代表互斥锁 ,保持线程同步
std::string delimiter=":";  //存放到STORE_FILE中时,将delimiter也存入进文件中,用于get_key_value_from_string的key与value区分

template<typename K,typename V>
class Node{
public:
    Node(){}
    Node(K k,V v,int);
    ~Node();
    K get_key() const;
    V get_value() const;
    void set_value(V);

    Node <K,V> **forward;  //forward是指针数组,用于指向下一层 例如  forward[0]是指向第一层,forward[1]指向上一层
    int node_level;
private:
     K key;
     V value;
};
template<typename K,typename V>
Node<K,V>::Node(const K k, const V v, int level)
{
    this->key=k;
    this->value=v;
    this->node_level=level;
    this->forward=new Node<K,V> *[level+1];
    memset(this->forward,0,sizeof(Node<K,V>*)*(level+1));
};
template<typename  K,typename V>
Node<K,V>::~Node()
{
    delete []forward;
};
template<typename K,typename V>
K Node<K,V>::get_key() const {
    return key;
};
template<typename K,typename V>
V Node<K,V>::get_value() const {
    return value;
};
template<typename K,typename V>
void Node<K,V>::set_value(V value)
{
    this->value=value;
};
template<typename K,typename V>
class SkipList{
public:
    SkipList(int);
    ~SkipList();
    int get_random_level();
    Node<K,V>*create_node(K,V,int);
    int insert_element(K,V);
    void display_list();
    bool search_element(K);
    void delete_element(K);
    void dump_file();
    void load_file();
    int size();
private:
    void get_key_value_from_string(const std::string &str,std::string*key,std::string *value);
    bool is_valid_string(const std::string &str);
private:
    int _max_level;              //跳表的最大层级
    int _skip_list_level;        //当前跳表的有效层级
    Node<K,V> *_header;          //表示跳表的头节点
    std::ofstream _file_writer;  //默认以输入(writer)方式打开文件。
    std::ifstream _file_reader;  //默认以输出(reader)方式打开文件。
    int _element_count;          //表示跳表中元素的数量
};

//create_node函数:根据给定的键、值和层级创建一个新节点,并返回该节点的指针
template<typename K,typename V>
Node<K,V> *SkipList<K,V>::create_node(const K k, const V v, int level)
{
    Node<K,V>*n=new Node<K,V>(k,v,level);
    return n;
}

//insert_element 函数:插入一个新的键值对到跳表中。通过遍历跳表,找到插入位置,并根据随机层级创建节点。
//如果键已存在,则返回 1,表示插入失败;否则,插入成功,返回 0。
template<typename K,typename V>
int SkipList<K,V>::insert_element(const K key,const  V value)
{
    mtx.lock();
    Node<K,V> *current=this->_header;
    Node<K,V> *update[_max_level];
    memset(update,0,sizeof(Node<K,V>*)*(_max_level+1));
      //99-113行-为查找key是否在跳表中出现,也可以直接调用search_element(K key)
    for(int i=_skip_list_level;i>=0;i--)
    {
        while(current->forward[i]!=NULL&&current->forward[i]->get_key()<key)
        {
            current=current->forward[i];
        }
        update[i]=current;   //update是存储每一层需要插入点节点的位置
    }
    current=current->forward[0];
    if(current!=NULL&&current->get_key()==key)
    {
        std::cout<<"key:"<<key<<",exists"<<std::endl;
        mtx.unlock();
        return 1;
    }

    //添加的值没有在跳表中
    if(current==NULL||current->get_key()!=key)
    {
        int random_level=get_random_level();
        if(random_level>_skip_list_level)
        {
            for(int i=_skip_list_level+1;i<random_level+1;i++)
            {
                update[i]=_header;
            }
            _skip_list_level=random_level;
        }
        Node<K,V>*inserted_node= create_node(key,value,random_level);
        for(int i=0;i<random_level;i++)
        {
            inserted_node->forward[i]=update[i]->forward[i];  //跟链表的插入元素操作一样
            update[i]->forward[i]=inserted_node;
        }
        std::cout<<"Successfully inserted key:"<<key<<",value:"<<value<<std::endl;
        _element_count++;
    }
    mtx.unlock();
    return 0;
}

//display_list函数:输出跳表包含的内容、循环_skip_list_level(有效层级)、从_header头节点开始、结束后指向下一节点
template<typename K,typename V>
void SkipList<K,V>::display_list()
{
    std::cout<<"\n*****SkipList*****"<<"\n";
    for(int i=0;i<_skip_list_level;i++)
    {
        Node<K,V>*node=this->_header->forward[i];
        std::cout<<"Level"<<i<<":";
        while(node!=NULL)
        {
            std::cout<<node->get_key()<<":"<<node->get_value()<<";";
            node=node->forward[i];
        }
        std::cout<<std::endl;
    }
}

//dump_file 函数:将跳跃表的内容持久化到文件中。遍历跳跃表的每个节点,将键值对写入文件。
//其主要作用就是将跳表中的信息存储到STORE_FILE文件中,node指向forward[0],每一次结束后再将node指向node.forward[0]。
template<typename K,typename V>
void SkipList<K,V>::dump_file()
{
    std::cout<<"dump_file-----------"<<std::endl;
    _file_writer.open(STORE_FILE);
    Node<K,V>*node=this->_header->forward[0];
    while(node!=NULL)
    {
        _file_writer<<node->get_key()<<":"<<node->get_value()<<"\n";
        std::cout<<node->get_key()<<":"<<node->get_value()<<"\n";
        node=node->forward[0];
    }
    _file_writer.flush();  //设置写入文件缓冲区函数
    _file_writer.close();
    return ;
}

//将文件中的内容转到跳表中、每一行对应的是一组数据,数据中有:分隔,还需要get_key_value_from_string(line,key,value)将key和value分开。
//直到key和value为空时结束,每组数据分开key、value后通过insert_element()存到跳表中来
template<typename K,typename V>
void SkipList<K,V>::load_file()
{
    _file_reader.open(STORE_FILE);
    std::cout<<"load_file----------"<<std::endl;
    std::string line;
    std::string *key=new std::string();
    std::string *value=new std::string();
    while(getline(_file_reader,line))
    {
        get_key_value_from_string(line,key,value);
        if(key->empty()||value->empty())
        {
            continue;
        }
        int target=0;
        std::string str_key=*key;   //当时定义的key为int类型,所以将得到的string类型的 key转成int
        for(int i=0;i<str_key.size();i++)
        {
            target=target*10+str_key[i]-'0';
        }
        int Yes_No=insert_element(target,*value);
        std::cout<<"key:"<<*key<<"value:"<<*value<<std::endl;
    }
    _file_reader.close();
}

//表示跳表中元素的数量
template<typename K,typename V>
int SkipList<K,V>::size() {
    return _element_count;
}

//从STORE_FILE文件读取时,每一行将key和value用 :分开,此函数将每行的key和value分割存入跳表中
template<typename K,typename V>
void SkipList<K,V>::get_key_value_from_string(const std::string &str, std::string *key, std::string *value)
{
    if(!is_valid_string(str)) return ;
    *key=str.substr(0,str.find(delimiter));
    *value=str.substr(str.find(delimiter)+1,str.length());
}

//判断从get_key_value_from_string函数中分割的字符串是否正确
template<typename K,typename V>
bool SkipList<K,V>::is_valid_string(const std::string &str)
{
    if(str.empty())
    {
        return false;
    }
    if(str.find(delimiter)==std::string::npos)
    {
        return false;
    }
    return true;
}

//遍历跳表找到每一层需要删除的节点,将前驱指针往前更新,遍历每一层时,都需要找到对应的位置
//前驱指针更新完,还需要将全为0的层删除
template<typename K,typename V>
void SkipList<K,V>::delete_element(K key)
{
    mtx.lock();
    Node<K,V>*current=this->_header;
    Node<K,V>*update[_max_level+1];
    memset(update,0,sizeof(Node<K,V>*)*(_max_level+1));
    for(int i=_skip_list_level;i>=0;i--)
    {
        while(current->forward[i]!=NULL&&current->forward[i]->get_key()<key)
        {
            current=current->forward[i];
        }
        update[i]=current;
    }
    current=current->forward[0];
    if(current!=NULL&&current->get_key()==key)
    {
        for(int i=0;i<=_skip_list_level;i++) {
            if (update[i]->forward[i] != current) {
                break;
            }
            update[i]->forward[i] = current->forward[i];
        }
            while(_skip_list_level>0&&_header->forward[_skip_list_level]==0)
            {
                _skip_list_level--;
            }
            std::cout<<"Successfully deleted key"<<key<<std::endl;
            _element_count--;
        }
        mtx.unlock();
        return ;
}

//遍历每一层,从顶层开始,找到每层对应的位置,然后进入下一层开始查找,直到查找到对应的key
//如果找到return true 输出Found  否则 return false ,输出Not Found
template<typename K,typename V>
bool SkipList<K,V>::search_element(K key)
{
    std::cout<<"search_element------------"<<std::endl;
    Node<K,V> *current=_header;
    for(int i=_skip_list_level;i>=0;i--)
    {
        while(current->forward[i]&&current->forward[i]->get_key()<key)
        {
            current=current->forward[i];
        }
    }
    current=current->forward[0];
    if(current and current->get_key()==key)
    {
        std::cout<<"Found key:"<<key<<",value:"<<current->get_value()<<std::endl;
        return true;
    }
    std::cout<<"Not Found Key:"<<key<<std::endl;
    return false;
}
template<typename K,typename V>

SkipList<K,V>::SkipList(int max_level)
{
    this->_max_level=max_level;
    this->_skip_list_level=0;
    this->_element_count=0;
    K k;
    V v;
    this->_header=new Node<K,V>(k,v,_max_level);
};
//释放内存,关闭_file_writer  _file_reader
template<typename K,typename V>
SkipList<K,V>::~SkipList()
{
    if(_file_writer.is_open())
    {
        _file_writer.close();
    }
    if(_file_reader.is_open())
    {
        _file_reader.close();
    }
    delete _header;
}
//生成一个随机层级。从第一层开始,每一层以 50% 的概率加入
template<typename K,typename V>
int SkipList<K,V>::get_random_level()
{
    int k=1;
    while(rand()%2)
    {
        k++;
    }
    k=(k<_max_level)?k:_max_level;
    return k;
};


main函数负责函数的调用测数据测试。

//所有函数的解释与用法都在skiplist.h中,main主函数主要用于测试各种函数是否可行

#include <iostream>
#include "skiplist.h"
#define FILE_PATH "./store/dumpFile"
int main()
{
    SkipList<int ,std::string>skipList(6);
    skipList.insert_element(1,"学习");
    skipList.insert_element(3,"跳表");
    skipList.insert_element(7,"去找");
    skipList.insert_element(8,"GitHub:");
    skipList.insert_element(9,"shy2593666979");
    skipList.insert_element(19,"赶紧给个");
    skipList.insert_element(19,"star!");
    std::cout<<"skipList.size = "<<skipList.size()<<std::endl;
    skipList.dump_file();
    skipList.search_element(8);
    skipList.search_element(9);
    skipList.display_list();
    skipList.delete_element(3);
    skipList.load_file();
    std::cout<<"skipList.size = "<<skipList.size()<<std::endl;
    skipList.display_list();
}

下载源码地址

Github下载地址(国际网站)

Gitee下载地址 (国内网站)

参考资料

https://github.com/youngyangyang04/Skiplist-CPP
https://juejin.cn/post/7149101822756519949

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/760851.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java-API简析_java.lang.Runtime类(基于 Latest JDK)(浅析源码)

【版权声明】未经博主同意&#xff0c;谢绝转载&#xff01;&#xff08;请尊重原创&#xff0c;博主保留追究权&#xff09; https://blog.csdn.net/m0_69908381/article/details/131714695 出自【进步*于辰的博客】 因为我发现目前&#xff0c;我对Java-API的学习意识比较薄弱…

【GESP】2023年06月图形化四级 -- 按身高排序

按身高排序 【题目描述】 默认小猫角色和白色背景。有两个列表,第一个列表“names”存储名字,第二个列表“heights”存储这组名字对应的身高,这些身高由互不相同的正整数组成。 请按身高由大到小排序,同时能够得到对应名字的列表“names”。 例如: 名字列表:names = …

变压器试验交流耐压

试验目的 交流耐压试验是鉴定电力设备绝缘强度最有效和最直接的方法。 电力设备在运行中&#xff0c; 绝缘长期受着电场、 温度和机械振动的作用会逐渐发生劣化&#xff0c; 其中包括整体劣化和部分劣化&#xff0c;形成缺陷&#xff0c; 例如由于局部地方电场比较集中或者局部…

unity 调用C++ dll 操作升级套娃函数调用

之前一直以为C生成dll&#xff0c;在unity中调用时要把传出去的值设置在主函数中&#xff0c;以参数或反回值的形式。 当然在DLL工程中可以说没有主函数&#xff0c;那个可以运行一个函数&#xff0c;其会调用其他函数从而一直调其他相关函数。 那问题是在层级是二或三------…

Android CoroutineScope Dispatchers.Main主线程delay,kotlin

Android CoroutineScope Dispatchers.Main主线程delay&#xff0c;kotlin import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import android.view.View import android.view.View.OnClickListener import android.widget.Bu…

【sgWaterfall】Vue实现图文瀑布流布局模式,图片预加载显示占位区域阴影,加载完成后向上浮动动画出现,支持不同浏览器尺寸宽度下自适应显示列数

特性&#xff1a; 自动计算每个图片最佳坐标位置&#xff0c;以达到最新加在图片占据位置尽量让整体更加协调图片预加载显示占位区域阴影加载完成后向上浮动动画出现支持不同浏览器尺寸宽度下自适应显示列数 Vue实现图文瀑布流布局模式&#xff0c;图片预加载显示占位区域阴影&…

SQL语法与数据库快速入门(2)

目录&#xff1a; 多表简介SQL 约束-外键约束多表关系简介多表查询多表查询-内连接查询多表查询-外连接查询子查询简介子查询实战数据库进阶redis 内存数据库mongodb nosql 数据库neo4j 图数据库 1.多表简介 多表及使用场景介绍&#xff1a; 多表顾名思义就是在数据库设计中…

【NacosSync】注册中心同步组件使用教程ZooKeeper迁移到Nacos

目录 介绍系统模块架构获取安装包数据库配置启动服务器控制台添加注册中心集群信息添加同步任务注意事项 介绍 NacosSync是一个支持多种注册中心的同步组件,基于Spring boot开发框架,数据层采用Spring Data JPA,遵循了标准的JPA访问规范,支持多种数据源存储,默认使用Hibernate…

【论文阅读】《Distilling the Knowledge in a Neural Network》

【论文阅读】《Distilling the Knowledge in a Neural Network》 推荐指数&#xff1a; 1. 动机 &#xff08;1&#xff09;虽然一个ensemble的模型可以提升模型的效果&#xff0c;但是在效率方面实在难以接受&#xff0c;尤其是在每个模型都是一个大型的网络模型的时候。 &…

《TCP IP网络编程》第五章

第5章 基于 TCP 的服务端/客户端&#xff08;2&#xff09; 5.1 回声客户端的完美实现 先回顾一下服务器端的 I/O 相关代码&#xff1a; //持续接收客户端发送的数据&#xff0c;并将数据原样发送回客户端&#xff0c;直到客户端关闭连接。 while ((str_len read(clnt_sock,…

CMS垃圾收集器三色标记-JVM(十二)

上篇文章说了CMS垃圾收集器是赋值清除&#xff0c;所以他不可以碎片整理&#xff0c;于是jvm支持两个参数&#xff0c;几次fullGC之后碎片整理压缩空间。Cms他会抢占cpu资源&#xff0c;因为是并行运行&#xff0c;所以会有浮动垃圾。还有执行不确定性&#xff0c;垃圾收集完&a…

Python爬虫学习笔记(三)————urllib

目录 1.使用urllib来获取百度首页的源码 2.下载网页图片视频 3.总结-1 4.请求对象的定制&#xff08;解决第一种反爬&#xff09; 5.编解码 &#xff08;1&#xff09;get请求方式&#xff1a;urllib.parse.quote&#xff08;&#xff09; &#xff08;2&#xff09;get请求…

深度学习——RNN解决回归问题

详细代码与注释 import torch from torch import nn import numpy as np import matplotlib.pyplot as plt# 有利于复现代码 # torch.manual_seed(1) # reproducible# Hyper Parameters TIME_STEP 10 # rnn time step # 输入sin函数的y值&#xff0c;所以输入尺寸为1 INP…

posix ipc之消息队列

note 1.mq_open函数的参数pathname应以/开始&#xff0c;且最多一个/ 2.mq_receive的参数msg_len应大于等于attr.msgsize 3.消息队列写方写时不要求读方就绪&#xff0c;读方读时不要求写方就绪(和管道不同) code #include <fcntl.h> #include <sys/stat.h> #…

汽车销售数据可视化分析实战

1、任务 市场需求&#xff1a;各年度汽车总销量及环比&#xff0c;各车类、级别车辆销量及环比 消费能力/价位认知&#xff1a;车辆销售规模及环比、不同价位车销量及环比 企业/品牌竞争&#xff1a;各车系、厂商、品牌车销量及环比&#xff0c;市占率及变化趋势 热销车型&…

x86架构ubuntu22下运行SFC模拟器zsnet

0. 环境 ubuntu22 1. apt安装 sudo apt install zsnes 2. 运行 zsnet 参考&#xff1a;在Ubuntu上用zsnes玩SFC游戏&#xff0c;https://blog.csdn.net/gqwang2005/article/details/3877121

MyBatis学习笔记之首次开发及文件配置

文章目录 MyBatis概述框架特点 有关resources目录开发步骤从XML中构建SqlSessionFactoryMyBatis中有两个主要的配置文件编写MyBatis程序关于第一个程序的小细节MyBatis的事务管理机制JDBCMANAGED 编写一个较为完整的mybatisjunit测试mybatis集成日志组件 MyBatis概述 框架 在…

win11 系统暂无可用音频设备导致播放失败/音频服务未响应

win11 系统暂无可用音频设备导致播放失败/音频服务未响应 win11再一次更新后音频突然用不了了&#xff0c;驱动和输出设备都显示正常&#xff0c;但每次播放就会出现下面的问题&#xff0c;重启和更新驱动也没用。最后百度了好久终于解决了。 最后发现可能是新的驱动和电脑不兼…

【C++11】function包装器的简单使用

function 1 function包装器使用场景2 包装器3 包装成员函数4 一道例题5 包装器的意义 1 function包装器使用场景 现在有代码如下&#xff1a; 要求声明出这两个函数的类型 int f(int a,int b) {return a b; } struct Functor {int operator(int a,int b){return a b;} }可以…

(atan2)+(最小回文字符统计)

C - Nearest vectors long double eps1e-18 atan2:x正轴旋转的弧度角&#xff0c;y>0为正&#xff0c;y<0为负const long double PIacos(-1.0); const long double eps1e-18; struct node {ll id;long double x,y,z; }t[NN]; bool cmp1(node l,node r) {return l.zep…