有序表2:跳表

news2025/4/16 8:20:25

跳表是一个随机化的数据结构,可以被看做二叉树的一个变种,它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,目前在Redis和LeveIDB中都有用到。

它采用随机技术决定链表中哪些节点应增加向前指针以及在该节点中应增加多少个指针。跳表结构的头节点需有足够的指针域,以满足可能构造最大级数的需要,而尾节点不需要指针域。

采用这种随机技术,跳表中的搜索、插入、删除操作的时间均为O(logn),然而,最坏情况下时间复杂性却变成O(n)。相比之下,在一个有序数组或链表中进行插入/删除操作的时间为O(n),最坏情况下为O(n)。

跳表的原理非常简单,跳表其实就是一种可以进行二分查找的有序链表。跳表的数据结构模型如图

 

可以看到,跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了。

说了这么多,你可能觉得也不知道跳表到底有啥重要性,我这么说吧:如果面试考redis,问到底层数据结构,跳表是必须要问的,几乎100%的概率,想想你要是能当场给面试官写一个出来,是不是能给他干懵了

那我们就手写一个出来吧:

1.表头元素是一个key和value都为空的节点,不参与任何计算的过程,只用于从开始进行查找

2.让所有的节点的加入所拥有的层次都是随机的,我们这里使用类似于抛硬币的方法,如果Math.random()<0.5就+1,一直到不再小于0.5为止

3.不管什么时候查找某个key(不确定层数的情况下)都从最高层往0层进行查找,每次查当前层比它小的最右边的元素,如果这个元素在当前层的next是它则找到,不然继续查找下一层。

4.删除的时候如果当前的层删除之后只剩head了,那没必要留着了。

package dataStructure.TreeMap;

import java.util.ArrayList;

//跳表节点类
class SkipListNode<K extends Comparable<K>, V> {
    public K key;
    public V value;
    //当前节点的next节点组成的arrayList,有多少层就有多少个节点
    public ArrayList<SkipListNode<K, V>> nextNodes;

    public SkipListNode(K key, V value) {
        this.key = key;
        this.value = value;
        this.nextNodes = new ArrayList<>();
    }

    public boolean isKeyLessThan(K otherKey) {
        return key != null && otherKey != null && key.compareTo(otherKey) < 0;
    }

    public boolean isKeyEqualsTo(K otherKey) {
        return (key == null && otherKey == null) || (key != null && otherKey != null && key.compareTo(otherKey) == 0);
    }
}
public class SkipListMap<K extends Comparable<K>, V > {
    //调表的最大层数
    private int maxLevel;
    //跳表中节点的个数
    private int size;
    //跳表的头节点,key和value都是null,head的层数和最大层数相同
    private SkipListNode<K, V> head;
    //随机概率,类似抛硬币
    private static final double PROBABILITY = 0.5;

    public SkipListMap() {
        //初始化一个key和value都为空的节点作为头节点,个人觉得也可以直接放在head声明的地方
        head = new SkipListNode<>(null, null);
        //最大的层级,我们这里的层级从0开始
        maxLevel = 0;
        //头节点的nextNodes增加一个空的执行
        head.nextNodes.add(null);
        size = 0;
    }

    /**
     * 最最高层maxLevel开始一直找到最底层
     * 最后一定是在第0层的小于key的最右边的节点
     * @param key
     * @return
     */
    public SkipListNode mostRightLessNodeInTree(K key) {
        if(key == null) return null;
        int level = maxLevel;
        SkipListNode<K, V> node = head;
        while(level >= 0) {
            node = mostRightLessNodeInLevel(key, node, level --);
        }
        return node;
    }

    /**
     * 获得curLevel层中小于cur节点的最右边的节点
     * @param key 目标节点的key(我们要找比他小的最右边的节点)
     * @param cur 这一层中从cur开始找
     * @param curLevel 当前所在的层
     * @return
     */
    private SkipListNode<K ,V> mostRightLessNodeInLevel(K key, SkipListNode<K,V> cur, int curLevel) {
        //因为可能右很多层,nextNodes.get(i)代表第i层中next的后继节点
        SkipListNode<K, V> next = cur.nextNodes.get(curLevel);
        //如果next不为空并且next小于目标key的值就一直继续
        //退出条件1:next为空,代表本层最后一个节点,本层所有节点都小于目标节点的key
        //推出条件2:next不再小于目标节点的key
        while(next != null && next.isKeyLessThan(key)) {
            cur = next;
            next = cur.nextNodes.get(curLevel);
        }
        //根据退出条件我们知道cur是当前层中小于目标的key的最右边的节点
        //因为next为null的时候没有null已经不是节点了,而next不再小于目标节点的时候,cur是最后一个小于目标key的节点
        return cur;
    }

    /**
     * 是否包含某个key为key的节点
     * @param key
     * @return
     */
    public boolean containsKey(K key) {
        //要查找的是null,就别找了,没有
        if(key == null) return false;
        //从最高层依次查找找到第0层的小于key的最右边的节点
        SkipListNode<K,V> less = mostRightLessNodeInTree(key);
        //如果key是存在于这个跳表里的,那less在第0行的下一个节点肯定是以key为key的
        return less.nextNodes.get(0) == null? false : less.nextNodes.get(0).isKeyEqualsTo(key);
    }

    public K firstKey() {
        return head.nextNodes.get(0) == null? null : head.nextNodes.get(0).key;
    }

    /**
     * 获取整个跳表中的最后一个节点,这里一定要从最高层遍历
     * 这样的时间复杂度是logN,如果从第0层开始依次往后遍历着找最后一个的话就是O(N)的时间复杂度了
     * @return
     */
    public K lastKey() {
        int level = maxLevel;
        SkipListNode<K, V> cur = head;
        //从最高层开始每次找到最后的节点
        while(level >= 0) {

            SkipListNode<K, V> next = cur.nextNodes.get(level);
            //本层直到找到next为null为止,cur就是本层最后一个节点
            while(next != null) {
                cur = next;
                next = cur.nextNodes.get(level);
            }
            //找下一层
            level--;
        }
        //出这个循环的时候level是-1,cur是0层最后一个节点
        return cur.key;
    }

    /**
     * 根据key在跳表中查询值
     * @param key
     * @return
     */
    public V get(K key) {
        if(key == null) return null;
        //最高层maxLevel开始一直找到最底层找到0层小于key的最后一个节点
        SkipListNode<K, V> less = mostRightLessNodeInTree(key);
        return less.nextNodes.get(0) == null? null : less.nextNodes.get(0).isKeyEqualsTo(key)? less.nextNodes.get(0).value : null;
    }

   public void put(K key, V value) {
        //跳表不接受空值的key
        if(key == null) return;
        SkipListNode<K, V> pre = mostRightLessNodeInTree(key);
        SkipListNode<K, V> preNext = pre.nextNodes.get(0);
        //如果元素已经存在,更新元素的值
        if(preNext != null && preNext.isKeyEqualsTo(key)) {
            preNext.value = value;
        } else {
            //如果不存在增加这个元素
            //首先size++
            size ++;
            //新的节点的层数
            int newNodeLevel = 0;
            //相当于抛硬币,随机得到的值小于PROBABILITY就增加1继续抛,直到大于等于PROBABILITY
            while(Math.random() < PROBABILITY) {
                newNodeLevel ++;
            }
            SkipListNode<K, V> newNode = new SkipListNode<>(key, value);
            while(maxLevel < newNodeLevel) {
                maxLevel ++;
                head.nextNodes.add(null);
            }
            for(int i = 0; i <= newNodeLevel; i++) {
                newNode.nextNodes.add(null);
            }
            int level = maxLevel;
            SkipListNode<K, V> preNode = head;
            while(level >= 0) {
                preNode = mostRightLessNodeInLevel(key, preNode, level);
                if(level <= newNodeLevel) {
                    newNode.nextNodes.set(level, preNode.nextNodes.get(level));
                    preNode.nextNodes.set(level, newNode);
                }
                level --;
            }
        }
   }

    public void remove(K key) {
        if(containsKey(key)) {
            //删除的时候整个跳表的size需要-1
            size --;
            //从最高层的head节点开始找,找到每一个都删除
            SkipListNode<K, V> pre = head;
            int level = maxLevel;
            while(level >= 0) {
                //查找当前层的小于key的最右边的节点
                pre = mostRightLessNodeInLevel(key, pre, level);
                //next是pre在当前层的下一个节点,如果key在当前层存在,那next就应该是key所代表的元素
                SkipListNode<K, V> next = pre.nextNodes.get(level);
                //如果next就是key所代表的元素,那把pre的next指向next的下一个
                if(pre.nextNodes.get(level) != null && pre.nextNodes.get(level).isKeyEqualsTo(key)) {
                    pre.nextNodes.set(level, next.nextNodes.get(level));
                    //如果当前节点在当前层的前一个节点是head而且当前节点后面没有其他的了,那这一层删除
                    if(pre == head && next.nextNodes.get(level) == null) {
                        head.nextNodes.remove(level);
                        //删了一层,maxLevel也要减1
                        maxLevel --;
                    }
                }
                level --;
            }
        }
    }

    // for test
    public static void printAll(SkipListMap<String, String> obj) {
        for (int i = obj.maxLevel; i >= 0; i--) {
            System.out.print("Level " + i + " : ");
            SkipListNode<String, String> cur = obj.head;
            while (cur.nextNodes.get(i) != null) {
                SkipListNode<String, String> next = cur.nextNodes.get(i);
                System.out.print("(" + next.key + " , " + next.value + ") ");
                cur = next;
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        SkipListMap<String, String> test = new SkipListMap<>();
        printAll(test);
        System.out.println("======================");
        test.put("A", "10");
        /*printAll(test);
        System.out.println("======================");
        test.remove("A");
        printAll(test);
        System.out.println("======================");*/
        test.put("E", "E");
        test.put("B", "B");
        test.put("A", "A");
        test.put("F", "F");
        test.put("C", "C");
        test.put("D", "D");
        printAll(test);
        System.out.println("======================");
        System.out.println(test.containsKey("B"));
        System.out.println(test.containsKey("Z"));
        System.out.println(test.firstKey());
        System.out.println(test.lastKey());
        //System.out.println(test.floorKey("D"));
        //System.out.println(test.ceilingKey("D"));
        System.out.println("======================");
        test.remove("D");
        printAll(test);
        System.out.println("======================");
        //System.out.println(test.floorKey("D"));
        //System.out.println(test.ceilingKey("D"));
    }
}

整体来说,跳表不难,但是你需要注意边界的判断以及时时刻刻想着跳表是为了实现O(logN)的时间复杂度,而不是O(N),所以类似于从head开始沿着第0层来查找的方法肯定不是跳表的主流方法。

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

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

相关文章

找不到“SqlServer”模块-- 在此计算机上找不到任何 SQL Server cmdlet。

https://github.com/PowerShell/PowerShell/releases/tag/v7.2.2SQL Server Management Studio 18 启动触发器报错 标题: 找不到“SqlServer”模块 --------------- 在此计算机上找不到任何 SQL Server cmdlet。 在 https://powershellgallery.com/packages/SqlServer 上获取“…

PyTorch深度学习实战(1)——神经网络与模型训练过程详解

PyTorch深度学习实战&#xff08;1&#xff09;——神经网络与模型训练过程详解 0. 前言1. 传统机器学习与人工智能2. 人工神经网络基础2.1 人工神经网络组成2.2 神经网络的训练 3. 前向传播3.1 计算隐藏层值3.2 执行非线性激活3.3 计算输出层值3.4 计算损失值3.5 实现前向传播…

Linux——应用层之序列号与反序列化

TCP协议通讯流程 tcp是面向连接的通信协议,在通信之前,需要进行3次握手,来进行连接的建立。 当tcp在断开连接的时候,需要释放连接,4次挥手 服务器初始化: 调用socket, 创建文件描述符; 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了…

【机器学习】9种回归算法及实例总结,建议学习收藏

我相信很多人跟我一样&#xff0c;学习机器学习和数据科学的第一个算法是线性回归&#xff0c;它简单易懂。由于其功能有限&#xff0c;它不太可能成为工作中的最佳选择。大多数情况下&#xff0c;线性回归被用作基线模型来评估和比较研究中的新方法。 在处理实际问题时&#…

VirtualBox安装增强功能

在刚安装完的VisualBox中&#xff0c;默认屏幕是固定设置的&#xff0c;不会根据实际的窗口大小做自适应&#xff0c;这时候我们需要【安装增强功能】&#xff0c;然后打开【自动调整显示大小】&#xff0c;就可以实现虚拟机中屏幕自适应。 本教程的软件环境如下&#xff1a; 宿…

数据结构: 第四章 串

文章目录 一、串的定义和实现1.1串的定义和基本操作1.1.1串的定义1.1.2串的基本操作1.1.3小结 1.2串的存储结构1.2.1顺序存储1.2.2链式存储1.2.3基于顺序存储实现基本操作1.2.4小结 二、串的模式匹配2.1什么是字符串的模式匹配2.2朴素模式匹配算法2.3KMP算法2.4求next数组2.5KM…

python+django协同过滤算法的美食O2O外卖点餐系统vue

当然使用的数据库是mysql。尽管没有面向对象的数据库的作用强大&#xff0c;但是在Python开发上还是比较的灵活和方便的。系统功能主要介绍以下几点&#xff1a; 本外卖点餐系统主要包括二大功能模块&#xff0c;即用户功能模块和管理员功能模块。 &#xff08;1&#xff09;管…

Linux上安装jdk Tomcat mysql redis

1.安装JDk 1.1这里使用xshell中xfxp进行文件的上传&#xff0c;将jdk二进制包上传到Linux服务器上 下载地址&#xff1a;Java Downloads | Oracle 或者这里有下载好的安装包&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1ZSJxBDzDaTwCH2IG-d2Gig 提取码&#xff1a;…

dubbo 3.2.0 consumer bean初始化及服务发现简记

consumer bean初始化 以spring 如下配置<dubbo:reference id"versionConsumerBean" interface"org.apache.dubbo.samples.version.api.VersionService" version"*"/>为例&#xff0c;先使用spring 的初始化&#xff0c;核心代码 try {fin…

EDR(端点、端点检测与响应中心、可视化展现)

EDR基本原理与框架 EDR定义 端点检测和响应是一种主动式端点安全解决方案&#xff0c;通过记录终端与网络事件&#xff08;例如用户&#xff0c;文件&#xff0c;进程&#xff0c;注册表&#xff0c;内存和网络事件&#xff09;&#xff0c;并将这些信息本地存储在端点或集中数…

C#,码海拾贝(26)——求解“一般带状线性方程组banded linear equations”之C#源代码,《C#数值计算算法编程》源代码升级改进版

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 求解线性方程组的类 LEquations /// 原作 周长发 /// 改编 深度混淆 /// </summary> public static partial class LEquations { /// <summary> /…

Redis五大基本数据结构(原理)

一、 Redis数据结构-String String是Redis中最常见的数据存储类型&#xff1a; 其基本编码方式是RAW&#xff0c;基于简单动态字符串&#xff08;SDS&#xff09;实现&#xff0c;存储上限为512mb。 如果存储的SDS长度小于44字节&#xff0c;则会采用EMBSTR编码&#xff0c;…

c++ 11标准模板(STL) std::map(六)

定义于头文件<map> template< class Key, class T, class Compare std::less<Key>, class Allocator std::allocator<std::pair<const Key, T> > > class map;(1)namespace pmr { template <class Key, class T, clas…

优化器| SGD/SGD-m/SGD-NAG/Adagrad/Adadelta/RMSProp/Adam/Nadam/Adamax

前言&#xff1a;最近准备复习一下深度学习的基础知识&#xff0c;开个专栏记录自己的学习笔记 各种SGD和Adam优化器整理 基本概念 优化&#xff1a;最大化或最小化目标函数&#xff0c;具体指最小化代价函数或损失函数 损失函数 J(θ)f(hθ(x)&#xff0c;y)&#xff0c;h…

软考A计划-试题模拟含答案解析-卷五

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

Android 12.0仿ios的hotseat效果修改hotseat样式

1.概述 最近在12.0产品项目需求的需要,系统原生Launcher的布局样式很一般,所以需要重新设计ui对布局样式做调整,产品在看到 ios的hotseat效果觉得特别美观,所以要仿ios一样不需要横屏铺满的效果 居中显示就行了,所以就要看hotseat的具体布局显示了 效果图如下: 2.仿io…

《Spring Guides系列学习》guide51 - guide55

要想全面快速学习Spring的内容&#xff0c;最好的方法肯定是先去Spring官网去查阅文档&#xff0c;在Spring官网中找到了适合新手了解的官网Guides&#xff0c;一共68篇&#xff0c;打算全部过一遍&#xff0c;能尽量全面的了解Spring框架的每个特性和功能。 接着上篇看过的gui…

网络设备的部署(串行与并行)

串行设备 1.防火墙&#xff1a;能够实现区域隔离和访问控制 2.IPS(入侵防御系统)&#xff1a;能够检测入侵行为并阻断 3.WAF&#xff08;上网行为管理设备&#xff09;&#xff1a;保障web应用的安全 4.上网行为管理设备&#xff1a;对用户上网行为进行控制 5.FC交换机&am…

【源码解析】SpringBoot使用Nacos配置中心和使用 @NacosValue 进行热更新

SpringBoot使用Nacos 引入依赖 <dependency><groupId>com.alibaba.boot</groupId><artifactId>nacos-config-spring-boot-starter</artifactId><version>0.2.12</version> </dependency>增加本地配置 nacos:config:server-…

通过 ChatGPT 制作一个短视频

图文&#xff0c;生成视频 当通过 ChatGPT 生成连贯的 prompt 时&#xff0c;除了连环画&#xff0c;我们理所当然还可能畅想更激进的场景——生成动画视频。目前 AIGC 社区确实在生成视频方面有一定的尝试。比如 Deforum 可以通过多条 prompt&#xff0c;配合具体的切换时间点…