服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C/C++ - c++如何实现跳表(skiplist)

c++如何实现跳表(skiplist)

2021-09-23 13:31evenleo C/C++

这篇文章主要介绍了c++如何实现跳表,帮助大家更好的理解和学习,感兴趣的朋友可以了解下

引言

二分查找底层依赖的是数组随机访问的特性,所以只能用数组来实现。如果数据存储在链表中,就真的没法用二分查找算法了吗?实际上,只需要对链表稍加改造,就可以支持类似“二分”的查找算法。改造之后的数据结构叫作跳表。

定义

跳表是一个随机化的数据结构。它允许快速查询一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(log n),优于普通队列的O(n)。性能上和红黑树,AVL树不相上下,但跳表的原理非常简单,目前Redis和LevelDB中都有用到。
跳表是一种可以替代平衡树的数据结构。跳表追求的是概率性平衡,而不是严格平衡。因此,跟平衡二叉树相比,跳表的插入和删除操作要简单得多,执行也更快。

C++简单实现

下面实现过程主要是简单实现跳表的过程,不是多线程安全的,LevelDB实现的跳表支持多线程安全,用了std::atomic原子操作,本文主要是为了理解跳表的原理,所以采用最简单的实现。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#ifndef SKIPLIST_H
#define SKIPLIST_H
 
#include <ctime>
#include <initializer_list>
#include <iostream>
#include <random>
 
template <typename Key>
class Skiplist {
public:
 struct Node {
 Node(Key k) : key(k) {}
 Key key;
 Node* next[1]; // C语言中的柔性数组技巧
 };
 
private:
 int maxLevel;
 Node* head;
 
 enum { kMaxLevel = 12 };
 
public:
 Skiplist() : maxLevel(1)
 {
 head = newNode(0, kMaxLevel);
 }
 
 Skiplist(std::initializer_list<Key> init) : Skiplist()
 {
 for (const Key& k : init)
 {
  insert(k);
 }
 }
 
 ~Skiplist()
 {
 Node* pNode = head;
 Node* delNode;
 while (nullptr != pNode)
 {
  delNode = pNode;
  pNode = pNode->next[0];
  free(delNode); // 对应malloc
 }
 }
 
 // 禁止拷贝构造和赋值
 Skiplist(const Skiplist&) = delete;
 Skiplist& operator=(const Skiplist&) = delete;
 Skiplist& operator=(Skiplist&&) = delete;
 
private:
 Node* newNode(const Key& key, int level)
 {
 /*
 * 开辟sizeof(Node) + sizeof(Node*) * (level - 1)大小的空间
 * sizeof(Node*) * (level - 1)大小的空间是给Node.next[1]指针数组用的
 * 为什么是level-1而不是level,因为sizeof(Node)已包含一个Node*指针的空间
 */
 void* node_memory = malloc(sizeof(Node) + sizeof(Node*) * (level - 1));
 Node* node = new (node_memory) Node(key);
 for (int i = 0; i < level; ++i)
  node->next[i] = nullptr;
 
 return node;
 }
 /*
 * 随机函数,范围[1, kMaxLevel],越小概率越大
 */
 static int randomLevel()
 {
 int level = 1;
 while (rand() % 2 && level < kMaxLevel)
  level++;
 
 return level;
 }
 
public:
 Node* find(const Key& key)
 {
 // 从最高层开始查找,每层查找最后一个小于key的前继节点,不断缩小范围
 Node* pNode = head;
 for (int i = maxLevel - 1; i >= 0; --i)
 {
  while (pNode->next[i] != nullptr && pNode->next[i]->key < key)
  {
  pNode = pNode->next[i];
  }
 }
 
 // 如果第一层的pNode[0]->key == key,则返回pNode->next[0],即找到key
 if (nullptr != pNode->next[0] && pNode->next[0]->key == key)
  return pNode->next[0];
 
 return nullptr;
 }
 
 void insert(const Key& key)
 {
 int level = randomLevel();
 Node* new_node = newNode(key, level);
 Node* prev[kMaxLevel];
 Node* pNode = head;
 // 从最高层开始查找,每层查找最后一个小于key的前继节点
 for (int i = level - 1; i >= 0; --i)
 {
  while (pNode->next[i] != nullptr && pNode->next[i]->key < key)
  {
  pNode = pNode->next[i];
  }
  prev[i] = pNode;
 }
 // 然后每层将新节点插入到前继节点后面
 for (int i = 0; i < level; ++i)
 {
  new_node->next[i] = prev[i]->next[i];
  prev[i]->next[i] = new_node;
 }
 
 if (maxLevel < level) // 层数大于最大层数,更新最大层数
  maxLevel = level;
 }
 
 void erase(const Key& key)
 {
 Node* prev[maxLevel];
 Node* pNode = head;
 // 从最高层开始查找,每层查找最后一个小于key的前继节点
 for (int i = maxLevel - 1; i >= 0; --i)
 {
  while (pNode->next[i] != nullptr && pNode->next[i]->key < key)
  pNode = pNode->next[i];
  prev[i] = pNode;
 }
 
 // 如果找到key,
 if (pNode->next[0] != nullptr && pNode->next[0]->key == key)
 {
  Node *delNode = pNode->next[0];
  // 从最高层开始,如果当前层的next节点的值等于key,则删除next节点
  for (int i = maxLevel - 1; i >= 0; --i)
  {
  if (prev[i]->next[i] != nullptr && key == prev[i]->next[i]->key)
   prev[i]->next[i] = prev[i]->next[i]->next[i];
  }
  free(delNode); // 最后销毁pNode->next[0]节点
 }
 
 // 如果max_level>1且头结点的next指针为空,则该层已无数据,max_level减一
 while (maxLevel > 1 && head->next[maxLevel] == nullptr)
 {
  maxLevel--;
 }
 }
};
 
#endif

Redis和LevelDB选用跳表而弃用红黑树的原因

  1. Skiplist的复杂度和红黑树一样,而且实现起来更简单。
  2. 在并发环境下Skiplist有另外一个优势,红黑树在插入和删除的时候可能需要做一些rebalance的操作,这样的操作可能会涉及到整个树的其他部分,而skiplist的操作显然更加局部性一些,锁需要盯住的节点更少,因此在这样的情况下性能好一些。

以上就是c++如何实现跳表的详细内容,更多关于c++ 跳表的资料请关注服务器之家其它相关文章!

原文链接:https://www.cnblogs.com/evenleee/p/13355499.html

延伸 · 阅读

精彩推荐
  • C/C++关于C语言中E-R图的详解

    关于C语言中E-R图的详解

    今天小编就为大家分享一篇关于关于C语言中E-R图的详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看...

    Struggler095962021-07-12
  • C/C++使用C++制作简单的web服务器(续)

    使用C++制作简单的web服务器(续)

    本文承接上文《使用C++制作简单的web服务器》,把web服务器做的功能稍微强大些,主要增加的功能是从文件中读取网页并返回给客户端,而不是把网页代码...

    C++教程网5492021-02-22
  • C/C++C语言main函数的三种形式实例详解

    C语言main函数的三种形式实例详解

    这篇文章主要介绍了 C语言main函数的三种形式实例详解的相关资料,需要的朋友可以参考下...

    ieearth6912021-05-16
  • C/C++OpenCV实现拼接图像的简单方法

    OpenCV实现拼接图像的简单方法

    这篇文章主要为大家详细介绍了OpenCV实现拼接图像的简单方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    iteye_183805102021-07-29
  • C/C++c/c++实现获取域名的IP地址

    c/c++实现获取域名的IP地址

    本文给大家汇总介绍了使用c/c++实现获取域名的IP地址的几种方法以及这些方法的核心函数gethostbyname的详细用法,非常的实用,有需要的小伙伴可以参考下...

    C++教程网10262021-03-16
  • C/C++C语言实现双人五子棋游戏

    C语言实现双人五子棋游戏

    这篇文章主要为大家详细介绍了C语言实现双人五子棋游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    两片空白7312021-11-12
  • C/C++c/c++内存分配大小实例讲解

    c/c++内存分配大小实例讲解

    在本篇文章里小编给大家整理了一篇关于c/c++内存分配大小实例讲解内容,有需要的朋友们可以跟着学习参考下。...

    jihite5172022-02-22
  • C/C++深入C++拷贝构造函数的总结详解

    深入C++拷贝构造函数的总结详解

    本篇文章是对C++中拷贝构造函数进行了总结与介绍。需要的朋友参考下...

    C++教程网5182020-11-30