数据结构:跳表

news2024/12/24 2:48:41

文章目录

          • 跳表
          • 跳表的由来
            • 单链表的查找效率太低
            • 提高单链表的查找效率
          • 跳表的时间复杂度分析
          • 跳表的空间复杂度分析
          • 跳表的插入操作
          • 跳表的删除操作
          • 跳表索引动态更新

跳表

对链表进行改造,在链表上加多级索引的结构就是跳表,使其可以支持类似“二分”的查找算法

跳表是一种各方面性能都比较优秀的动态数据结构,可以支持快速地插入、删除、查找操作,时间复杂度都为O(logn),写起来也不复杂,甚至可以替代红黑树(Red-black tree)。

应用场景:Redis 中的**有序集合(Sorted Set)**就是用跳表来实现的。

  • Redis 中的有序集合支持的核心操作:

    • 插入一个数据;

    • 删除一个数据;

    • 查找一个数据;

    • 迭代输出有序序列;

    • 按照区间查找数据(比如查找值在[100, 356]之间的数据);

      对于按照区间查找数据这个操作,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。

"""
    An implementation of skip list.
    The list stores positive integers without duplicates.

    跳表的一种实现方法。
    跳表中储存的是正整数,并且储存的是不重复的。

    Author: Wenru
"""

from typing import Optional
import random


class ListNode:

    def __init__(self, data: Optional[int] = None):
        self._data = data
        self._forwards = []  # Forward pointers


class SkipList:
    _MAX_LEVEL = 16

    def __init__(self):
        self._level_count = 1
        self._head = ListNode()
        self._head._forwards = [None] * type(self)._MAX_LEVEL

    def find(self, value: int) -> Optional[ListNode]:
        p = self._head
        for i in range(self._level_count - 1, -1, -1):  # Move down a level
            while p._forwards[i] and p._forwards[i]._data < value:
                p = p._forwards[i]  # Move along level

        return p._forwards[0] if p._forwards[0] and p._forwards[0]._data == value else None

    def insert(self, value: int):
        level = self._random_level()
        if self._level_count < level: self._level_count = level
        new_node = ListNode(value)
        new_node._forwards = [None] * level
        update = [self._head] * level  # update is like a list of prevs

        p = self._head
        for i in range(level - 1, -1, -1):
            while p._forwards[i] and p._forwards[i]._data < value:
                p = p._forwards[i]

            update[i] = p  # Found a prev

        for i in range(level):
            new_node._forwards[i] = update[i]._forwards[i]  # new_node.next = prev.next
            update[i]._forwards[i] = new_node  # prev.next = new_node

    def delete(self, value):
        update = [None] * self._level_count
        p = self._head
        for i in range(self._level_count - 1, -1, -1):
            while p._forwards[i] and p._forwards[i]._data < value:
                p = p._forwards[i]
            update[i] = p

        if p._forwards[0] and p._forwards[0]._data == value:
            for i in range(self._level_count - 1, -1, -1):
                if update[i]._forwards[i] and update[i]._forwards[i]._data == value:
                    update[i]._forwards[i] = update[i]._forwards[i]._forwards[
                        i]  # Similar to prev.next = prev.next.next

    def _random_level(self, p: float = 0.5) -> int:
        level = 1
        while random.random() < p and level < type(self)._MAX_LEVEL:
            level += 1
        return level

    def __repr__(self) -> str:
        values = []
        p = self._head
        while p._forwards[0]:
            values.append(str(p._forwards[0]._data))
            p = p._forwards[0]
        return "->".join(values)


if __name__ == "__main__":
    l = SkipList()
    for i in range(10):
        l.insert(i)
    print(l)
    p = l.find(7)
    print(p._data)
    l.delete(3)
    print(l)
跳表的由来
单链表的查找效率太低

对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高,是 O(n)

提高单链表的查找效率

当链表的长度n比较大时,比如 1000、10000 的时候,在构建索引之后,查找效率的提升就会非常明显。

  • 建立一级索引

    每两个结点提取一个结点到上一级,我们把抽出来的那一级叫做索引或索引层

    如果我们现在要查找某个结点,比如 16。我们可以先在索引层遍历,当遍历到索引层中值为 13 的结点时,我们发现下一个结点是 17,那要查找的结点 16 肯定就在这两个结点之间。然后我们通过索引层结点的 down 指针,下降到原始链表这一层,继续遍历。这个时候,我们只需要再遍历 2 个结点,就可以找到值等于 16 的这个结点了。这样,原来如果要查找 16,需要遍历 10 个结点,现在只需要遍历 7 个结点。

    加来一层索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了。

请添加图片描述

  • 建立二级索引

    跟前面建立第一级索引的方式相似,我们在第一级索引的基础之上,每两个结点就抽出一个结点到第二级索引。

请添加图片描述

跳表的时间复杂度分析

跳表中查询任意数据的时间复杂度就是 O(logn)

这个查找的时间复杂度跟二分查找是一样的。换句话说,我们其实是基于单链表实现了二分查找

这种查询效率的提升,前提是建立了很多级索引,是空间换时间的设计思路。

跳表的空间复杂度分析

假设原始链表大小为 n,那第一级索引大约有 n/2 个结点,第二级索引大约有 n/4 个结点,以此类推,每上升一级就减少一半,直到剩下 2 个结点。

如果我们把每层索引的结点数写出来,就是一个等比数列。这几级索引的结点总和就是 n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空间复杂度是 O(n)

实际上,在软件开发中,我们不必太在意索引占用的额外空间。在讲数据结构和算法时,我们习惯性地把要处理的数据看成整数,但是在实际的软件开发中,原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,所以当对象比索引结点大很多时,那索引占用的额外空间就可以忽略了。

跳表的插入操作

对于纯粹的单链表,需要遍历每个结点,来找到插入的位置。但是,对于跳表来说,我们讲过查找某个结点的时间复杂度是 O(logn),所以这里查找某个数据应该插入的位置,方法也是类似的,时间复杂度也是 O(logn)

请添加图片描述

跳表的删除操作
跳表索引动态更新
  • 索引需要更新的原因 - 避免操作性能下降

    当我们不停地往跳表中插入数据时,如果我们不更新索引,就有可能出现某 2 个索引结点之间数据非常多的情况。极端情况下,跳表还会退化成单链表。

    作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。

  • 索引动态更新的方法

    跳表是通过随机函数来维护前面提到的“平衡性”。

    当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。通过一个随机函数,来决定将这个结点插入到哪几级索引中。

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

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

相关文章

Redis查询之RediSearch和RedisJSON讲解

文章目录1 Redis查询1.1 RedisMod介绍1.2 安装Redis1.3 RediSearchRedisJSON安装1.3.1 下载安装1.3.2 修改配置1.4 RedisJSON操作1.4.1 基本操作1.4.1.1 保存操作JSON.SET1.4.1.2 读取操作JSON.GET1.4.1.3 批量读取操作JSON.MGET1.4.1.4 删除操作JSON.DEL1.4.1.5 其他命令1.4.1…

鲲鹏Bigdata pro之Hive的基本操作(创建表、查询表)

1 介绍 本文主要依据《鲲鹏Bigdata pro之Hive集群部署》实验教程上的Hive操作例子讲解&#xff0c;方便大数据学员重用相应的操作语句。同时对实验过程中出现的问题给以解决方法&#xff0c;重现问题解决的过程。以让大家认识到&#xff0c;出现问题很正常&#xff1b;同时&am…

Java设计模式中接口隔离原则是什么?迪米特原则又是什么,啥又是合成复用原则,这些又怎么运用

继续整理记录这段时间来的收获&#xff0c;详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用&#xff01; 3.5 接口隔离原则 3.5.1 特点 使用的类不应该被迫依赖于不想使用的方法&#xff0c;应该依赖接口方法 3.5.2 案例(安全门) 防火功能代码 public interface Fi…

第一章:统计学习方法概论

大纲1.1统计学习的特点1.2统计学习方法步骤1.3 统计学习的分类基本分类&#xff1a;1.4 监督学习方法的三要素模型&#xff1a;条件概率分布P(Y∣X)P(Y|X)P(Y∣X)或决策分布Yf(X)Yf(X)Yf(X)策略&#xff1a;在所有假设空间中选择一个最优模型注意事项&#xff1a;算法&#xff…

Java设计模式中适配器模式是什么/适配器模式可以干什么/又如何实现

继续整理记录这段时间来的收获&#xff0c;详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用&#xff01; 5.3 适配器模式 5.3.1 概述 将一个类的接口转换为客户希望的另一种接口&#xff0c;使得原本由于接口不兼容而不能一起工作的那些类能一起工作分为类适配器模式和…

一套采用ASP.NET开发的工作通OA协同办公系统源码 流程审批 公文流转 文档管理

分享一套采用ASP.NET基于C#开发&#xff0c;使用桌面式的OA协同办公系统&#xff0c;超好用户体验效果的后台管理界面&#xff0c;集成 资讯、邮件、日程、文档&#xff08;在线文件档案管理&#xff09;、流程审批、公文流转、沟通与分享&#xff08;在线聊天和内部论坛&#…

基于LLVM的C编译器--lcc——以CLion用SSH连接WSL Ubuntu22.04为例

Windows 10 22H2CLion 2022.3.1Ubuntu 20.04 &#xff08;Microsoft Store内的WSL发行版&#xff09; 一、下载WSL&#xff0c;换源&#xff0c;切换到WSL2 1.1 保证windows版本 在设置->系统->关于中查看 必须是win10及以上对于x64系统&#xff1a;版本1903或更高版…

ArcGIS基础实验操作100例--实验63由图片创建点符号

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验63 由图片创建点符号 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&…

Java设计模式中代理模式是什么/JDK动态代理分为哪些,静态代理又怎么实现,又适合哪些场景

继续整理记录这段时间来的收获&#xff0c;详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用&#xff01; 5.结构型模式 5.1 概述 根据如何将类或对象按某种布局组成更大的结构&#xff0c;分为类结构模式和对象结构模式&#xff0c;前者采用继承机制来组织接口和类&am…

视频序列对比学习

前言 视频embedding化也即表征有很多实际的应用场景&#xff0c;比如文本-视频 pair的检索等等。由于视频一般来说较长&#xff0c;所以对于给定的一段话&#xff0c;其中的某些sentence句子一般对应着视频中某几个clip片段&#xff0c;之前常规的做法都是去匹配所有的sentence…

人工服务、人工智能和分析是联络中心的主要趋势

数字联络中心提供商 IPI 宣布了其对 2023 年的预测。IPI 非常重视提供卓越的客户联系&#xff0c;认为未来一年将由以下趋势定义&#xff1a;专注于人工服务&#xff1b;增加对人工智能和自动化的采用&#xff1b;以及更多地使用数据和分析。 关注人性化服务 据 IPI 称&#…

实现QTreeView、QTableView子项中的复选框勾选/取消勾选功能

1.前言本博文所说的技术点适用于同时满足下面条件的所有视图类&#xff1a;模型类从 QAbstractItemModel派生。代理类从QStyledItemDelegate派生。故本博文所说的技术点也适用于QTableView。2.需求提出基于Qt的model/view framework技术&#xff0c;利用QTreeView树视图实现业务…

【异常】SpringSecurity登录失败:Full authentication is required to access this resource

一、报错提示 SpringSecurity提示如下内容&#xff1a; 2023-01-07 06:08:51.843 [cdi-ids-commonprovider] [http-nio-9092-exec-14] WARN com.desaysv.tsp.logic.ids.config.MyAuthenticationEntryPoint - 登录失败&#xff1a;Full authentication is required to acces…

基于Java+Jsp+SpringMVC漫威手办商城系统设计和实现

基于JavaJspSpringMVC漫威手办商城系统设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联…

2023 年值得关注的 7 大人工智能 (AI) 技术趋势

&#x1f482; 个人网站:【海拥】【摸鱼游戏】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 想寻找共同学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 人工智能 (AI) 已经接…

图数据库Neo4j实战(全网最详细教程)

1.图数据库Neo4j介绍 1.1 什么是图数据库&#xff08;graph database&#xff09; 随着社交、电商、金融、零售、物联网等行业的快速发展&#xff0c;现实社会织起了了一张庞大而复杂的关系网&#xff0c;传统数据库很难处理关系运算。大数据行业需要处理的数据之间的关系随数…

《Go 并发数据结构和算法实践》学习笔记 Day 1

极客时间21天打卡活动&#xff1a;2023.1.16-2.5 链表的接口&#xff1a; 插入元素删除元素读取元素 并发化改造&#xff1a; 并发插入元素并发删除元素并发读取元素 锁&#xff0c;每个节点都定义一把锁。 并发插入 区域猜想&#xff1a;如果某个CPU 锁定了某个节点&…

U3D客户端框架(资源管理篇)之资源热更新管理器 ResourceManager

一、资源热更新管理器模块设计 1.热更新是什么&#xff1f; 游戏或者软件内的 美术/脚本代码等资源 发生变化时&#xff0c;无需下载客户端重新进行安装&#xff0c;而是在应用程序启动的情况下&#xff0c;通过比对本地资源与CDN资源的MD5码&#xff0c;如果本地资源与CDN中…

Visual Code 打开方式添加到右键菜单

一、配置右键打开 文件 注册表找到分支&#xff1a; 计算机\HKEY_CLASSES_ROOT\*\shell 在这个里面 shell 分支里右键添加项 VisualCode&#xff08;这个可以随便起&#xff0c;便于识别就行&#xff09; 在 VisualCode 分支里右键添加项 Command&#xff08;必须这个名&am…

【C++】双指针用法

快慢指针/同向指针 [0,i)的数据代表处理好的数据[i,j)的数据是那些处理过但不需要的数据[j,array.length)区间的数据为接下来待处理的数据。 以上三个区间的开和闭需要根据题目要求定义&#xff0c;但是要保持一致。 用此方法处理过的数组&#xff0c;处理好的数据相对位置会保…