skiplist-cpp

0. 优化空间

  1. 增加Score权值
  2. 使用了 C++11 智能指针,实现了自动管理动态分配的内存
  3. 每次创建节点时,是随机生成层数,改为根据幂次定律(越大的数出现的概率越小)随机生成层数
  4. 增加异步日志系统。

1. Node类

1.1 定义

Node类中有以下几个成员函数

  1. Key and Value
  2. node_level:当前节点的层数
  3. forward:每个节点的forward成员函数是一个指向Node的指针数组,作用:Node_A->forward[x]保存的是当前节点A的第x层的下一个Node指针。
template<typename K, typename V> 
class Node {
// ~ Node 节点定义
public:

    Node() {}            //  默认构造函数

    Node(K k, V v, int); // 有参构造函数

    ~Node();             // 析构函数

    K get_key() const;   // 取 Key

    V get_value() const; // 取 value

    void set_value(V);   // 设定 value

    // 前向指针数组,对于不同的层,可能指向不同节点
    Node<K, V> **forward; // 前向指针数组,内部存放前向指针,指向下一个 Node

    int node_level;      // 节点层数

private:
    K key;
    V value;
};

1.2 构造函数

Node类的构造函数主要负责:

  1. 给出Key and Value, level。
  2. 根据level,初始化forward指针数组。
//构造函数
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; 

    // level + 1, because array index is from 0 - level
    this->forward = new Node<K, V>*[level+1];//创建一个保存Node*的数组,长度为level+1
    
	// 初始化
    memset(this->forward, 0, sizeof(Node<K, V>*)*(level+1));
};

//析构函数
template<typename K, typename V> 
Node<K, V>::~Node() {
    delete []forward;
};

2. SkipList类

1.1 定义

SkipList类代表的是整个跳表,类中有个最重要的元素:_header:一个指向Node节点的指针,为头节点指针。

SkipList负责跳表的增删改查、落盘工作
所提供的接口 :

  • insert_element 插入数据
  • delete_element 删除数据
  • search_element 查询数据
  • update_element 更新数据
  • display_list 打印跳跃表
  • dump_file 数据落盘
  • load_file 加载数据
  • size 返回数据规模
  • clear 清空跳表(新加)
template <typename K, typename V> 
class SkipList {

public:
    // 构造和析构函数,创建 Node 节点函数
    SkipList(int);
    ~SkipList();
    Node<K, V>* create_node(K, V, int);

    //获得随即层高
    int get_random_level();

    int insert_element(K, V);//增
    bool search_element(K);//查
    void delete_element(K);//删

    void display_list();//打印跳表

    // 数据落盘和数据加载
    void dump_file();
    void load_file();

    int size();

private:
    // 数据加载相关函数, 用来区分 key 和 value
    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;  // 当前跳表的最高层
    int _element_count;    // 跳表中节点数
    Node<K, V> *_header;   // 跳表中头节点指针


    // file operator
    std::ofstream _file_writer;
    std::ifstream _file_reader;

};

1.2 构造函数、析构函数、创建节点函数

构造函数负责:

  1. 给出整个跳表所允许的最大层数
  2. 创建一个头节点,头节点的层数为所允许的最大层数。(头节点不保存数据)
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;

    // create header node and initialize key and value to null
    K k;
    V v;
    this->_header = new Node<K, V>(k, v, _max_level);
};

析构函数负责:内存回收(注意:原项目中没有释放内存)

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;
}

创建新的节点用到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;
}

1. 3 insert_element()----插入

插入数据:将数据插入跳表中,并为其随机生成层数
插入数据的整体步骤:

  1. 先加锁。
  2. 使用一个指向Node的指针current来负责遍历整个跳表。
  3. 初始化一个指向Node的指针数组update,作用:update[x]保存的是第x层最后一个符合要求的节点。要求:只要当前节点的下一节点非空,且 key 小于目标。
  4. current从那个最高层开始遍历,保存每一层的Node指针到update数组中
  5. 判断所插入的元素是否存在在表中,如果存在则返回1,插入失败;
  6. 如果不存在:开始为该节inserted_node点生成随机层数level。
  7. 如果新添加的节点层高level大于当前跳表层高,则需要更新 update 数组,将原本[_skip_list_level, random_level]范围内的NULL改为_header,
  8. 得到最终确定的层高后,就可以创建节点,挨层进行插入:根据刚刚所保存的update数组,将该节点的forward[i]=update->forward[i]update->forward[i]=inserted_node
    注意:每个节点的forward成员函数是一个指向Node的指针数组,作用:Node_A->forward[x]保存的是当前节点A的第x层的下一个Node指针。
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; // 从头节点遍历

    // update 是一个指针数组,数组内存放指针,指向 node 节点,其索引代表的是层数。
    // update[x]保存的是第x层最后一个符合要求的节点(要求:只要当前节点的下一节点非空,且 key 小于目标)
    Node<K, V> *update[_max_level + 1];  // update 的大小 >= forward
    memset(update, 0, sizeof(Node<K, V>*)*(_max_level + 1)); // 初始化

    // 从最高层开始遍历
    for(int i = _skip_list_level; i >= 0; i--) {
        // 只要当前节点的下一节点非空,且 key 小于目标, 就会向后遍历
        while(current->forward[i] != NULL && current->forward[i]->get_key() < key) {
            current = current->forward[i];  // 节点向后移动,这里你就把forward[i]理解为next就行
        }
        update[i] = current;  // update[i] 记录当前层最后符合要求的节点
    }

    // 遍历到 level 0 说明到达最底层了,forward[0]指向的就是跳表下一个邻近节点
    current = current->forward[0];
    // 注意此时 current->get_key() >= key !!!

    // 1. 插入元素已经存在。插入失败!
    if (current != NULL && current->get_key() == key) {
        std::cout << "key: " << key << ", exists" << std::endl;
        mtx.unlock();
        return 0;  // 插入元素已经存在,返回 -1,插入失败
    }
    // 2. 如果当前 current 不存在,或者 current->get_key > key。则可以插入
    if (current == NULL || current->get_key() != key ) {
        // 随机生成层的高度,也即 forward[] 大小
        int random_level = get_random_level();

        // 如果新添加的节点层高大于当前跳表层高,则需要更新 update 数组
        // 将原本[_skip_list_level random_level]范围内的NULL改为_header
        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);

        // 该操作等价于:
        // new_node->next = pre_node->next;
        // pre_node->next = new_node; 只不过是逐层进行
        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;  // 返回 0,插入成功
}

1.4 delete_element----删除

如果理解了插入,那么删除也很简单:

  1. 自顶向下找到要删除节点的前一个节点,并保存在指针数组update中。注:update[x]第x层保存要删除节点的前一个节点
  2. 自底向上将跳表重新连接
template<typename K, typename V>
int SkipList<K, V>::delete_element(K key) {
    // 操作同 insert_element
    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];

    // 1. 非空,且 key 为目标值
    if (current != NULL && current->get_key() == key) {
        // 从最底层开始删除 update->forward 指向的节点,即目标节点
        for (int i = 0; i <= _skip_list_level; i++) {
            // 如果 update[i]->forward[i] 已经不指向 current,说明 i 的上层也不会指向 current
            // 也说明了被删除节点层高 i - 1。直接退出循环即可
            if (update[i]->forward[i] != current)
                break;
            // 删除操作,等价于 node->next = node->next->next
            update[i]->forward[i] = current->forward[i];
        }
        // 因为可能删除的元素它的层数恰好是当前跳跃表的最大层数
        // 所以此时需要重新确定 _skip_list_level,通过头节点判断
        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 0; // 返回值 0 说明成功删除
    }
        // 2. 笔者添加了没有该键时的情况,打印输出提示
    else {
        std::cout << key << " is not exist, please check your input !\n";
        mtx.unlock();
        return -1; // 返回值 -1 说明没有该键值
    }
}

1.5 search_element----查找数据

从最顶层开始遍历,找到最底层中,最后一个满足小于key的节点

template<typename K, typename V>
bool SkipList<K, V>::search_element(K key) {
    std::cout << "search_element..." << std::endl;
    Node<K, V> *current = _header;
    // 从最高层开始遍历,找到最底层中最后一个满足小于key的节点
    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]; // 该操作后 current->get_key >= key 或者 null
    // 找到
    if (current != NULL && 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;
}

1.6 display_list----打印跳表

template<typename K, typename V>
void SkipList<K, V>::display_list() {
    std::cout << "\n******** Skip List ********"<<"\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;
    }
}

1.7 落盘、加载及其辅助函数

落盘负责:将数据导入磁盘
这里STORE_FILE是我们预定义的文件落盘位置

// 数据落盘到磁盘文件
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) {
        // 文件写入(key value 以 : 为分隔符),及信息打印
        _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 ;
}

加载负责:将数据从磁盘中导入内存,但是层数关系会发生变化

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;
    // key 与 value 是一个指向 string 对象的指针
    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;
        }
        // 重新载入过程使用 insert_element()
        // 所以层之间的关系(各节点的层高)可能发生变化, 所以与之前的SkipList不同
        insert_element(*key, *value);
        std::cout << "key:" << *key << "value:" << *value << std::endl;
    }
    _file_reader.close();
}

辅助函数:get_key_value_from_string()主要用于在从磁盘中加载数据时,区分出key and value

//辅助函数

// Get current SkipList size 
template<typename K, typename V> 
int SkipList<K, V>::size() { 
    return _element_count;
}

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, 分隔符之后的为 value
    *key = str.substr(0, str.find(delimiter));
    *value = str.substr(str.find(delimiter) + 1, str.length());
}

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;
}

1.8 clear----清空跳表

template<typename K, typename V> 
void SkipList<K, V>::clear() { 
    std::cout << "clear ..." << std::endl;
    Node<K, V> *node = this->_header->forward[0]; 
    // 删除节点
    while (node != NULL) {
        Node<K, V> *temp = node;
        node = node->forward[0];
        delete temp;
    }
    // 重新初始化 _header
    for (int i = 0; i <= _max_level; i++) {
        this->_header->forward[i] = 0;
    }
    this->_skip_list_level = 0;
    this->_element_count = 0;
}

3. 写跳表项目途中所学习的知识点

1. 流的相关知识

    std::ofstream _file_writer;
    std::ifstream _file_reader;

2. include <mutex>

mutex和之前用的有什么区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值