skiplist

跳表的定义和理解

参考csdn的博主文章:

跳表的定义

理解一个key对应的节点只有一个。其实只有一个节点,但是用一个来forward[_max_level]表示每一层的指针关系,多层的关系其实是体现在指针上,而不是该节点的实体上

整个节点和跳表的设计

  • 节点的设计
    一个节点应该拥有的成员变量有

public variable: node_level(表示该节点处在哪一个层次上面),forward

private variable: key ,value

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);
    
    //forward保存的是同一个级别的下一个节点的指针,所以是指针数组,所以用两个*表示
    Node<K, V> **forward;
    

    int node_level;//用于标记这个节点是处于哪一个层次,这个点很重要

private:
    K key;
    V value;
};
  • 跳表的设计
    目前实现的是增删改查以及和文件的操作,后续补充

forward数组的理解和用法

*** 下图中绿色的部分就是_header***

  • 这个forward表示的是我不同等级的同一个key-value的一个数组,可能这样子说不好理解,那么用下面这张图片理解比较好

forwards保存了每一层上的索引信息:

image

forwards描述了上图中标红的部分,16是通过该节点的value属性保存的。只有理解了forward数组才能比较好理解后续的操作
实际上核心的代码就是在这个forward上和同一层的相邻元素之间跳转实现的

  • 那么current->forward[i]表示的是什么含义呢?
    表示的是这个节点current。例如值是16,那么16->forward[i]表示的16这个节点在第i层的下一个节点,那么current->forward[1]就是71节点

核心代码(在search_element()函数开始理解这个代码,因为后面的所有增删改查的操作都是在“查到”的基础之上你才能操作)

我们需要理解一点,那就是每次增删改查操作完毕跳表都是处在那一层,通过自增的函数return_now_level打印结果得知为最上层
注意以上说的最上层不是_max_level

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];
    }
}*/
//用一下的表示或许更好理解,把!=null加上
for (int i = _skip_list_level; i >= 0; i--) {
    while (current->forward[i] != NULL && current->forward[i]->get_key() < key) {
        current = current->forward[i];
    }
}

以上的代码十分巧妙,首先最外面的for循环是为了向下跳,里面的while循环是为了同i层平行跳,而每一层的临界的时候下一层for循环都自动做了一件事情
那就是从current->forward[i]current->forward[i-1] 的变化,那么最后出来的current就是0层要查找的节点的上一个节点。

具体的函数理解

search_element(k key)

  • 通常对一个节点的操作都是查找该节点的上一个节点,所以首先和单链表的操作一样,先把current节点置成头节点,然后开始往后循环:
    Node<K, V> *current = _header;
    
  • 尝试开始查找该节点的上一个节点,也就是我们核心的代码:
    for (int i = _skip_list_level; i >= 0; i--) {
       while (current->forward[i] != NULL && current->forward[i]->get_key() < key) {
           current = current->forward[i];
       }
    }
    
    出来之后的current为我们要查找的节点的上一个节点位置,但是注意的是current为0层的元素。
    * 将current后移一位,注意这个时候已经是0层:
    current = current->forward[0];
    
    此时出来的current应该是我们要查找的节点。
    * 判断该节点的key是要查找的key,那么返回答案,如不是,那么说明查找失败:
    if (current && current->get_key() == key) {//用and和&&是一样的
      std::cout << "Found key: " << key << ", value: " << current->get_value() << std::endl;
      return true;
    }
    std::cout << "Not Found Key:" << key << std::endl;
    return false;
    

insert_element(const k key, const v value)

  • 首先应该和search_element一样,取出_header赋值给current,然后开始循环,因为插入节点的时候需要考虑该不该建立该节点的分层结构,
    所以我们需要一个数组记录查找过程中走过的节点,以便后续建立分层:
    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;
    }
    
    注意update数组记录的是每走过一层的转角位置
  • current后移一位,注意这个时候已经是0层:
    current = current->forward[0];
    
    此时出来的current应该是我们要插入的节点位置。
  • 这个时候的current要做如下的判断:
     if (current != NULL && current->get_key() == key) {//如果这个节点已经存在那么不做插入操作
        std::cout << "key: " << key << ", exists" << std::endl;
        //mtx.unlock();
        return 1;
    }
    
  • 如果这个节点不存在,那么就要开始考虑插入问题了,那么应该插入几层呢?是由主观判断的吗?
    其实不然,我们通过以一个随机产生的层数来判断我们应该为该节点建立几层
    int random_level = get_random_level();//通过get_random_level函数获得一个随机的层数
    //如果说得到的random_level > _skip_list_level,也就是说这个时候应该扩展_header竖向长度
    if (random_level > _skip_list_level) {
          //为什么从_skip_list_level开始呢?因为在0-_skip_list_level之前的所有_header已经扩展成_skip_list_level长度了
          //只需要继续往上扩展_header
          for (int i = _skip_list_level+1; i < random_level+1; i++) {
              update[i] = _header;
          }
          //将当前跳表的层数停留在当前数据的最上层
          _skip_list_level = random_level;
     }
    
  • 建立该节点的o - random_level层的数据insert:(这个时候的update数组就有大用了,因为你要根据这个数组来insert该节点)
    // 新建一个要插入的节点
      Node<K, V>* inserted_node = create_node(key, value, random_level);
    
      // 在o - random_level层插入这个节点 ,插入思想和链表的插入是一样的,就是多了一个层数
      for (int i = 0; i <= random_level; i++) {
          inserted_node->forward[i] = update[i]->forward[i];
          update[i]->forward[i] = inserted_node;
      }
      //插入数据成功之后元素数量加1
      _element_count++;
    

delete_element(k key)

  • 前面的大部分代码和insert_element()代码差不多,同样要维护一个update数组,用于标记转角处的元素,因为每次都要借助转角元素进行插入或者删除
    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];
    
  • 如果找到该节点,也就是current != NULL && current->get_key() == key
     for (int i = 0; i <= _skip_list_level; i++) {
    
         // 该层节点不存在,那么直接跳出循环
         /*
                                  +------------+
                                  |  delete 4 |
                                  +------------+
        level 4     +-->1+                                                     
                        |
                        |                     
        level 3         1+          10
                        |
                        |                                                             
        level 2         1+          10    --------->这一层没有4,所以执行到该层的时候直接跳出循环    
                        |
                        |                                                           
        level 1         1+    4     10
                        |
                        |                                                     
        level 0         1     4  9  10        
    
    
        */
    
         if (update[i]->forward[i] != current) 
             break;
    
         update[i]->forward[i] = current->forward[i];//删除i层的current元素
     }
     // 如果有的层元素删除没有了(只能是最上面的层),那么_skip_list_level应该更新了
     while (_skip_list_level > 0 && _header->forward[_skip_list_level] == 0) {
         _skip_list_level --; 
     }
    
     //元素个数减一
     _element_count --;
    

display_list()

  • 从第零层开始打印元素,从而展示整个跳表
    template<typename K, typename V> 
    void SkipList<K, V>::display_list() {
    
        std::cout << "\n*****Skip List*****"<<"\n"; 
        for (int i = 0; i <= _skip_list_level; i++) {
            //下面这条语句的作用是取出第i行的头节点利于后面的打印
            Node<K, V> *node = this->_header->forward[i]; 
            std::cout << "Level " << i << ": ";
            while (node != NULL) {
                std::cout << node->get_key() << ":" << node->get_value() << ";";
                node = node->forward[i];
            }
            std::cout << std::endl;
        }
    }
    

dump_file()//将内存中的数据存储到文件夹下

  • 只要注意文件夹的打开方式和关闭方式,还有如何写进数据即可
    template<typename K, typename V> 
    void SkipList<K, V>::dump_file() {
    
        std::cout << "dump_file-----------------" << std::endl;
        _file_writer.open(STORE_FILE, std::ios::out);//以只写的
        Node<K, V> *node = this->_header->forward[0]; 
    
        while (node != NULL) {
            _file_writer << node->get_key() << ":" << node->get_value() << "\n";//采用<<方式写进数据
            std::cout << node->get_key() << ":" << node->get_value() << ";\n";
            //存储的话那就是存储的最底层的节点,是没有缺少元素的
            node = node->forward[0];
        }
    
        _file_writer.flush();//实时出现刷新
        _file_writer.close();
        return ;
    }
    

load_file()//将文件夹下的数据加载到内存中

  • getline(std::istream &filename, std::string &_str)
    作用是将该filename文件的每一行读取进来,并且赋值给_str,每次执行之后鼠标停留在
  • get_key_value_from_string(const std::string& str, std::string* key, std::string* value)函数
    作用是将由get_line函数获取的每一行元素中的keyvalue取出来
    源代码中有些许测试问题,因为测试的时候我们用的是K = int,V = string类型所以针对该类型进行了代码改进
    template<typename K, typename V> 
    void SkipList<K, V>::load_file() {
    
        _file_reader.open(STORE_FILE);
        std::cout << "load_file-----------------" << std::endl;
        std::string line;
        std::string* key = new std::string();
        std::string* value = new std::string();
        while (getline(_file_reader, line)) {
            get_key_value_from_string(line, key, value);
            //为了临时测试int,string而加入的代码
            K key_string_to_K = atoi((*key).c_str());
            if (key->empty() || value->empty()) {
                continue;
            }
            //为了临时测试改变了insert_element()函数第一个参数的写法
            insert_element(key_string_to_K, *value);
            std::cout << "key:" << *key << "value:" << *value << std::endl;
        }
        _file_reader.close();
    }
    
posted @ 2023-09-02 16:47  铜锣湾陈昊男  阅读(12)  评论(0)    收藏  举报