跳表--C++实现

news2025/1/15 12:50:10

目录

作者有话说

为何要学习跳表?为了快,为了更快,为了折磨自己.....

跳表作用场景 

1.不少公司自己会设计哈希表,如果解决哈希冲突是不可避免的事情。通常情况下会使用链址,很好理解,当有冲突产生时,我们在附加的链表中添加一位(如果使用的循环双链表没直接在后面加,效率很高O(1)。如果是单链表也可以使用头插法直接在头部添加,效率很高O(1),如果使用跳表需要O(logN))。

2.看到这里不少朋友会觉得,那需要什么跳表呀,还变慢了。不要着急,我们继续往下看,首先使用简单的头差法,得到的序列可以认为是乱序(不考虑依次插入是有序的),那么查找起来会变得比较费劲,需要O(N)。但是使用跳表查找的平均效率是O(logN),插入O(logN),删除O(logN)。

【注意这里写的是平均情况,如果跳表设计不好,很容易导致跳表退化成链表】

3.跳表有时候可以代替红黑树和AVL树,甚至说跳表的插入和删除的维护比AVL树代价低与红黑树差不多。

 跳表的主要思想

二分!二分!还是二分!

接触了这么久的编程,会发现很多比较优秀的算法都是基于二分思想演变而来的。

至于算法如何选择,需要结合具体的业务情况,一个算法的最好时间复杂度、平均时间复杂度、最坏时间复杂度都是需要考虑的。两个相同功能的算法在不同应用场景下会有很大是差距,即使平时我们都认为他们的时间复杂度都是O(logN).

与时间复杂度具有类似概念的就是空间复杂度了,空间复杂度也是需要考虑的问题。一个占据CPU、一个占据内存....

空间复杂度总是与时间复杂度此消彼长,他们向一对冤家。但是通常情况下,我们普遍接受在允许的内存消耗内,选择最快的算法。天下武功为快不破....

跳表的特点

1.单向链表(这个不是绝对的,如果想居于范围查找,使用双向链表会更快 ==> 未测试过)

2.有序保存(二分的前提条件)

3.支持添加、删除和查找

4.查找效率 O(logN)

 跳表与普通链表的区别

普通单链表

 

         对于普通的单链表,越靠前的节点,查找快。对于越靠后的节点查找效率越低。其平均效率 = (1+ 2 + 3 + ... + n) / n = (1/2 * n(a1+an))/n = (1 + n) / 2  ==> O(N)

 

简单的跳表

   

         跳表的结构是通过建立高纬度的索引来减少低纬度从而达到任何元素的检索接近线性时间的目的O(logN)。其实跳表的思想并不复杂,为了提高查找效率将中间节点提高维度,在查找过程中逐步的对半减小查找过程。

跳表的建立 

        我们知道了跳表的基本思想后,我们来手动模拟建立一个跳表。现在我们依次顺序插入1 5 8 3 2 7 9 这七个元素。===> 【我们采用间隔一个的方式提取高纬度的节点】

        1.初始化 ==> 准备一个头部节点,我们知道链表有两种方式(一种是有头节点的、一种是没有头节点的,为了方便对第一个节点的操作,我们统一使用有头节点的)。

          注意:指针域这里没有画出来

        2.插入 1 ==> 像单链表一样直接将节点插入到右边就可以了。

         注意: 空白部分代表指针指向NULL,防止野指针

        3.插入 5 ==> 这里考虑将向上提取元素(也可以不提取),先查找需要查找的位置,5 > 1所以插入到1的后面就可以了。

           ==> 为了保证隔一个的一致性和写代码时少考虑一种情况,我们还是向上提取 5

          注意: 空白部分代表指针指向NULL,防止野指针

        4. 插入 8 ==> 还是同样的道理,我们需要提取查找8所需要查找的位置。8 > 5因为提前做了5的提升,我们不需要比较1,然后再比较5,我们直接拿到5就可以知道8应该插入到5的后面,跳表的优势就表现出来了。

        【这个过程插入过程分成了两个步骤,先在1维链表中插入8】

         注意:空白部分代表指针指向NULL,防止野指针

      ​​​​​​​ 注意: 空白部分代表指针指向NULL,防止野指针

        5.插入 3 ==> 我们先比较 5 > 3,那么整个范围变成了 【1-5】,然后 1 < 3 < 5 。那么需要将3插入到1和5之间

        【将3插入,按两个提取的原则,需要将3提升,然后 3、5在同一层,需要将5提升】

        ​​​​​​​ 注意:空白部分代表指针指向NULL,防止野指针

        6.插入 2 ==> 过程同上面一样。不做过多的解释了....

        【画图不易,点个关注吧....只输出优质文章:大家有什么想算法,可以下方留言

        注意:空白部分代表指针指向NULL,防止野指针

        7.插入 7 ==> 过程同上面一样。不做过多的解释了....

        注意:空白部分代表指针指向NULL,防止野指针

       8.插入 9 ==> 过程同上面一样。不做过多的解释了.... 

        注意:空白部分代表指针指向NULL,防止野指针

      【以上就是跳表建立的全部过程,如果不明白可以加入微信C++技术交流群:C++技术交流群-陈达叔

       【bilibili 搜索 陈达叔视频后续会更新,感谢大家关注....创作不易,点个关注吧...】  ​

跳表的查找

  跳表的查找很简单,就是二分查找的逻辑,为了更好的了解查找的过程,我们来看下示例...

  示例1: 查找 3

  先比较 5 ==> 3 < 5   ==> 查找范围变成 1-5之间

  然后 header向下移动一位,比较 3 == 3 ==> 找到 3 返回 true

  

  示例2:查找 9 

  

跳表的删除

  跳表的查找很简单,就是二分查找的逻辑,为了更好的了解查找的过程,我们来看下示例...

  示例1:删除 2

  

 为了防止不断是增加,层数过高,我们可以做个调整

跳表的提取维度说明

        不难发现,跳表的实现就是对数据做了向上的冗余操作,以时间换空间的的典型。

当我们设置的冗余颗粒越小,那么需要的空间越大。所以选择好的冗余颗粒度很重要,这个需要根据具体情况而定。

        再跳表存在一个随机函数,其存在的意义是决定什么时候需要向上做冗余。实际的建造过程不会像我们示例一样的建造,这样代价会比较高。为了防止跳表插入节点增加而导致退化成链表的情况,我们通常通过一个随机函数来决定向上做提升的时刻。

跳表的代码 

#pragma once
#ifndef SKIPLIST_ENTRY_H_
#define SKIPLIST_ENTRY_H_
/* 一个更具备代表性的泛型版本 */
#include <ctime>
#include <cstdlib>

template<typename T>
class Entry {
private:
    int key; // 排序值
    T value; // 保存对象
    Entry* pNext;
    Entry* pDown;
public:
    
    Entry(int k, T v) :value(v), key(k), pNext(nullptr), pDown(nullptr) {}
    
    Entry(const Entry& e) :value(e.value), key(e.key), pNext(nullptr), pDown(nullptr) {}

public:
    /* 重载运算符 */
    bool operator<(const Entry& right) {
        return key < right.key;
    }
    bool operator>(const Entry& right) {
        return key > right.key;
    }
    bool operator<=(const Entry& right) {
        return key <= right.key;
    }
    bool operator>=(const Entry& right) {
        return key >= right.key;
    }
    bool operator==(const Entry& right) {
        return key == right.key;
    }
    Entry*& next() {
        return pNext;
    }
    Entry*& down() {
        return pDown;
    }
};

template<typename T>
class SkipList_Entry {
private:
    struct Endpoint {
        Endpoint* up;
        Endpoint* down;
        Entry<T>* right;
    };
    struct Endpoint* header;
    int lvl_num; // level_number 已存在的层数
    unsigned int seed;
    bool random() {
        srand(seed);
        int ret = rand() % 2;
        seed = rand();
        return ret == 0;
    }
public:
    SkipList_Entry() :lvl_num(1), seed(time(0)) {
        header = new Endpoint();
    }
    /* 插入新元素 */
    void insert(Entry<T>* entry) { // 插入是一系列自底向上的操作
        struct Endpoint* cur_header = header;
        // 首先使用链表header到达L1
        while (cur_header->down != nullptr) {
            cur_header = cur_header->down;
        }
        /* 这里的一个简单想法是L1必定需要插入元素,而在上面的各跳跃层是否插入则根据random确定
           因此这是一个典型的do-while循环模式 */
        int cur_lvl = 0; // current_level 当前层数
        Entry<T>* temp_entry = nullptr; // 用来临时保存一个已经完成插入的节点指针
        do {
            Entry<T>* cur_cp_entry = new Entry<T>(*entry); // 拷贝新对象
            // 首先需要判断当前层是否已经存在,如果不存在增新增
            cur_lvl++;
            if (lvl_num < cur_lvl) {
                lvl_num++;
                Endpoint *new_header = new Endpoint();
                new_header->down = header;
                header->up = new_header;
                header = new_header;
            }
            // 使用cur_lvl作为判断标准,!=1表示cur_header需要上移并连接“同位素”指针
            if (cur_lvl != 1) {
                cur_header = cur_header->up;
                cur_cp_entry->down() = temp_entry;
            }
            temp_entry = cur_cp_entry;
            // 再需要判断的情况是当前所在链表是否已经有元素节点存在,如果是空链表则直接对右侧指针赋值并跳出循环
            if (cur_header->right == nullptr) {
                cur_header->right = cur_cp_entry;
                break;
            }
            else {
                Entry<T>* cursor = cur_header->right; // 创建一个游标指针
                while (true) { // 于当前链表循环向右寻找可插入点,并在找到后跳出当前循环
                    if (*cur_cp_entry < *cursor) { // 元素小于当前链表所有元素,插入链表头
                        cur_header->right = cur_cp_entry;
                        cur_cp_entry->next() = cursor;
                        break;
                    }
                    else if (cursor->next() == nullptr) { // 元素大于当前链表所有元素,插入链表尾
                        cursor->next() = cur_cp_entry;
                        break;
                    }
                    else if (*cur_cp_entry < *cursor->next()) { // 插入链表中间
                        cur_cp_entry->next() = cursor->next();
                        cursor->next() = cur_cp_entry;
                        break;
                    }
                    cursor = cursor->next(); // 右移动游标
                }
            }
        } while(random());
    }

    /* 查询元素 */
    bool search(Entry<T>* entry) const {
        if (header->right == nullptr) { // 判断链表头右侧空指针
            return false;
        }
        Endpoint* cur_header = header;
        // 在lvl_num层中首先找到可以接入的点
        for (int i = 0; i < lvl_num; i++) {
            if (*entry < *cur_header->right) {
                cur_header = cur_header->down;
            }
            else {
                Entry<T>* cursor = cur_header->right;
                while (cursor->down() != nullptr) {
                    while (cursor->next() != nullptr) {
                        if (*entry <= *cursor->next()) {
                            break;
                        }
                        cursor = cursor->next();
                    }
                    cursor = cursor->down();
                }
                while (cursor->next() != nullptr) {
                    if (*entry > *cursor->next()) {
                        cursor = cursor->next();
                    }
                    else if (*entry == *cursor->next()) {
                        return true;
                    }
                    else {
                        return false;
                    }
                }
                return false; // 节点大于L1最后一个元素节点,返回false
            }
        }
        return false; // 找不到接入点,则直接返回false;
    }
    /* 删除元素 */
    void remove(Entry<T>* entry) {
        if (header->right == nullptr) {
            return;
        }
        Endpoint* cur_header = header;
        Entry<T>* cursor = cur_header->right;
        int lvl_counter = lvl_num; // 因为在删除的过程中,跳跃表的层数会中途发生变化,因此应该在进入循环之前要获取它的值。
        for (int i = 0; i < lvl_num; i++) {
            if (*entry == *cur_header->right) {
                Entry<T>* delptr = cur_header->right;
                cur_header->right = cur_header->right->next();
                delete delptr;
            }
            else {
                Entry<T> *cursor = cur_header->right;
                while (cursor->next() != nullptr) {
                    if (*entry == *cursor->next()) { // 找到节点->删除->跳出循环
                        Entry<T>* delptr = cursor->next();
                        cursor->next() = cursor->next()->next();
                        delete delptr;
                        break;
                    }
                    cursor = cursor->next();
                }
            }
            // 向下移动链表头指针的时候需要先判断当前链表中是否还存在Entry节点
            if (cur_header->right == nullptr) {
                Endpoint* delheader = cur_header;
                cur_header = cur_header->down;
                header = cur_header;
                delete delheader;
                lvl_num--;
            }
            else {
                cur_header = cur_header->down;
            }
        }
    }
};
#endif // !SKIPLIST_ENTRY_H_

    对于C++的学习,存在一个的最大问题就是很少可以交流的人,甚至而言网上的资料又比较少。

   如果对c++有疑惑或者想要交流的朋友记得加入V: Errrr113 

    坚持初心,勇敢果断....至每一个热爱技术的朋友!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/387726.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习训练营之识别宝可梦人物和角色

深度学习训练营之识别宝可梦人物和角色原文链接环境介绍前置工作设置GPU数据加载数据查看数据预处理加载数据可视化数据检查数据配置数据集prefetch()功能详细介绍&#xff1a;调用官方的网络的模型模型训练官方模型调用设置动态学习率模型训练模型评估结果分析参考链接原文链接…

【Redis】Redis 如何实现分布式锁

Redis 如何实现分布式锁1. 什么是分布式锁1.1 分布式锁的特点1.2 分布式锁的场景1.3 分布式锁的实现方式2. Redis 实现分布式锁2.1 setnx expire2.2 set ex px nx2.3 set ex px nx 校验唯一随机值&#xff0c;再删除2.4 Redisson 实现分布式锁1. 什么是分布式锁 分布式锁其实…

【C语言进阶:指针的进阶】回调函数

本章重点内容&#xff1a; 字符指针指针数组数组指针数组传参和指针传参函数指针函数指针数组指向函数指针数组的指针回调函数指针和数组面试题的解析什么是回调函数&#xff1a; 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针&#xff08;地址&#xff09;作…

Lenovo 联想-IdeaPad-Y530电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。硬件型号驱动情况主板联想-IdeaPad-Y530处理器Intel 酷睿2双核 T9400已驱动内存2GB已驱动硬盘2TB HP EX950 PCI-E Gen3 x4 NVMe SSD已驱动显卡NVIDIA GeForce 9300M GS无法驱动声卡Realtek ALC888无法驱动网卡RTL8168H Giga…

【Java学习笔记】3.Java 基础语法

Java 基础语法 一个 Java 程序可以认为是一系列对象的集合&#xff0c;而这些对象通过调用彼此的方法来协同工作。下面简要介绍下类、对象、方法和实例变量的概念。 对象&#xff1a;对象是类的一个实例&#xff0c;有状态和行为。例如&#xff0c;一条狗是一个对象&#xff…

【NLP相关】从零开始理解BERT模型:NLP领域的突破(BERT详解与代码实现)

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️&#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

Python异常处理更新,正常和不正常的都在这里

嗨害大家好鸭&#xff01;我是小熊猫~ 异常处理篇嗨害大家好鸭&#xff01;我是小熊猫~Python标准异常&#x1f4a8;什么是异常&#xff1f;不正常异常处理&#x1f4a8;使用except而不带任何异常类型使用except而带多种异常类型try-finally 语句异常的参数触发异常用户自定义异…

Lesson12---人工神经网络(1)

12.1 神经元与感知机 12.1.1 感知机 感知机&#xff1a; 1957&#xff0c; Fank Rosenblatt 由两层神经元组成&#xff0c;可以简化为右边这种&#xff0c;输入通常不参与计算&#xff0c;不计入神经网络的层数&#xff0c;因此感知机是一个单层神经网络 感知机 训练法则&am…

MyBatis - 13 - MyBatis逆向工程

文章目录1.准备工作1.1 建表1.2 创建Maven工程1.2.1 在pom.xml中添加依赖和插件&#xff0c;更新maven1.2.2 在src/main/resources下创建mybatis-config.xml1.2.3 在src/main/resources下创建jdbc.properties1.2.4 在src/main/resources下创建log4j.xml文件1.2.5 在src/main/re…

搭建zabbix4.0监控服务实例

一.Zabbix服务介绍 1.1服务介绍 Zabbix是基于WEB界面的分布式系统监控的开源解决方案&#xff0c;Zabbix能够监控各种网络参数&#xff0c;保证服务器系统安全稳定的运行&#xff0c;并提供灵活的通知机制让SA快速定位并解决存在的各种问题。 1.2 Zabbix优点 Zabbix分布式监…

python用openpyxl包操作xlsx文件,统计表中合作电影数目最多的两个演员

题目&#x1f389;&#x1f389;&#x1f389;&#xff1a;编程完成下面任务&#xff1a;已知excel文件“电影导演演员信息表.xlsx”如下图所示&#xff1a;&#x1f373;&#x1f373;&#x1f373;要求&#xff1a;使用 openpyxl 包操作打开此文件&#xff0c;编写程序统计在…

sqlli-labs基本使用

1.安装hackbar插件 链接&#xff1a;https://pan.baidu.com/s/1-QIYmAU-BV_DEONfxovizQ 提取码&#xff1a;dc66 2.SQL注入表信息解析&#xff08;案例使用的sqlli-labs自带的数据库security&#xff09; 2.1 通过order by 判断表有多少列 分析表有多少列&#xff08;通过…

【Storm】【六】Storm 集成 Redis 详解

Storm 集成 Redis 详解 一、简介二、集成案例三、storm-redis 实现原理四、自定义RedisBolt实现词频统计一、简介 Storm-Redis 提供了 Storm 与 Redis 的集成支持&#xff0c;你只需要引入对应的依赖即可使用&#xff1a; <dependency><groupId>org.apache.storm…

红日(vulnstack)2 内网渗透ATTCK实战

环境配置 链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;wmsi 攻击机&#xff1a;kali2022.03 web 192.168.111.80 10.10.10.80 自定义网卡8&#xff0c;自定义网卡18 PC 192.168.111.201 10.10.10.201 自定义网卡8&#xff0c;自定义网卡18 DC 192.168.52.1…

【Word/word2007】将标题第1章改成第一章

问题&#xff1a;设置多级列表没有其他格式选的解决办法和带来的插入图注解的问题&#xff0c;将标题第1章改成第一章的问题其他方案。 按照百度搜索的方法设置第一章&#xff0c;可以是没有相应的样式可以选。 那就换到编号选项 设置新的编号值 先选是 然就是变得很丑 这时打开…

数据结构(一)(嵌入式学习)

数据结构干货总结&#xff08;一&#xff09;基础线性表的顺序表示线性表的链式表示单链表双链表循环链表循环单链表循环双链表栈顺序存储链式存储队列队列的定义队列的常见基本操作队列的顺序存储结构顺序队列循环队列队列的链式存储结构树概念二叉树二叉树的创建基础 数据&a…

项目实战典型案例14——代码结构混乱 逻辑边界不清晰 页面美观设计不足

代码结构混乱 逻辑边界不清晰 页面美观设计不足一&#xff1a;背景介绍问题1 代码可读性差&#xff0c;代码结构混乱问题2 逻辑边界不清晰&#xff0c;封装意识缺乏示例3.展示效果上的美观设计二&#xff1a;思路&方案问题一&#xff0c;代码可读性差&#xff0c;代码结构混…

tun驱动之ioctl

struct ifreq ifr; ifr.ifr_flags | IFF_TAP | IFF_NO_PI; ioctl(fd, TUNSETIFF, (void *)&ifr); 上面的代码的意思是设置网卡信息&#xff0c;并将tun驱动设置为TAP模式。在TAP模式下&#xff0c;在用户空间下调用open打开/dev/net/tun驱动文件&#xff0c;发送(调用send函…

C语言不踩坑: 自动类型转换规则

先看一个例程&#xff1a; # include <stdio.h> int main(void) {int a -10;unsigned b 5;if ((ab) > 0){printf("(ab) > 0\n");printf("(ab) %d\n",ab);}else{printf("(ab) < 0\n");}return 0; }运行的结果是&#xff1a; …

svn 分支(branch)和标签(tag)管理

版本控制的一大功能是可以隔离变化在某个开发线上&#xff0c;这个开发线就是分支&#xff08;branch&#xff09;。分支通常用于开发新功能&#xff0c;而不会影响主干的开发。也就是说分支上的代码的编译错误、bug不会对主干&#xff08;trunk&#xff09;产生影响。然后等分…