KV存储项目-代码随想录
众所周知,非关系型数据库redis,以及levedb,rockdb其核心存储引擎的数据结构就是跳表。本项目就是基于跳表实现的轻量级键值型存储引擎,使用C++实现。插入数据、删除数据、查询数据、数据展示、数据落盘、文件加载数据,以及数据库大小显示。# 项目中文件* main.cpp 包含skiplist.h使用跳表进行数据操作* skiplist.h 跳表核心实现* README.md 中文介绍*
文章目录
前言
众所周知,非关系型数据库redis,以及levedb,rockdb其核心存储引擎的数据结构就是跳表。
本项目就是基于跳表实现的轻量级键值型存储引擎,使用C++实现。插入数据、删除数据、查询数据、数据展示、数据落盘、文件加载数据,以及数据库大小显示。
# 项目中文件
* main.cpp 包含skiplist.h使用跳表进行数据操作
* skiplist.h 跳表核心实现
* README.md 中文介绍
* README-en.md 英文介绍
* bin 生成可执行文件目录
* makefile 编译脚本
* store 数据落盘的文件存放在这个文件夹
* stress_test_start.sh 压力测试脚本
* LICENSE 使用协议
# 提供接口
* insertElement(插入数据)
* deleteElement(删除数据)
* searchElement(查询数据)
* displayList(展示已存数据)
* dumpFile(数据落盘)
* loadFile(加载数据)
* size(返回数据规模)
# 项目运行方式
make // complie demo main.cpp
./bin/main // run
如果想自己写程序使用这个kv存储引擎,只需要在你的CPP文件中include skiplist.h 就可以了。
可以运行如下脚本测试kv存储引擎的性能(当然你可以根据自己的需求进行修改)
sh stress_test_start.sh
待优化
- delete的时候没有释放内存 (我这里进行了优化,更改SkipList析构函数的代码,使得析构完全,还请各路大佬来指正)
- 压力测试并不是全自动的
- 跳表的key用int型,如果使用其他类型需要自定义比较函数,当然把这块抽象出来更好
- 如果再加上一致性协议,例如raft就构成了分布式存储,再启动一个http server就可以对外提供分布式存储服务了
压测性能
sh stress_test_start.sh
跳表的原理和实现
跳表可以达到和 红黑树 一样的时间复杂度 O(logN) ,且实现简单, Redis 中的有序集合对象的底层数据结构就使用了跳表。
跳表基础概念
跳表,即 跳跃表 ( Skip List ),是基于并联的链表数据结构,操作效率可以达到 O(logN) ,对并发友好。跳表的示意图如下所示。
跳表的特点,可以概括如下:
跳表是多层( level )链表结构;
跳表中的每一层都是一个有序链表,并且按照元素升序(默认)排列;
跳表中的元素会在哪一层出现是随机决定的,但是只要元素出现在了第 k 层,那么 k 层以下的链表也会出现这个元素;
跳表的底层的链表包含所有元素;
跳表头节点和尾节点不存储元素,且头节点和尾节点的层数就是跳表的最大层数;
跳表中的节点包含两个指针,一个指针指向同层链表的后一节点,一个指针指向下层链表的同元素节点。
以上图中的跳表为例,如果要查找元素71,那么查找流程如下图所示
从顶层链表的头节点开始查找,查找到元素71的节点时,一共遍历了4个节点,但是如果按照传统链表的方式(即从跳表的底层链表的头节点开始向后查找),那么就需要遍历7个节点,所以跳表以空间换时间,缩短了操作跳表所需要花费的时间。
跳表的结点
已知跳表中的节点,需要有指向当前层链表后一节点的指针,和指向下层链表的同元素节点的指针,所以跳表中的节点,定义如下:
public class SkiplistNode {
public int data;
public SkiplistNode next;
public SkiplistNode down;
public int level;
public SkiplistNode(int data, int level) {
this.data = data;
this.level = level;
}
}
上述是跳表中的节点的最简单的定义方式,存储的元素 data 为整数,节点之间进行比较时直接比较元素 data 的大小。
跳表的初始化
跳表初始化时,将每一层链表的头尾节点创建出来并使用集合将头尾节点进行存储,头尾节点的层数随机指定,且头尾节点的层数就代表当前跳表的层数。初始化后,跳表结构如下所示。
跳表初始化的相关代码如下所示。
public LinkedList<SkiplistNode> headNodes;
public LinkedList<SkiplistNode> tailNodes;
public int curLevel;
public Random random;
public Skiplist() {
random = new Random();
// headNodes用于存储每一层的头节点
headNodes = new LinkedList<>();
// tailNodes用于存储每一层的尾节点
tailNodes = new LinkedList<>();
// 初始化跳表时,跳表的层数随机指定
curLevel = getRandomLevel();
// 指定了跳表的初始的随机层数后,就需要将每一层的头节点和尾节点创建出来并构建好关系
SkiplistNode head = new SkiplistNode(Integer.MIN_VALUE, 0);
SkiplistNode tail = new SkiplistNode(Integer.MAX_VALUE, 0);
for (int i = 0; i <= curLevel; i++) {
head.next = tail;
headNodes.addFirst(head);
tailNodes.addFirst(tail);
SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
headNew.down = head;
tailNew.down = tail;
head = headNew;
tail = tailNew;
}
}
跳表的添加方法
每一个元素添加到跳表中时,首先需要随机指定这个元素在跳表中的层数,如果随机指定的层数大于了跳表的层数,则在将元素添加到跳表中之前,还需要扩大跳表的层数,而扩大跳表的层数就是将头尾节点的层数扩大。下面给出需要扩大跳表层数的一次添加的过程。
初始状态时,跳表的层数为2,如下图所示。
现在要往跳表中添加元素120,并且随机指定的层数为3,大于了当前跳表的层数2,此时需要先扩大跳表的层数,如下图所示。
将元素120插入到跳表中时,从顶层开始,逐层向下插入,如下图所示。
跳表的添加方法的代码如下所示。
public void add(int num) {
// 获取本次添加的值的层数
int level = getRandomLevel();
// 如果本次添加的值的层数大于当前跳表的层数
// 则需要在添加当前值前先将跳表层数扩充
if (level > curLevel) {
expanLevel(level - curLevel);
}
// curNode表示num值在当前层对应的节点
SkiplistNode curNode = new SkiplistNode(num, level);
// preNode表示curNode在当前层的前一个节点
SkiplistNode preNode = headNodes.get(curLevel - level);
for (int i = 0; i <= level; i++) {
// 从当前层的head节点开始向后遍历,直到找到一个preNode
// 使得preNode.data < num <= preNode.next.data
while (preNode.next.data < num) {
preNode = preNode.next;
}
// 将curNode插入到preNode和preNode.next中间
curNode.next = preNode.next;
preNode.next = curNode;
// 如果当前并不是0层,则继续向下层添加节点
if (curNode.level > 0) {
SkiplistNode downNode = new SkiplistNode(num, curNode.level - 1);
// curNode指向下一层的节点
curNode.down = downNode;
// curNode向下移动一层
curNode = downNode;
}
// preNode向下移动一层
preNode = preNode.down;
}
}
private void expanLevel(int expanCount) {
SkiplistNode head = headNodes.getFirst();
SkiplistNode tail = tailNodes.getFirst();
for (int i = 0; i < expanCount; i++) {
SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
headNew.down = head;
tailNew.down = tail;
head = headNew;
tail = tailNew;
headNodes.addFirst(head);
tailNodes.addFirst(tail);
}
}
跳表的搜索方法
在跳表中搜索一个元素时,需要从顶层开始,逐层向下搜索。搜索时遵循如下规则。
目标值大于当前节点的后一节点值时,继续在本层链表上向后搜索;
目标值大于当前节点值,小于当前节点的后一节点值时,向下移动一层,从下层链表的同节点位置向后搜索;
目标值等于当前节点值,搜索结束。
下图是一个搜索过程的示意图。
跳表的搜索的代码如下所示。
public boolean search(int target) {
// 从顶层开始寻找,curNode表示当前遍历到的节点
SkiplistNode curNode = headNodes.getFirst();
while (curNode != null) {
if (curNode.next.data == target) {
// 找到了目标值对应的节点,此时返回true
return true;
} else if (curNode.next.data > target) {
// curNode的后一节点值大于target
// 说明目标节点在curNode和curNode.next之间
// 此时需要向下层寻找
curNode = curNode.down;
} else {
// curNode的后一节点值小于target
// 说明目标节点在curNode的后一节点的后面
// 此时在本层继续向后寻找
curNode = curNode.next;
}
}
return false;
}
跳表的删除方法
当在跳表中需要删除某一个元素时,则需要将这个元素在所有层的节点都删除,具体的删除规则如下所示。
首先按照跳表的搜索的方式,搜索待删除节点,如果能够搜索到,此时搜索到的待删除节点位于该节点层数的最高层;
从待删除节点的最高层往下,将每一层的待删除节点都删除掉,删除方式就是让待删除节点的前一节点直接指向待删除节点的后一节点。
下图是一个删除过程的示意图。
跳表的删除的代码如下所示。
public boolean erase(int num) {
// 删除节点的遍历过程与寻找节点的遍历过程是相同的
// 不过在删除节点时如果找到目标节点,则需要执行节点删除的操作
SkiplistNode curNode = headNodes.getFirst();
while (curNode != null) {
if (curNode.next.data == num) {
// preDeleteNode表示待删除节点的前一节点
SkiplistNode preDeleteNode = curNode;
while (true) {
// 删除当前层的待删除节点,就是让待删除节点的前一节点指向待删除节点的后一节点
preDeleteNode.next = curNode.next.next;
// 当前层删除完后,需要继续删除下一层的待删除节点
// 这里让preDeleteNode向下移动一层
// 向下移动一层后,preDeleteNode就不一定是待删除节点的前一节点了
preDeleteNode = preDeleteNode.down;
// 如果preDeleteNode为null,说明已经将底层的待删除节点删除了
// 此时就结束删除流程,并返回true
if (preDeleteNode == null) {
return true;
}
// preDeleteNode向下移动一层后,需要继续从当前位置向后遍历
// 直到找到一个preDeleteNode,使得preDeleteNode.next的值等于目标值
// 此时preDeleteNode就又变成了待删除节点的前一节点
while (preDeleteNode.next.data != num) {
preDeleteNode = preDeleteNode.next;
}
}
} else if (curNode.next.data > num) {
curNode = curNode.down;
} else {
curNode = curNode.next;
}
}
return false;
}
跳表完整代码
public class Skiplist {
public LinkedList<SkiplistNode> headNodes;
public LinkedList<SkiplistNode> tailNodes;
public int curLevel;
public Random random;
public Skiplist() {
random = new Random();
// headNodes用于存储每一层的头节点
headNodes = new LinkedList<>();
// tailNodes用于存储每一层的尾节点
tailNodes = new LinkedList<>();
// 初始化跳表时,跳表的层数随机指定
curLevel = getRandomLevel();
// 指定了跳表的初始的随机层数后,就需要将每一层的头节点和尾节点创建出来并构建好关系
SkiplistNode head = new SkiplistNode(Integer.MIN_VALUE, 0);
SkiplistNode tail = new SkiplistNode(Integer.MAX_VALUE, 0);
for (int i = 0; i <= curLevel; i++) {
head.next = tail;
headNodes.addFirst(head);
tailNodes.addFirst(tail);
SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
headNew.down = head;
tailNew.down = tail;
head = headNew;
tail = tailNew;
}
}
public boolean search(int target) {
// 从顶层开始寻找,curNode表示当前遍历到的节点
SkiplistNode curNode = headNodes.getFirst();
while (curNode != null) {
if (curNode.next.data == target) {
// 找到了目标值对应的节点,此时返回true
return true;
} else if (curNode.next.data > target) {
// curNode的后一节点值大于target
// 说明目标节点在curNode和curNode.next之间
// 此时需要向下层寻找
curNode = curNode.down;
} else {
// curNode的后一节点值小于target
// 说明目标节点在curNode的后一节点的后面
// 此时在本层继续向后寻找
curNode = curNode.next;
}
}
return false;
}
public void add(int num) {
// 获取本次添加的值的层数
int level = getRandomLevel();
// 如果本次添加的值的层数大于当前跳表的层数
// 则需要在添加当前值前先将跳表层数扩充
if (level > curLevel) {
expanLevel(level - curLevel);
}
// curNode表示num值在当前层对应的节点
SkiplistNode curNode = new SkiplistNode(num, level);
// preNode表示curNode在当前层的前一个节点
SkiplistNode preNode = headNodes.get(curLevel - level);
for (int i = 0; i <= level; i++) {
// 从当前层的head节点开始向后遍历,直到找到一个preNode
// 使得preNode.data < num <= preNode.next.data
while (preNode.next.data < num) {
preNode = preNode.next;
}
// 将curNode插入到preNode和preNode.next中间
curNode.next = preNode.next;
preNode.next = curNode;
// 如果当前并不是0层,则继续向下层添加节点
if (curNode.level > 0) {
SkiplistNode downNode = new SkiplistNode(num, curNode.level - 1);
// curNode指向下一层的节点
curNode.down = downNode;
// curNode向下移动一层
curNode = downNode;
}
// preNode向下移动一层
preNode = preNode.down;
}
}
public boolean erase(int num) {
// 删除节点的遍历过程与寻找节点的遍历过程是相同的
// 不过在删除节点时如果找到目标节点,则需要执行节点删除的操作
SkiplistNode curNode = headNodes.getFirst();
while (curNode != null) {
if (curNode.next.data == num) {
// preDeleteNode表示待删除节点的前一节点
SkiplistNode preDeleteNode = curNode;
while (true) {
// 删除当前层的待删除节点,就是让待删除节点的前一节点指向待删除节点的后一节点
preDeleteNode.next = curNode.next.next;
// 当前层删除完后,需要继续删除下一层的待删除节点
// 这里让preDeleteNode向下移动一层
// 向下移动一层后,preDeleteNode就不一定是待删除节点的前一节点了
preDeleteNode = preDeleteNode.down;
// 如果preDeleteNode为null,说明已经将底层的待删除节点删除了
// 此时就结束删除流程,并返回true
if (preDeleteNode == null) {
return true;
}
// preDeleteNode向下移动一层后,需要继续从当前位置向后遍历
// 直到找到一个preDeleteNode,使得preDeleteNode.next的值等于目标值
// 此时preDeleteNode就又变成了待删除节点的前一节点
while (preDeleteNode.next.data != num) {
preDeleteNode = preDeleteNode.next;
}
}
} else if (curNode.next.data > num) {
curNode = curNode.down;
} else {
curNode = curNode.next;
}
}
return false;
}
private void expanLevel(int expanCount) {
SkiplistNode head = headNodes.getFirst();
SkiplistNode tail = tailNodes.getFirst();
for (int i = 0; i < expanCount; i++) {
SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
headNew.down = head;
tailNew.down = tail;
head = headNew;
tail = tailNew;
headNodes.addFirst(head);
tailNodes.addFirst(tail);
}
}
private int getRandomLevel() {
int level = 0;
while (random.nextInt(2) > 1) {
level++;
}
return level;
}
}
总结
跳表的时间复杂度与 AVL 树和红黑树相同,可以达到 O(logN) ,但是 AVL 树要维持高度的平衡,红黑树要维持高度的近似平衡,这都会导致插入或者删除节点时的一些时间开销,所以跳表相较于 AVL 树和红黑树来说,省去了维持高度的平衡的时间开销,但是相应的也付出了更多的空间来存储多个层的节点,所以跳表是用空间换时间的数据结构。
跳表思想介绍
一种基于单链表的高级数据结构, 跳表
将单链表先进行排序,然后针对 有序链表 为了实现高效的查找,可以使用跳表这种数据结构。
其根本思想是 二分查找 的思想。
跳表的前提条件是 针对 有序的单链表 ,实现高效地查找,插入,删除。
Redis中的 有序集合 sorted set 就是用跳表实现的。
跳表原理
为了提高查找效率,使用二分查找的思想,对有序链表建立一级“索引”。 每两个节点提取一个节点到索引层。 索引层中的每个节点 都包含两个指针,一个指向下一个节点,一个down指针,指向下一级节点。
建立二级索引:与建立一级索引的方式类似,在第一级索引的基础上,每两个节点抽出一个节点到第二级索引,如下图:
这种链表加多级索引的结构 就是 跳表。
跳表的空间复杂度和时间复杂度
1、时间复杂度
假设链表包含n个节点,在单链表中查询某个数据的时间复杂度是O(n)。
一个包含n个节点的有序单链表最多会有多少级索引?
每两个节点抽出一个节点作为上一级索引的节点,则 :
第一级索引的节点个数大约是 n/2 ,第二级索引的节点个数大约是 n/4,第三级索引的节点个数大约是 n/8,则第k级索引节点的个数大约是n/2的k次方。
假设有 h 级索引,最高一级的索引有两个节点,也就是n/2的h次方 = 2 ,从而求得h = log 2 n - 1 如果把原始链表这一层也算进去,那么整个跳表的高度约为log 2 n
在跳表查询时,每一级索引 最多需要遍历3个节点。
那么在跳表中查询数据的时间复杂度就是 每一层遍历的节点数乘以层数,因此跳表中查找的时间复杂度就是O(logn). 与二分查找的时间复杂度相同。
基于单链表实现了二分查找,查询效率的提升依赖构建了多级索引,是一种空间换时间的设计思路。
2、空间复杂度
建立索引后 的 总的索引点 的 空间复杂度:
跳表的查询数据的空间复杂度是O(n),也就是说,基于单链表构造跳表,需要额外再用一倍的存储空间。
有没有办法降低索引占用的存储空间呢?
如果每3个节点 或 每5个节点抽1个节点到上一级索引,索引节点就会相应减少。假设每3个节点抽取一个节点到上一级,总的索引节点个数为:
每3个节点抽1个节点到上一级索引的方法 比 每2个节点抽1个节点构建索引的方式,减少了一半的索引节点存储空间。
因此,通过调节抽取节点的间隔,控制索引节点占用的存储空间,以此来达到空间复杂度 和 时间复杂度的平衡。
插入和删除操作
跳表作为一个动态数据结构,不仅支持查找操作,还支持数据的插入和删除操作,并且 插入和删除的操作的时间复杂度都是O(logn).
跳表索引动态更新
当频繁地向跳表中插入数据时,如果插入过程不伴随着索引更新,就有可能导致某2个索引节点之间数据非常多,在极端地情况下,跳表就会退化成单链表。
作为一种动态数据结构,为了避免性能下降,我们需要在数据插入,删除的过程中,动态地更新跳表的索引结构。 就像红黑树,二叉平衡树是通过左右旋来保持左右子树的大小平衡, 而跳表是借助随机函数来更新索引结构。
当向跳表中插入数据时,我们选择同时将这个数据插入到部分索引层中。 如何决定插入到哪些索引层中呢? 通过一个随机函数来决定,比如通过 随机函数得到某个值 K, 那么就将这个节点添加到第一级到第K级索引中。
总结
为什么Redis中的有序集合用跳表而非红黑树来实现呢?
1.对于插入,删除,查找 以及 输出有序序列 这几个操作,红黑树也可以完成,时间复杂度 与 用跳表实现是相同的。 但是,对于按照区间查找数据这个操作(比如 [20,300]),红黑树的效率没有跳表高,跳表可以做到 O(logn)的时间复杂度定位区间的起点,然后在原始链表中顺序向后遍历输出,直到遇到值大于区间终点的节点为止。
2.跳表更加灵活,它可以通过改变节点的抽取间隔,灵活地平衡空间复杂度和时间复杂度
3.相比红黑树,跳表更容易实现,代码更简单。
代码实现(Leetcode1206题)
struct Node
{
Node* right;
Node* down;
int val;
Node(Node *right, Node *down, int val) :right(right), down(down), val(val){}
};
class Skiplist {
private:
Node *head;
const static int MAX_LEVEL = 32;
vector<Node*> pathList;
public:
Skiplist() {
head = new Node(nullptr, nullptr, -1);
pathList.reserve(MAX_LEVEL);//是用来预分配 pathList 的内存空间,确保在使用 pathList 时不需要频繁地重新分配内存,从而提高性能。reserve 只预分配内存,但并不实际增加 pathList 的元素数量。pathList 仍然为空,只有当实际插入元素时,它的大小才会增加
}
//从本层节点值往后查,直到查到下一节点值大于target,此时朝下一层,然后在此层再继续,target此时是大于等于target的,如果下一个结点时空,说明遍历完了,p = null,然后退出while;如果下一节点非空,继续走;
bool search(int target) {
Node *p = head;
while(p)
{ //// 首先在当前层向右查找,直到找到一个节点 p->right,该节点的值大于等于 num。
while(p->right && p->right->val < target)
{
p = p->right;
}
if(!p->right || target < p->right->val)//// 如果 p->right 为空或者 p->right->val > num,则说明当前层不包含目标值,向下层继续查找。
{
p = p->down;
}
else
{
return true;//找到目标值
}
}
return false;//没找到
}
void add(int num) {
pathList.clear();// 用于记录插入位置的路径,从最高层到最低层。
Node *p = head;//p 指向跳表的头结点(从最高层开始)。
/*在每一层,通过向右移动 p,找到当前层次中小于 num 的最大节点,并将该节点加入 pathList。
然后,继续向下层搜索,直到到达最底层。*/
while(p)
{
while(p->right && p->right->val < num)
{
p = p->right;
}
pathList.push_back(p);
p = p->down;
}
bool insertUp = true;
Node* downNode = nullptr;
while(insertUp && pathList.size() > 0)
{
Node *insert = pathList.back();
pathList.pop_back();
// add新结点 表示当前层以下的对应节点,作为新节点的下指针
insert->right = new Node(insert->right,downNode,num); //将新创建的节点插入到当前节点的右侧。
// 更新 downNode,使其指向刚刚插入的新节点,确保下一层的节点能够正确地链接到这一层的新节点。
downNode = insert->right;
// 50%概率 使用 rand() 函数以 50% 的概率决定是否继续向上层插入新节点,这是跳表保持平衡性的重要机制。
insertUp = (rand()&1)==0;
}
// 如果最后决定继续向上插入(insertUp 仍为 true),则增加一个新的头结点(新的最高层),并将新的节点插入这一层。
if(insertUp)
{
head = new Node(new Node(NULL,downNode,num), head, -1);
}
}
bool erase(int num) {
Node *p = head;
bool seen = false;//seen 记录是否成功找到并删除了目标值 num,初始值为 false
while (p)
{
// 首先在当前层向右查找,直到找到一个节点 p->right,该节点的值大于等于 num。
while (p->right && p->right->val < num)
{
p = p->right;
}
// 如果 p->right 为空或者 p->right->val > num,则说明当前层不包含目标值,向下层继续查找。
if (!p->right || p->right->val > num)
{
p = p->down;
}
else//如果 p->right->val == num,则找到了目标节点,将其从链表中删除,并将 seen 设置为 true 表示找到了目标值。
{
// 搜索到目标结点,进行删除操作,结果记录为true
// 继续往下层搜索,删除一组目标结点
seen = true;
p->right = p->right->right;
p = p->down;////然后继续向下层查找,删除所有与 num 值相等的节点(因为跳表可能有多层相同值的节点)。
}
}
return seen;
}
};
skiplist.h
这个代码实现了一个通用的跳表(Skip List)数据结构,并提供了插入、删除、查找、序列化和反序列化等功能。
#include <iostream>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <mutex>
#include <fstream>
:用于随机数生成(rand())。
:用于数学函数,但在这段代码中没有使用。
:用于处理C风格字符串操作(memset())。
:用于多线程同步,确保对共享资源的访问是线程安全的。
:用于文件读写操作。
#define STORE_FILE "store/dumpFile"
std::mutex mtx; // mutex for critical section
std::string delimiter = ":";
STORE_FILE:定义文件存储路径。
mtx:互斥锁,用于保护临界区,确保多线程操作的安全。
delimiter:分隔符,用于键值对的序列化和反序列化。
节点类
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;
};
Node 类:定义了跳表的节点结构,每个节点存储一个键值对和一个指向下一个节点的指针数组 forward。
node_level:表示节点所在的层级。
key 和 value:存储节点的键和值。
forward:指向下一个节点的指针数组,不同的层级对应不同的指针。Node<K, V> **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;
this->forward = new Node<K, V>*[level+1];
memset(this->forward, 0, sizeof(Node<K, V>*)*(level+1));//将刚刚分配的指针数组中的所有指针初始化为 nullptr(即 0),以确保在使用这些指针之前,它们不会指向任何随机的或未初始化的内存位置。
};
template<typename K, typename V>
Node<K, V>::~Node() {
delete []forward;
};
Node(K k, V v, int level):根据传入的键、值和层级,创建一个节点,并初始化 forward 指针数组为 NULL。这行代码分配了一个大小为 level+1 的指针数组,并将 forward 指向这个数组。数组中的每个元素都是一个指向 Node<K, V> 的指针,用于表示跳表的每一层的指针。
~Node():释放 forward 指针数组的内存。
跳表类
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:
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;//文件流,用于数据的持久化存储和加载。
std::ifstream _file_reader;
int _element_count;//跳表中元素的数量。
};
SkipList 类:定义了跳表的主要数据结构和操作方法。
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;
this->_header = new Node<K, V>(K(), V(), _max_level);
};
SkipList(int 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();
}
if(_header->forward[0]!=nullptr){
clear(_header->forward[0]);
}
delete(_header);
}
~SkipList():关闭文件流,递归删除所有节点,释放头节点的内存。
创建节点
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;
}
create_node(K k, V v, int level):根据传入的键、值和层级创建一个新节点。
随机生成层级:
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;
}
get_random_level():使用随机数生成一个节点的层级,层级越高节点的数量越少,这保证了跳表的效率。
插入元素
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+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) {
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;
}
锁定互斥量以保证线程安全。
从跳表的最高层开始查找插入位置。
如果找到相同的键值对,则直接返回。
如果没有找到,则生成一个随机层级,并将新节点插入到合适的位置。
更新 _skip_list_level 和 _element_count。
删除元素
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
stress_test_start.sh
g++ stress-test/stress_test.cpp -o ./bin/stress --std=c++11 -pthread
./bin/stress
g++ stress-test/stress_test.cpp -o ./bin/stress --std=c++11 -pthread
g++:GCC C++编译器。
stress-test/stress_test.cpp:源代码文件路径。
-o ./bin/stress:指定输出可执行文件的位置和名称为 ./bin/stress。
--std=c++11:使用C++11标准进行编译。
-pthread:链接线程库,以支持多线程编程。
./bin/stress:**运行**编译生成的可执行文件。
makefile
CC=g++
CXXFLAGS = -std=c++0x
CFLAGS=-I
skiplist: main.o
$(CC) -o ./bin/main main.o --std=c++11 -pthread
rm -f ./*.o
clean:
rm -f ./*.o
一个简单的Makefile,用于编译一个C++项目。
CC = g++:指定编译器为 g++。
CXXFLAGS = -std=c++0x:指定C++编译选项,使用C++0x标准(早期C++11的草案版本)。建议使用 -std=c++11 以确保你使用的是C++11标准。
CFLAGS = -I:指定编译器的标志(通常用于包含头文件的路径)。-I 后面没有指定路径,这可能导致编译时找不到头文件。
skiplist: main.o:定义目标 skiplist,它依赖于 main.o 文件。
$(CC) -o ./bin/main main.o --std=c++11 -pthread:用 g++ 编译并链接 main.o 生成可执行文件 ./bin/main,同时使用 --std=c++11 标准和 -pthread 选项。
rm -f ./*.o:编译后删除所有 .o 文件(目标文件)。
clean:定义了一个清理目标,用于删除所有 .o 文件
main.cpp
/* ************************************************************************
> File Name: main.cpp
> Author: 程序员Carl
> 微信公众号: 代码随想录
> Created Time: Sun Dec 2 20:21:41 2018
> Description:
************************************************************************/
#include <iostream>
#include "skiplist.h"
#define FILE_PATH "./store/dumpFile"
int main() {
// 键值中的key用int型,如果用其他类型,需要自定义比较函数
// 而且如果修改key的类型,同时需要修改skipList.load_file函数
SkipList<int, std::string> skipList(6);
skipList.insert_element(1, "学");
skipList.insert_element(3, "算法");
skipList.insert_element(7, "认准");
skipList.insert_element(8, "微信公众号:代码随想录");
skipList.insert_element(9, "学习");
skipList.insert_element(19, "算法不迷路");
skipList.insert_element(19, "赶快关注吧你会发现详见很晚!");
std::cout << "skipList size:" << skipList.size() << std::endl;
skipList.dump_file();
// skipList.load_file();
skipList.search_element(9);
skipList.search_element(18);
skipList.display_list();
skipList.delete_element(3);
skipList.delete_element(7);
std::cout << "skipList size:" << skipList.size() << std::endl;
skipList.display_list();
}
代码展示了如何使用 SkipList(跳表)数据结构来存储键值对,并执行插入、删除、查找和展示跳表的操作。
strees_test.cpp
/* ************************************************************************
> File Name: stress_test.cpp
> Author: 程序员Carl
> 微信公众号: 代码随想录
> Created Time: Sun 16 Dec 2018 11:56:04 AM CST
> Description:
************************************************************************/
#include <iostream>
#include <chrono>
#include <cstdlib>
#include <pthread.h>
#include <time.h>
#include "../skiplist.h"
#define NUM_THREADS 1
#define TEST_COUNT 100000
SkipList<int, std::string> skipList(18);
void *insertElement(void* threadid) {
long tid;
tid = (long)threadid;
std::cout << tid << std::endl;
int tmp = TEST_COUNT/NUM_THREADS;
for (int i=tid*tmp, count=0; count<tmp; i++) {
count++;
skipList.insert_element(rand() % TEST_COUNT, "a");
}
pthread_exit(NULL);
}
void *getElement(void* threadid) {
long tid;
tid = (long)threadid;
std::cout << tid << std::endl;
int tmp = TEST_COUNT/NUM_THREADS;
for (int i=tid*tmp, count=0; count<tmp; i++) {
count++;
skipList.search_element(rand() % TEST_COUNT);
}
pthread_exit(NULL);
}
int main() {
srand (time(NULL));
{
pthread_t threads[NUM_THREADS];
int rc;
int i;
auto start = std::chrono::high_resolution_clock::now();
for( i = 0; i < NUM_THREADS; i++ ) {
std::cout << "main() : creating thread, " << i << std::endl;
rc = pthread_create(&threads[i], NULL, insertElement, (void *)i);
if (rc) {
std::cout << "Error:unable to create thread," << rc << std::endl;
exit(-1);
}
}
void *ret;
for( i = 0; i < NUM_THREADS; i++ ) {
if (pthread_join(threads[i], &ret) !=0 ) {
perror("pthread_create() error");
exit(3);
}
}
auto finish = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsed = finish - start;
std::cout << "insert elapsed:" << elapsed.count() << std::endl;
}
// skipList.displayList();
// {
// pthread_t threads[NUM_THREADS];
// int rc;
// int i;
// auto start = std::chrono::high_resolution_clock::now();
// for( i = 0; i < NUM_THREADS; i++ ) {
// std::cout << "main() : creating thread, " << i << std::endl;
// rc = pthread_create(&threads[i], NULL, getElement, (void *)i);
// if (rc) {
// std::cout << "Error:unable to create thread," << rc << std::endl;
// exit(-1);
// }
// }
// void *ret;
// for( i = 0; i < NUM_THREADS; i++ ) {
// if (pthread_join(threads[i], &ret) !=0 ) {
// perror("pthread_create() error");
// exit(3);
// }
// }
// auto finish = std::chrono::high_resolution_clock::now();
// std::chrono::duration<double> elapsed = finish - start;
// std::cout << "get elapsed:" << elapsed.count() << std::endl;
// }
pthread_exit(NULL);
return 0;
}
stress_test.cpp 代码是用于对跳表(SkipList)进行压力测试的程序。它通过多线程并发地插入和查找元素来测试跳表的性能
头文件引入了必要的库,包括跳表的头文件 skiplist.h。
NUM_THREADS 定义了线程数,TEST_COUNT 定义了测试的操作次数。
创建了一个跳表实例 skipList,最大层数为18。
更多推荐
所有评论(0)