C++实现跳表:四、跳表的插入

本文将正式介绍跳表的创建和搜索、插入操作几个核心部分:

1. 创建跳表

下面是跳表的基础代码框架,仅仅包括本节内容中必要的类定义和成员函数,其余内容为了保证篇幅长度均已省略:

#include <iostream>
#include <cstdlib>
#include <cmath>
#include <cstring>

// 节点类定义
template <typename K, typename V>
class Node {
    // 节点类成员变量和方法(省略)
};

// 跳表类定义
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);  // 插入元素
    bool search_element(K);  // 搜索元素
    // 其他成员变量和函数(省略)
private:
    int _max_level;  // 最大层级
    int _skip_list_level;  // 当前层级
    Node<K, V> *_header;  // 头节点
    // 其他私有成员(省略)
    int _element_count;  // 节点数量
};

跳表的构造函数负责初始化跳表,主要包括以下步骤:

  1. 设置最大层级:根据预设值确定跳表的最大层级
  2. 初始化成员变量:设置跳表当前层级为 0,节点计数为 0
  3. 创建头节点:初始化一个头节点,其层级等于跳表的最大层级
    具体代码如下:
template <typename K, typename V>
SkipList<K, V>::SkipList(int max_level) {
    this->_max_level = max_level;  // 设置最大层级
    this->_skip_list_level = 0;    // 初始化当前层级为 0
    this->_element_count = 0;      // 初始化节点计数为 0
    K k;  // 默认键
    V v;  // 默认值
    // 创建头节点,并初始化键值为默认值
    this->_header = new Node<K, V>(k, v, _max_level);
};

完成跳表的初始化之后,接下来的环节是实现节点创建的方法。

创建新节点的过程涉及以下主要步骤:

  1. 节点实例化:依据提供的键(k)和值(v),创建一个新的节点实例。同时,为这个新节点指定一个层级(level),这一层级决定了节点在跳表中的高度
  2. 返回新节点:完成节点的创建后,返回这个新创建的节点实例,以便于进一步在跳表中进行插入操作。
    具体代码如下:
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; // 返回新创建的节点
}

2. 实现搜索方法

2.1 理论基础

完成跳表的创建之后,让我们先了解跳表的搜索方法,因为后续的插入方法和删除方法都依赖于搜索方法。

我们之前已经简要介绍过跳表的搜索机制。搜索开始于跳表的顶层,这一点在我们的 SkipList 类中通过变量 _skip_list_level 得到体现,该变量记录了跳表当前的最高层级。

并且在 “跳表的定义” 章节中,我们曾经介绍过,每个节点都维护一个 forward 数组,该数组记录了该节点在每一层的下一个节点的指针。

_header 作为跳表的头节点,为操作跳表提供了一个统一的入口。跳表的本质是由原始链表经过筛选部分节点构建成的多级索引链表。因此,跳表可视为多个层级的单链表组合而成。在单链表结构中,通常会有一个头节点,其 next 指针指向链表的第一个实际节点。相应地,对于多层级的跳表结构,我们需要多个头节点来指向各层的第一个节点。这些头节点被存储在 _header 节点的 forward 数组中。例如,_header->forward[0] 指向最底层的第一个节点,_header->forward[1] 指向第二层的第一个节点,依此类推。

基于这个结构,利用 _header 节点和 _skip_list_level(记录跳表实际最高层级的变量)作为起点,我们可以从跳表的最顶层开始进行搜索。

2.2 代码实现

以下是拥有详细注释的搜索方法代码:

template <typename K, typename V>
bool SkipList<K, V>::search_element(K key) {
    // 定义一个指针 current,初始化为跳表的头节点 _header
    Node<K, V> *current = _header;
    // 从跳表的最高层开始搜索
    for (int i = _skip_list_level; i >= 0; i--) {
        // 遍历当前层级,直到下一个节点的键值大于或等于待查找的键值
        while (current->forward[i] && current->forward[i]->get_key() < key) {
            // 移动到当前层的下一个节点
            current = current->forward[i];
        }
        // 当前节点的下一个节点的键值大于待查找的键值时,进行下沉到下一层
        // 下沉操作通过循环的 i-- 实现
    }
    // 检查当前层(最底层)的下一个节点的键值是否为待查找的键值
    current = current->forward[0];
    if (current && current->get_key() == key) {
        // 如果找到匹配的键值,返回 true
        return true;
    }
    // 如果没有找到匹配的键值,返回 false
    return false;
}

3. 实现插入方法

3.1 理论基础

继搜索节点的逻辑之后,我们现在转向如何在跳表中插入新节点。

插入过程主要涉及三个关键步骤:

1、确定节点层级

首先,我们需要为新插入的节点随机确定其所在的层级

2、寻找插入位置

通过之前讨论的搜索方法,我们能够定位到了新节点应当插入的具体位置

3、更新指针关系

最关键的步骤是在插入节点时更新各层的指针关系。

具体而言,这包括两个方面:

  1. 将新节点在各层的前驱节点(即在该层中小于新节点且最接近新节点的节点)的 forward 指针指向新节点。
  2. 同时,新节点的 forward 指针需要指向其在各层的前驱节点原本指向的节点。
    此操作和单链表的插入操作类似,区别在于跳表需要在多层中的重复进行此操作,而链表只需要进行一次。

3.2 代码实现

在插入新节点前,我们首先需要定位插入位置,此过程与 search_element 函数的逻辑相似。下面的代码框架展示了如何执行这一操作:

template <typename K, typename V>
int SkipList<K, V>::insert_element(const K key, const V value) {
    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--) {
        // 寻找当前层中最接近且小于 key 的节点
        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) {
        // 键已存在,取消插入
        return 1;
    }
    // 后续插入操作(略)
}

在这段代码中,Node<K, V>* update[_max_level + 1] 是用于实现插入节点的关键数据结构,它是一个节点指针数组,用于记录在上文中提到的,待插入节点的前驱节点(即在该层中小于新节点且最接近新节点的节点)。这个数组解决了之前提到的关键问题:在插入新节点时如何更新每层的指针关系。

通过内层的 while 循环,一旦发现 current->forward[i] 指向的节点的 key 值 > 待插入节点的 key,那么 current 就是待插入节点的前驱节点。而通过外层的 for 循环,我们可以寻找出待插入节点在不同层的所有前驱节点。

接下来的判断逻辑是为了确保不会插入重复的节点。如果 current 指向的节点的 key 与待插入的节点的 key 相等,说明跳表中已存在与待插入节点相同 key 的节点,此时我们只需要将该节点的 value 更新,并且返回 1;

继续深入跳表的插入逻辑,以下是插入操作的代码实现:

template <typename K, typename V>
int SkipList<K, V>::insert_element(const K key, const V value) {
    // ...
    // 检查待插入的节点是否已存在于跳表中
    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; i++) {
                update[i] = _header;
            }
            // 更新跳表的当前最高层级为新节点的层级
            _skip_list_level = random_level;
        }
        
        Node<K, V> *inserted_node = create_node(key, value, random_level);
        // 在各层插入新节点,同时更新前驱节点的forward指针
        for (int i = 0; i <= random_level; i++) {
            // 新节点指向当前节点的下一个节点
            inserted_node->forward[i] = update[i]->forward[i];
            // 当前节点的下一个节点更新为新节点
            update[i]->forward[i] = inserted_node;
        }
        _element_count++;
    }
    return 0;
}

当新插入节点的层级高于跳表当前层级时,我们需要在 update 数组中为这些新层级指定头节点(_header),因为这些层级在插入之前是不存在节点的。这样,新节点在这些高层级直接作为第一个节点。

新节点按照确定的层级被插入。对每一层,我们首先设置新节点的 forward 指针指向当前节点的下一个节点,然后更新当前节点的 forward 指针指向新节点。这一过程确保了新节点正确地被链入每一层。

通过这些步骤,我们不仅完成了新节点的插入操作,还确保了跳表结构的正确性和索引的有效维护。

3.3 完整代码与解释

template <typename K, typename V>
int SkipList<K, V>::insert_element(const K key, const V value) {
        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--) {
        // 寻找当前层中最接近且小于 key 的节点
        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) {
        // 键已存在,取消插入
        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; i++) {
                update[i] = _header;
            }
            // 更新跳表的当前最高层级为新节点的层级
            _skip_list_level = random_level;
        }
        
        Node<K, V> *inserted_node = create_node(key, value, random_level);
        // 在各层插入新节点,同时更新前驱节点的 forward 指针
        for (int i = 0; i <= random_level; i++) {
            // 新节点指向当前节点的下一个节点
            inserted_node->forward[i] = update[i]->forward[i];
            // 当前节点的下一个节点更新为新节点
            update[i]->forward[i] = inserted_node;
        }
        _element_count++;
    }
    return 0;
}

4.小结

实现了 SkipList 类中搜索节点和插入节点的成员函数:

#include <iostream>
#include <cstdlib>
#include <cmath>
#include <cstring>

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;
};

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));
};

template <typename K, typename V>
Node<K, V>::~Node() {
    delete[] forward;
};

template <typename K, typename V>
K Node<K, V>::get_key() const {
    return key;
};

template <typename K, typename V>
V Node<K, V>::get_value() const {
    return value;
};

template <typename K, typename V>
void Node<K, V>::set_value(V value) {
    this->value = value;
};

// 跳表结构
template <typename K, typename V>
class SkipList {
public:
    SkipList(int);
    int get_random_level();
    Node<K, V> *create_node(K, V, int);
    int insert_element(K, V);
    bool search_element(K);
private:
    int _max_level;
    int _skip_list_level;
    Node<K, V> *_header;
    int _element_count;
};

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;
}

template <typename K, typename V>
int SkipList<K, V>::insert_element(const K key, const V value) {
    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) {
        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;
        }
        _element_count++;
    }
    return 0;
}

template <typename K, typename V>
bool SkipList<K, V>::search_element(K key) {
    Node<K, V> *current = _header;
    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];
    if (current and current->get_key() == key) {
        return true;
    }
    return false;
}

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;
    K k;
    V v;
    this->_header = new Node<K, V>(k, v, _max_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;
};

int main() {
    int N;
    int M;
    SkipList<int, int> *skip_list = new SkipList<int, int>(16);
    std::cin >> N >> M;
    for (int i = 0; i < N; i++) {
        int key;
        int value;
        std::cin >> key >> value;
        if (skip_list->insert_element(key, value) == 0) {
            std::cout << "Insert Success" << std::endl;
        } else {
            std::cout << "Insert Failed" << std::endl;
        }
    }

    // 搜索
    for (int i = 0; i < M; i++) {
        int key;
        std::cin >> key;
        if (skip_list->search_element(key)) {
            std::cout << "Search Success" << std::endl;
        } else {
            std::cout << "Search Failed" << std::endl;
        }
    }
    return 0;
}
  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
跳表是一种数据结构,可以高效地实现有序链表和有序集合,可以用来实现排行榜。跳表是通过在原始链表中添加多级索引节点来加速查找操作的。 跳表实现思路如下: 1. 创建一个带有头节点的链表,头节点的值为负无穷大,尾节点的值为正无穷大。 2. 在原始链表中,插入新的节点时,根据节点的值,决定是否在当前层级上添加索引节点。添加索引节点的概率可以根据需求进行调整。 3. 使用索引节点可以将跳表分为多个层级(level),每一层级都是一个有序链表。 4. 查询操作时,从最高层级开始,从左向右逐层搜索,直到找到目标值所在的区间(比目标值大的最小节点和比目标值小的最大节点之间)。 5. 对于插入和删除操作,首先在最底层进行,然后根据概率决定是否在上层级插入或删除对应的节点。 使用跳表实现排行榜的步骤如下: 1. 创建一个跳表,每个节点存储着用户的信息,包括用户的排名、分数等。 2. 初始化排行榜时,将所有用户按照分数从大到小顺序插入跳表中。 3. 当有新的用户加入或者用户的分数发生变化时,根据新的分数更新用户节点的位置。 4. 当需要查询某个用户的排名时,可以通过跳表中的索引节点,快速定位到该用户所在的层级,然后在该层级中按照顺序遍历找到目标节点,并返回排名。 通过以上步骤,我们可以使用跳表高效地实现排行榜功能。跳表插入、删除和查找操作的时间复杂度都可以达到O(log n),在大数据量下具有较高的效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值