Redis は詳細な説明付きの SkipList (スキップ リスト) コードを実装します

RedisはskipList(スキップリスト)を実装します

プロジェクト紹介

非リレーショナル データベース redis、leveledb、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 ですが、2 番目の層では、各要素が 50% の確率で 2 番目の層に上昇します。(説明: 挿入と削除の操作は O(log N) で制御する必要があり、各要素はランダムで、時間計算量は O(1) です。必要な時間計算量は O(N) です。) 以下はグラフです
ここに画像の説明を挿入
。最適な操作に従って構築されます
ここに画像の説明を挿入
ここに画像の説明を挿入
。要素の検索は、最上位のインデックスから開始されます。各レベルで、現在のインデックスに対応する値より大きく、下位のレベルに対応する値より小さい値が実行されます。次の 1 層の操作。

要素 8 を検索する場合、セカンダリ インデックスに従って区間 [7-12] を見つけ、次の層を実行して、区間 [7-9] を実行します。次のレベルに進み、わずか 4 回の操作で要素 8 を見つけてください。
ここに画像の説明を挿入

しかし、単結合リストに従って 8 つの単語を検索すると、必要な操作は 5 つだけであることがわかりました。これでは、リストをジャンプする強力な魅力がまったく示されていません。
次に、さらに要素を検索してみます。
ここに画像の説明を挿入元のリンク リストには 64 個の要素があり、60 番目の要素が検索されます。単一のリンク リストに従って検索すると、60 回の操作が必要です。テーブルをスキップすると、6 回の操作が可能になります。時間計算量 O(log N) を満たす 60 番目の要素を見つけるために使用されます。
今度はジャンプ台の強力な能力を反映できるか!

スキップテーブルの挿入、削除、検索の時間計算量を分析する

挿入、削除、検索はいずれもジャンプテーブルのインデックスをもとに検索​​を行うため、3つの方法はほぼ同じと言えます。検索の例:
単一リンクリストの要素数は 16 で、各要素は
50% の確率に従ってレベル 0 (元のリンクリスト) に引き上げられます インデックスの数: 16
第 1 レベルの数インデックス数: 8
第 2 レベルのインデックス数: 4
第 3 レベルのインデックス数: 2
基本的に、インデックス数は層数に応じて対数累乗で減少することがわかります。

スキップリストの空間複雑性の分析

ジャンプテーブルの時間計算量はO(log N)、空間計算量は空間と時間を交換する演算であり、インデックス数は層数で半分になり、空間計算量はO(N)となる。

テーブルインデックスの更新をスキップする

要素 8 を挿入する上記のプロセスから、8 を挿入するとインデックスが更新されず、2 つのインデックス ノード間に大量のデータが存在することがわかりました。データを頻繁に挿入してもインデックスを更新しない場合、最終的には単一のリンク リストに縮退してしまいます。データ構造はデータ検索の効率の低下につながります。

動的データ構造として、ジャンプ リストはインデックスと元のリンク リストのサイズを動的に維持する必要があります。元のリンク リストに挿入されるノードの数が増加する場合、検索、削除、挿入のパフォーマンスの低下を避けるために、対応するインデックス ノードも追加する必要があります。
実際、ランダム関数は、このノードを挿入するインデックスのレベルを決定するために使用されます。たとえば、ランダム関数が rand の値を生成する場合、このノードは、最初のレベルから 2 番目のレベルまでの 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 ダウンロード アドレス(海外 Web サイト)

Giteeダウンロードアドレス(国内サイト)

参考文献

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

おすすめ

転載: blog.csdn.net/m0_63743577/article/details/131748260