C++实现跳表:二、跳表定义

本节将深入探讨 K-V 存储引擎实现的关键组成部分:Node 类与 SkipList 类。Node 类的核心在于其属性,特别是其 forward 属性。可以说理解了 forward 属性,就理解了整个 K-V 存储引擎底层使用数据结构 —— 跳表。

而 SkipList 类的重点在于其提供的一系列公共成员函数,这些函数负责组织和管理 Node 类的实例。后续的章节将围绕这些函数进行展开,详细介绍它们的实现和应用。

1. Node 类

1.1 Node 类中的关键属性

在开发一个基于跳表的 K-V 存储引擎、设计用于实际存储数据的 Node 类(节点)时,我们需要考虑以下三个因素。
(为了明确语义以及方便叙述,后文中所有的名词「节点」都是代指 Node 类,更具体的来说是指 Node 类的实例)

  1. 键值存储机制:如何存储键和对应的值
  2. 跳转机制实现:跳表的搜索操作核心在于节点间的跳转,这如何实现
  3. 层级确定:如何确定节点存在于跳表中的哪些层级

针对上述第一点和第三点考虑因素,Node 类需要包含两个主要私有属性:key 和 value,分别用于存储键和值。此外,Node 类还有一个 node_level 公共属性,用于标识节点在跳表中的层级位置。

  • 当 node_level = 1 时,表示当前的节点只会出现在跳表中的第 1 层
  • 当 node_level = 2 时,表示当前的节点会出现在跳表中的第 2 层和第 1 层
  • 以此类推

此时,Node 类的结构如下:

template <typename K, typename V>
class Node {
public:
    int node_level;
private:
    K key;
    V value;
}

在介绍了 Node 类中负责键值存储和标识节点层次的属性之后,下文将会介绍用于支持节点间跳转机制的属性。(所谓跳转,指的是通过定义一种特定的指针机制,使得该指针能够以一定规则指向跳表中的各个节点。以单链表为例,其 next 指针便是一种实现节点间顺序跳转功能的关键属性)

在 “跳表简介” 章节中模拟跳表搜索部份,我们知道节点间的跳转机制可以分为两大类:

  1. 同一节点的多层跳转:在相同键值、不同层级节点之间的跳转
  2. 不同节点的单层跳转:在相同层级、不同键值节点之间的跳转

通过这两类跳转机制的结合,我们可以在跳表中灵活地实现不同层级和不同节点之间的跳转。
(就像通过 x 坐标和 y 坐标结合,可以表示坐标轴内上的任意一个点一样)

那么,能够支持这两种节点间的跳转机制的属性长什么样子呢?
让我们先聚焦于第二种跳转机制:不同节点的单层跳转。这实际上与单链表的结构相似。
在单链表中,每个节点由两部分组成:数据域和指针域。数据域负责存储当前节点的值,而指针域则指向下一个节点,将各个单独的节点链接起来。

链表结构的实现如下:

class LinkList {
  int data;
  LinkList* next;
}

在单链表的结构中,可以通过访问当前节点的 next 指针,来实现从当前节点到下一个节点之间跳转的功能。这个 next 指针指向链表中的后续节点,从而使我们能够从当前节点顺利跳转到紧随其后的节点。

// 单链表实现节点跳转的简单实现
void travasal(LinkList* listHeader) {
    LinkList* current = listHeader;
    while (current->next != nullptr) {
        current = current->next;
    }
}

所以,我们可以借鉴单链表中访问 next 指针的成员函数,来实现跳表内同一层级上不同节点间的跳转功能。也就是说,节点内部用于支持跳转功能的属性,实质上是一种指针。这个指针将会指向当前节点同一层中的后一个节点。

现在还需要解决节点跳转的第一个问题,就是节点内的该属性如何支持节点在其不同层级间的跳转呢?

到目前为止,我们可以通过 node_level 属性确定一个节点会在跳表的哪些层级出现。基于这一点,我们可以采用数组结构来组织一个节点在不同层级的指针。这意味着,用于支持两种跳转功能的属性,实际上是一个指针数组,数组其中的每个指针对应节点在一个特定层级的后继节点。通过变更数组下标,我们便能够实现同一节点在不同层级之间的跳转功能。这样的设计不仅保持了结构的简洁性,也为跳表提供了必要的灵活性和效率。

为了保持一致性和易于理解,我们将这个指针数组命名为 forward,这个命名方式与大多数跳表实现中的惯例相同。

最终的节点定义如下:

template <typename K, typename V>
class Node {
public:
    Node<K, V>** forward; // 在 C++ 中,二维指针等价于指针数组 
    int node_level;
private:
    K key;
    V value;
};

假设一个节点在跳表中的层级为 3,那么这个节点的 forward 指针数组的大小为 3。其 forward[0] 指向该节点在第一层的下一个节点;forward[1] 指向该节点在第二层的下一个节点,forward[2] 指向该节点在第三层的下一个节点。

完成节点的最终定义后,我们再介绍这个结构的跳转机制是如何运作的。

同一层级内节点的跳转:

/**
 * 遍历跳表的底层链表
 * current : 指向当前遍历节点的指针
 */
Node<K,V>* current = head; // 假设 head 是跳表第一层的头节点
while (current->forward[0] != nullptr) {
    // 通过迭代的方式,实现同一层内的不同节点之间的跳转
    current = current->forward[0];
}

不同层级内同一节点的跳转:

/**
 * 同一个节点,不同层级之间的跳转
 * node : 当前节点
 * n : 节点所在的最高层级
 */
Node<K,V>* node; // 假设 node 是当前节点
int n = node->forward.size(); // 假设 forward 是动态数组
for (int i = n - 1; i >= 0; i--) {
    // 通过变更数组下标进行同一个节点在不同层级之间的跳转
    Node<K,V>* current = node->forward[i];
}

1.2 Node 类的代码实现

在定义完毕 Node 类关键的属性之后,还需要一些基本的问题需要处理。例如获取 / 设置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;
    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;
};

2. SkipList 类

在确定了具体用于存储键值对的 Node 类之后,现在我们需要设计一个能组织和管理存储引擎 Node 类的 SkipList 类。

2.1 SkipList 属性

头节点:作为跳表中所有节点组织的入口点,类似于单链表
最大层数:跳表中允许的最大层数
当前层数:跳表当前的层数
节点数量:跳表当前的组织的所有节点总数
文件读写:跳表生成持久化文件和读取持久化文件的写入器和读取器
具体定义如下:

template <typename K, typename V>
class SkipList {
private:
    int _max_level;              // 跳表允许的最大层数
    int _skip_list_level;        // 跳表当前的层数
    Node<K, V> *_header;         // 跳表的头节点
    int _element_count;          // 跳表中组织的所有节点的数量
    std::ofstream _file_writer;  // 文件写入器
    std::ifstream _file_reader;  // 文件读取器
};

2.2 SkipList 成员函数
在定义完毕 SkipList 类的关键属性后,我们还需要设计出组织和管理 Node 类的成员函数。

核心成员函数:

节点创建:生成新的节点实例
层级分配:为每个新创建的节点分配一个合适的层数
节点插入:将节点加入到跳表中的适当位置
节点搜索:在跳表中查找特定的节点
节点删除:从跳表中移除指定的节点
节点展示:显示跳表中所有节点的信息
节点计数:获取跳表中当前的节点总数
数据持久化:将跳表的数据保存到磁盘中
数据加载:从磁盘加载持久化的数据到跳表中
垃圾回收:对于删除的节点,需要回收其内存空间
获取节点数量:获取跳表组织的节点个数
接口的具体代码如下:

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();                   // 从文件加载数据
    void clear(Node<K, V> *);           // 递归删除节点
    int size();                         // 跳表中的节点个数
private:
    // ...
};

在定义完毕 SkipList 类中的属性和成员函数之后,后续的章节内容就是实现上述的各个函数。

3 小结

我们实现一个 Node 类,用于表示跳表中的节点。

Node 类的定义如下,这是本存储引擎项目中拥有完整功能的类,在定义完毕之后,我们分别实现 Node 类的构造函数,析构函数,获取值,设置值等成员函数。

#include <iostream>
#include <cstdlib>
#include <cmath>
#include <cstring>

// 定义节点
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;
    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;
};

// 类拥有的获取 key 成员函数
template <typename K, typename V>
K Node<K, V>::get_key() const {
    return key;
};

// 类拥有的获取 value 成员函数
template <typename K, typename V>
V Node<K, V>::get_value() const {
    return value;
};

// 类拥有的设置 value 成员函数
template <typename K, typename V>
void Node<K, V>::set_value(V value) {
    this->value = value;
};

int main() {
    int K, V, L;  // 定义变量
    std::cin >> K >> V >> L;  // 获取变量
    // 创造对应的类
    Node<int, int> *node = new Node<int, int>(K, V, L);
    // 调用 get_key 成员函数和 set 成员函数
    std::cout << node->get_key() << " " << node->get_value() << std::endl;
    // 释放内存
    delete node;
    return 0;
}
  • 19
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值