【数据结构】动手实现一个简单的跳表!

引入

跳表是对链表结构的一种优化。

链表的缺点是无法随机访问节点,每次访问元素都需要进行遍历,时间复杂度为 O ( n ) O(n) O(n)

在这里插入图片描述

一个基本的改进思路是增加一些指针,帮助跳跃,减少遍历次数

首先,在链表的前面增加一个“哨兵节点”,并把该链表当作 L0 层(不进行跳越)。

在这里插入图片描述

跳表是一种随机数据结构,随机地决定节点高度是否增长

现在,我们开始来搭建 L1 层,即:从 L0 层的哨兵节点开始,遍历链表,随机地决定该节点是否“长高”。

在这里插入图片描述

我们再来搭建 L2 层,即:从 L1 层的哨兵节点开始,遍历链表,随机地决定该节点是否“长高”。

【数据结构】动手实现一个简单的跳表!_第1张图片

以上就是跳表的结构原理,通过搭建“跳越层”,我们可以实现更快的搜索和插入节点元素!

跳表的层数最好为 O ( l o g n ) O(logn) O(logn) 层,因为这样可以让搜索效率和插入效率都为 O ( l o g n ) O(logn) O(logn)

代码实现

SkipList 定义

现在,我们来实现对节点的定义,Skip List 的节点定义如下:

const int MAX_LEVEL = 16; // 最大层数

// 跳表节点
struct Node {
    int key;
    int value;
    // 用数组存放指针,表示最多可以有 MAX_LEVEL 层
    std::vector<Node*> forward;

    Node(int k, int v, int level) : key(k), value(v), forward(level, nullptr) {}
};

我们定义的是一个具有 key 和 value 的节点,相比单一的 value,更为贴切 K-V 存储系统。

接下来,是对跳表结构的定义:

class SkipList{
public:
    SkipList() : level(1), head(new Node(0, 0, MAX_LEVEL)) {}
    ~SkipList() {
        Node* current = head;
        while (current) {
            Node* next = current->forward[0];
            delete current;
            current = next;
        }
    }
    
    int search(int key);
    void insert(int key, int val);
    void remove(int key);
  
private:
    int randomLevel();  // 随机决定新节点的层数
  
    Node* head;
    int level;
}

在上面的定义中,我们看到了 randomLevel 的声明,现在试着来实现它。我们已经知道跳表是一种 随机数据结构 ,那怎么设置随机的*「概率」*呢?

这就涉及到一个*「概率更新算法」*;对于一个新插入的节点,将它的层数每提升一级的概率设置为1/2。

那么可知,L0 层的节点提升至 L2 层的概率为1/4,L0 层的节点提升至 L3 层的概率为1/8 …

int SkipList::randomLevel(){
    int level = 1;
    while(rand() % 2 && level < MAX_LEVEL){   // 随机数生成 0 或 1,rand() 在头文件 
        level++;
    }
    return level;
}

搜索函数实现

SkipList 的搜索思路为,从 head 的最高层开始,如果节点值小于搜索值,向后遍历;否则,向下一层寻找;遍历之后,当前位置为目标节点(如果存在的话)的前一个节点,因此需要再往后移动一位。具体代码如下:

int SkipList::search(int key){
    Node* current = head;
  
    // 从最高层开始找起
    for(int i = level - 1; i >= 0; --i){
        while(current[i] && current->forward[i]->key < key){
            current = current->forward[i];
        }
    }
    
    current = current->forward[0];
    
    if(current && current->key == key) return current->value;
    return -1;  // 未找到
}

插入代码实现

现在我们来看插入节点的情况;假设在跳表中插入一个 D2 节点,且随机层数为 4 层,则插入效果如下:

【数据结构】动手实现一个简单的跳表!_第2张图片

从上图可知,如果新节点的层数大于 1,则需要让对应层数的*「前一个节点」指向新节点,而新节点指向原先该节点的「后一个节点」,相当于每层都需要进行一次“单链表插入”*。

因为我们不知道一个新节点的层数会是多少;所以,我们需要记录一个节点数值 Node* update[MAX_LEVEL],并在搜索插入位置的过程中记录「新节点在每层中的前一个节点」。具体的代码如下:

void insert(int key, int value) {
    Node* current = head;
    std::vector<Node*> update(MAX_LEVEL, nullptr);  // 记录新节点在每层中的前一个节点

    // 找到每层中要插入的位置
    for (int i = level - 1; i >= 0; --i) {
        while (current->forward[i] && current->forward[i]->key < key) {
            current = current->forward[i];
        }
        update[i] = current;
    }

    current = current->forward[0];

    // 如果键已存在,则更新值
    if (current && current->key == key) {
        current->value = value;
    } 
    else {
        // 随机确定新节点的层数
        int newLevel = randomLevel();

        if (newLevel > level) {
            for (int i = level; i < newLevel; ++i) {
                update[i] = head;
            }
            level = newLevel;
        }

        // 创建新节点
        Node* newNode = new Node(key, value, newLevel);

        // 更新前后连接,即:在每层中进行一次 “单链表插入”
        for (int i = 0; i < newLevel; ++i) {
            newNode->forward[i] = update[i]->forward[i];
            update[i]->forward[i] = newNode;
        }
    }
}

删除代码实现

最后再来看删除节点的代码,和插入一个新节点一样,需要记录节点(如果存在)在每层中的 「前一个节点」,并且需要删除节点在每层中的引用。

void remove(int key){
    Node* current = head;
    std::vector<Node*> update(MAX_LEVEL, nullptr);  // 记录待删除的节点在每层中的前一个节点
    
    // 找到要删除的节点
    for(int i = level - 1; i >= 0; --i){
        while(forward[i] && current->forward[i]->key < key){
            current = current->forward[i];
        }
        update[i] = current;
    }
    
    current = current->forward[0];
    
    if(current && current->key == key){
        // 删除该节点在各层中的引用
        for(int i = 0; update[i]->forward[i] == current; ++i){
            update[i]->forward[i] = current->forward[i];
        }
        
        delete current;
      
        // 更新跳表的层数
        while(level > 1 && head->forward[level - 1] == nullptr){
            --level;
        }
    }
}

总结

OK,我们现在已经实现了一个最基本的跳表!

后续我们可以为跳表的增加 文件操作,使得我们的数据可以保存下来;也可以将这个跳表改写称一个模板类,让它支持更多类型;另外,还可以加入互斥锁(mutex)来确保线程安全。

你可能感兴趣的:(数据结构与算法,数据结构)