文章目录
一、跳跃表的概念
跳跃表具有如下性质:
- 由很多层链表组成,每一层都是一个有序链表
- 最底层(level 1)的链表包含所有元素
- 如果一个元素出现在level i层的链表中,则它在level i之下的链表也都会出现
- 每个节点都包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素跳跃表的增加、删除、查询操作时间复杂度和红黑树一样,也是 O ( l o g 2 N ) O(log_2N) O(log2N)
相比于红黑树,它的优势是:
- 实现起来更加简单
- 跳跃表的增加、删除操作只会改动局部,不像红黑树的增加、删除操作,因为需要节点重新着色和旋转,可能整棵树都要进行调整,这就导致了并发环境下红黑树锁的粒度大。并发环境下,跳跃表只需要对插入元素影响的区间加锁即可,跳跃表加锁的粒度会更小一些,并发能力更强
- 因为跳跃表的每一层都是一个有序的链表,因此范围查找非常方便,优于红黑树的范围搜索的跳跃表相比于红黑树,是用空间换时间(level 2层开始每一层都有会存储重复的数据),因此占用的内存空间比红黑树大
跳跃表相对于红黑树而言,是在空间换时间,占用内存比较大。红黑树中,所有元素值只是存储一个节点的,但是在跳跃表中,所有的元素不仅仅在最下面那层出现,而且根据抛硬币选出来的元素在上面层进行存储,以此类推上去,模拟了一个二分查找搜索树的过程,简化了红黑树,达到O(logn)的搜索和删除的时间复杂度,而且实现起来比较简单
在实际应用场景中,跳跃表和红黑树在很多情况下是可以互换的
redis的有序集合也是用SkipList实现的,增、删、查都是
O
(
l
o
g
2
N
)
O(log_2N)
O(log2N),只要能定义比较方式,就可以用SkipList存储
关于SkipList的层数,我们采用符合二项分布的函数确定是否将某个节点存储至上一层。既然是符合二项分布,当数据量较大时,一般来说对于相邻两层,上层节点数量会是下层节点数量的 1 / 2 1/2 1/2,这就很类似于二叉树了
二、跳跃表的find
我们查找117的过程如下:
- 先在最上层的链表找第一个大于117的数,从这个大于117节点的前一个节点往下寻找,于是从37往下查找
- 同理,从71往下找
- 在最下层节点找到117
bool find(int data) const{
Node* pre = head_;
Node* cur = pre->next_;
while (true) {
if (cur != nullptr) {
if (cur->data_ < data) {
pre = cur;
cur = cur->next_;
}
else if (cur->data_ > data) {
if (pre->down_ == nullptr) {
// 已经到了level 1
return false;
}
pre = pre->down_;
cur = pre->next_;
}
else {
return true;
}
}
else {
// cur == nullptr
if (pre->down_ == nullptr) {
// 已经到了level 1
return false;
}
else {
pre = pre->down_;
cur = pre->next_;
}
}
}
}
三、跳跃表的insert
如果我们要插入119这个数字,通过抛硬币,决定119要插几行
- K=2:跳表本身是3行,插2行是没有问题的,在最后一行按序插入,因为要插2行,所以在第二行也插了一个119
- K=4:跳表只有3层,但是最终我们也是要插4层的,也就是说,在最上层(第4层)就只有一个数字119
- K=5:119这个数字要添加到5层,但是跳表只有3层,所以我们最终也是只插4层就可以了
编写代码顺序:
- 查找data,重复则不插入
- 获取level,若level过大,调整level使得skiplist只增长一层
- 用数组存储要插入的level个节点,并纵向连接
- 找到最先插入节点的第level层链表的头节点,往后遍历找到合适的位置插入数组中的节点,然后再到下层链表找到合适的位置插入,直到在底层链表插入
void insert(int data) {
if (find(data)) {
// 不插入重复数据
return;
}
int level = get_level();
if (level > head_->level_) {
// level过大,只增长一层
level = head_->level_ + 1;
// 需要增长,则构造新的头节点
HeadNode* new_head = new HeadNode(level);
new_head->down_ = head_;
head_ = new_head;
}
// 先创建一个指针数组,用于存放data节点的地址
Node** new_nodes = new Node * [level];
for (int i = level - 1; i >= 0; i--) {
new_nodes[i] = new Node(data);
if (i != level - 1) {
new_nodes[i]->down_ = new_nodes[i + 1];
}
}
// 若获取的level本身很小,需要将insert_head指向首次插入的层
Node* insert_head = head_;
for (int i = head_->level_; i > level; i--) {
insert_head = insert_head->down_;
}
Node* pre = insert_head;
Node* cur = pre->next_;
for (int i = 0; i < level; i++) {
while (cur != nullptr && cur->data_ < data) {
// 找第一个大于data的节点
pre = cur;
cur = cur->next_;
}
// 找到第一个大于data的节点后,将数组内存放的data节点插入pre和cur之间
new_nodes[i]->next_ = cur;
pre->next_ = new_nodes[i];
pre = pre->down_;
if (pre != nullptr) {
cur = pre->next_;
}
}
delete[] new_nodes;
new_nodes = nullptr;
}
四、跳跃表的remove
- 删除71:先查找到level 2的71,修改37节点的指针域,然后把71这一整列的节点全部delete
- 删除37:在level 3中找到37,修改37前面节点的指针域,并把37这一整列的节点全部delete
需要注意:如果此时链表的最高层只有一个数据节点119,我们在删除操作中把最高层的数据节点119删除了,那么此时最高层链表就只剩一个头节点了,而这个头节点也是要删除的
编写代码顺序:
- 查找第一个data的数据,现在上层链表找第一个大于data的节点,找到则删除,并且pre向下走;找不到则pre直接向下走
- 在同一层链表查找data时,一直向后遍历,直到找到data或cur为空
void remove(int data) {
// 头节点不可能为空
Node* pre = head_;
Node* cur = pre->next_;
while (true) {
if (cur != nullptr) {
if (cur->data_ < data) {
pre = cur;
cur = cur->next_;
continue;
}
else if (cur->data_ == data) {
pre->next_ = cur->next_;
delete cur;
if (head_->next_ == nullptr) {
// 如果只剩头节点了,说明pre肯定也指向头节点
delete head_;
head_ = static_cast<HeadNode*>(pre);
}
}
}
// 这里 (cur==nullptr) || (删除节点了pre准备向下走) || (找到了第一个大于data的节点,pre准备向下走)
if (pre->down_ == nullptr) {
// 已经到了level 1,且cur==nullptr,说明已经遍历到最右下角的节点了
return;
}
pre = pre->down_;
cur = pre->next_;
}
}
五、完整代码
class SkipList {
public:
SkipList() {
head_ = new HeadNode(1); // 跳跃表初始化为第一层
}
~SkipList() {
Node* next_head = head_;
int level = head_->level_;
for (int i = level; i > 0; i--) {
Node* pre = next_head;
Node* cur = pre->next_;
next_head = next_head->down_;
while (cur != nullptr) {
delete pre;
pre = cur;
cur = cur->next_;
}
}
}
bool find(int data) const{
Node* pre = head_;
Node* cur = pre->next_;
while (true) {
if (cur != nullptr) {
if (cur->data_ < data) {
pre = cur;
cur = cur->next_;
}
else if (cur->data_ > data) {
if (pre->down_ == nullptr) {
// 已经到了level 1
return false;
}
pre = pre->down_;
cur = pre->next_;
}
else {
return true;
}
}
else {
// cur == nullptr
if (pre->down_ == nullptr) {
// 已经到了level 1
return false;
}
else {
pre = pre->down_;
cur = pre->next_;
}
}
}
}
void insert(int data) {
if (find(data)) {
// 不插入重复数据
return;
}
int level = get_level();
if (level > head_->level_) {
// level过大,只增长一层
level = head_->level_ + 1;
// 需要增长,则构造新的头节点
HeadNode* new_head = new HeadNode(level);
new_head->down_ = head_;
head_ = new_head;
}
// 先创建一个指针数组,用于存放data节点的地址
Node** new_nodes = new Node * [level];
for (int i = level - 1; i >= 0; i--) {
new_nodes[i] = new Node(data);
if (i != level - 1) {
new_nodes[i]->down_ = new_nodes[i + 1];
}
}
// 若获取的level本身很小,需要将insert_head指向首次插入的层
Node* insert_head = head_;
for (int i = head_->level_; i > level; i--) {
insert_head = insert_head->down_;
}
Node* pre = insert_head;
Node* cur = pre->next_;
for (int i = 0; i < level; i++) {
while (cur != nullptr && cur->data_ < data) {
// 找第一个大于data的节点
pre = cur;
cur = cur->next_;
}
// 找到第一个大于data的节点后,将数组内存放的data节点插入pre和cur之间
new_nodes[i]->next_ = cur;
pre->next_ = new_nodes[i];
pre = pre->down_;
if (pre != nullptr) {
cur = pre->next_;
}
}
delete[] new_nodes;
new_nodes = nullptr;
}
void remove(int data) {
// 头节点不可能为空
Node* pre = head_;
Node* cur = pre->next_;
while (true) {
if (cur != nullptr) {
if (cur->data_ < data) {
pre = cur;
cur = cur->next_;
continue;
}
else if (cur->data_ == data) {
pre->next_ = cur->next_;
delete cur;
if (head_->next_ == nullptr) {
// 如果只剩头节点了,说明pre肯定也指向头节点
delete head_;
head_ = static_cast<HeadNode*>(pre);
}
}
}
if (pre->down_ == nullptr) {
// 已经到了level 1,且cur==nulptr,说明已经遍历到最右下角的节点了
return;
}
pre = pre->down_;
cur = pre->next_;
}
}
void show() const {
Node* head = head_;
while (head != nullptr) {
Node* cur = head->next_;
while (cur != nullptr) {
cout << cur->data_ << " ";
cur = cur->next_;
}
cout << endl;
head = head->down_;
}
cout << endl;
}
private:
int get_level() {
int n = 1;
while (rand() % 2 == 1) {
n++;
}
return n;
}
private:
// 普通节点类型
struct Node {
Node(int data = 0)
: data_(data)
, next_(nullptr)
,down_(nullptr)
{}
int data_;
Node* next_;
Node* down_;
};
// 头节点类型
struct HeadNode : public Node {
HeadNode(int level)
: level_(level)
{}
int level_; // 记录跳跃表的层数
};
private:
HeadNode* head_;
};