1. 跳表的展示
在完成跳表的节点插入和搜索功能后,展示跳表的结构成为了下一个重要的任务。这不仅有助于理解跳表的工作原理,也是验证实现正确性的一个有效手段。
1.1 理论基础
跳表的结构本质上是一个通过对原始链表的部分节点进行筛选而构建的多级索引链表,可以视为多个层级的单链表的组合。
跳表的每一层都有一个头节点,通过这些头节点可以访问到该层的所有节点。我们首先遍历这些头节点,从而实现对每一层的访问。
1.2 代码实现
为了遍历跳表的每一层,我们利用跳表的头节点数组_header,其中_header[i]代表第i层的头节点。通过以下代码,我们可以实现对每一层头节点的遍历:
for (int i = 0; i <= _skip_list_level; i++) {
Node<K, V> *node = _header->forward[i];
}
在获取到每一层的头节点后,我们通过迭代的方式遍历该层的所有节点,并打印出节点中的键和值:
while (node != NULL) {
std::cout << node->get_key() << ":" << node->get_value() << ";";
}
将上述步骤综合起来,我们得到了展示跳表内容的完整方法:
template <typename K, typename V>
void SkipList<K, V>::display_list() {
// 从最上层开始向下遍历所有层
for (int i = _skip_list_level; i >= 0; i--) {
Node<K, V>* node = this->_header->forward[i]; // 获取当前层的头节点
std::cout << "Level " << i << ": ";
// 遍历当前层的所有节点
while (node != nullptr) {
// 打印当前节点的键和值,键值对之间用":"分隔
std::cout << node->get_key() << ":" << node->get_value() << ";";
// 移动到当前层的下一个节点
node = node->forward[i];
}
std::cout << std::endl; // 当前层遍历结束,换行
}
}
2. 生成和读取持久化文件
作为核心的存储引擎功能,数据的持久化保存与高效读取是至关重要的。
2.1 数据的保存
在之前的章节中,我们介绍了如何在存储引擎中实现数据的搜索、插入和删除操作。这些操作都是在内存中进行的,意味着一旦程序终止,所有的数据就会丢失。因此,实现数据的持久化保存变得尤为重要。
考虑到键值对数据结构的特点,我们选择将数据保存到文件中,采用 key:value 格式进行存储,每行存储一个键值对。这种格式既简单又易于解析,适合快速的数据存取。
目标文件结构如下:
1:store
2:engine
3:text
在 C++ 中,我们利用 std::ofstream 来打开文件、写入数据,并在数据写入完成后关闭文件。
实现代码:
template <typename K, typename V>
void SkipList<K, V>::dump_file() {
_file_writer.open(STORE_FILE); // 打开文件
Node<K, V>* node = this->_header->forward[0]; // 从头节点开始遍历
while (node != nullptr) {
_file_writer << node->get_key() << ":" << node->get_value() << ";\n"; // 写入键值对
node = node->forward[0]; // 移动到下一个节点
}
_file_writer.flush(); // 刷新缓冲区,确保数据完全写入
_file_writer.close(); // 关闭文件
}
STORE_FILE 是代码中定义的一个路径
2. 2 数据的读取
数据持久化之后,下一步就是实现其读取过程。在这个过程中,我们面临两个挑战:一是如何将文件中的key:value字符串解析为键值对;二是如何将读取的数据插入到内存中的跳表并建立索引。
我们首先需要定义一个工具函数,用于验证字符串的合法性。这包括检查字符串是否为空,以及是否包含分隔符:。
template <typename K, typename V>
bool SkipList<K, V>::is_valid_string(const std::string& str) {
return !str.empty() && str.find(delimiter) != std::string::npos;
}
验证字符串合法性后,我们将字符串分割为键和值。
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);
}
有了上述工具函数,我们可以继续实现从磁盘加载数据到跳表的过程。
在对字符串进行校验了之后,此时我们就需要将磁盘中 key:value 串转换成内存中的 key 和 value 了。
通过使用 std::string::substr 函数,我们可以将字符串切片,得到我们想要的 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());
}
写完所需的工具函数之后,下一步就是具体的操作了。
// Load data from disk
template <typename K, typename V>
void SkipList<K, V>::load_file() {
_file_reader.open(STORE_FILE);
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;
}
// Define key as int type
insert_element(stoi(*key), *value);
std::cout << "key:" << *key << "value:" << *value << std::endl;
}
delete key;
delete value;
_file_reader.close();
}
这段代码展示了如何将数据从磁盘读取并恢复到跳表中,同时建立必要的索引,以保持存储引擎的效率和响应性。
将上述代码整合到一起后,可在本地进行运行检查,最终文件中的内容应该为如下格式:
key1:value1
key2:value2
…
key3:value3