0. 优化空间
- 增加Score权值
- 使用了 C++11 智能指针,实现了自动管理动态分配的内存
- 每次创建节点时,是随机生成层数,改为根据幂次定律(越大的数出现的概率越小)随机生成层数
- 增加异步日志系统。
1. Node类
1.1 定义
Node类中有以下几个成员函数
- Key and Value
node_level
:当前节点的层数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类的构造函数主要负责:
- 给出Key and Value, level。
- 根据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 构造函数、析构函数、创建节点函数
构造函数负责:
- 给出整个跳表所允许的最大层数
- 创建一个头节点,头节点的层数为所允许的最大层数。(头节点不保存数据)
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()----插入
插入数据:将数据插入跳表中,并为其随机生成层数
插入数据的整体步骤:
- 先加锁。
- 使用一个指向
Node
的指针current
来负责遍历整个跳表。 - 初始化一个指向
Node
的指针数组update
,作用:update[x]
保存的是第x层最后一个符合要求的节点。要求:只要当前节点的下一节点非空,且 key 小于目标。 - 用
current
从那个最高层开始遍历,保存每一层的Node
指针到update数组中 - 判断所插入的元素是否存在在表中,如果存在则返回1,插入失败;
- 如果不存在:开始为该节
inserted_node
点生成随机层数level。 - 如果新添加的节点层高level大于当前跳表层高,则需要更新
update
数组,将原本[_skip_list_level, random_level]范围内的NULL改为_header, - 得到最终确定的层高后,就可以创建节点,挨层进行插入:根据刚刚所保存的
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----删除
如果理解了插入,那么删除也很简单:
- 自顶向下找到要删除节点的前一个节点,并保存在指针数组
update
中。注:update[x]
第x层保存要删除节点的前一个节点 - 自底向上将跳表重新连接
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和之前用的有什么区别