7.2 跳跃表(skiplist)

文章目录

  • 前言
  • 一、跳跃表——查找操作
  • 二、跳跃表——插入操作
  • 三、代码演示
    • 3.1 输出结果
    • 3.2 代码细节
  • 四、总结:
  • 参考文献:


前言

本章内容参考海贼宝藏胡船长的数据结构与算法中的第七章——查找算法,侵权删。

查找的时间复杂度能从原来链表的 O ( n ) O(n) O(n)降到 O ( l o g n ) O(logn) O(logn),典型的用空间复杂度换时间复杂度的例子。
在这里插入图片描述

直观上感受就是把原来的链表给拉升了,处于同一高度的节点被串联成了一个单独的链表,每个链表都有一个层高
第0层:1,6,15,30
第1层:1,6,15,30
第2层:1,15,30
第3层:1,15
第4层:15

跳跃表的特点:

  1. 有层高,每一层又是单独的链表
  2. 有两个特殊的节点,头节点代表极小值,尾节点代表极大值。也就是说不管插入什么样的值都是在头尾节点之中
  3. 跳跃表本身会维护这些元素的有序性(跳跃表中的元素是有大小顺序的)

跳跃表具体的操作:

  1. 插入
  2. 删除
  3. 查找

一、跳跃表——查找操作

  1. 首先从最左上方的节点开始找,以当前节点值下一个节点值作为基准值
    • 如果下一个当前节点值比待查找值要小,当前层高(本层)节点的下一个节点与待查找值作比较。
    • 如果下一个当前节点值比待查找值要大,该节点的位置(而不是下一层的第一个节点)向下走。
    • 如果下一个当前节点值等于待查找值,那就找到了!
  2. 重复步骤1。

如图展示:

在这里插入图片描述
举个例子:

在这里插入图片描述


二、跳跃表——插入操作

结合普通链表的插入操作:我们应该先查找待插入节点的前一个节点
如何找到呢?

  1. 首先插入节点要有一个高度(高度随机),找到初始节点中与待插入节点等高的节点作为头节点
    在这里插入图片描述

  2. 规则和查找操作类似:从上往下找,以当前节点值下一个节点值作为基准值

    • 如果下一个节点值比待查找值要小,前往本层中的下一个节点
    • 如果下一个节点值比待查找值要大,前往当前节点的下一层
  3. 重复操作2,到达0层。至此,找到了每一层需要连接插入节点的对应节点
    在这里插入图片描述


三、代码演示

  1. 初始化了一个具有n层结构的跳跃表中的节点
  2. 需要一个表示整个跳跃表的结构,因为在跳跃表中有几个比较特殊的信息,这几个特殊的信息,必须是由跳跃表去维护的:
    • 头尾指针
    • 当前跳跃表最高的层数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <inttypes.h>
#include<time.h>typedef struct Node
{int key, level;struct Node *next, *down, *up;
} Node;typedef struct Skiplist
{Node *head, *tail;int max_level;
} Skiplist;//初始化了一个具有n层结构的跳跃表中的节点
Node *getNewNode(int key, int n){Node *nodes = (Node*)malloc(sizeof(Node) * n);for (int i = 0; i < n; i++){nodes[i].key = key;nodes[i].level = i;nodes[i].next = NULL;nodes[i].down = (i ? nodes + (i - 1) : NULL);nodes[i].up = (i + 1 < n ? nodes + (i + 1) : NULL);}return nodes + n - 1;
}//初始化跳跃表
Skiplist *getNewSkiplist(int n){Skiplist *s = (Skiplist *)malloc(sizeof(Skiplist));s->head = getNewNode(INT32_MIN, n);s->tail = getNewNode(INT32_MAX, n);s->max_level = n;Node *p = s->head, *q = s->tail;while (p){p->next = q;p = p->down, q = q->down;}//刚开始创建跳表时,跳跃表是空,应该让跳表的头指针应该指向第0层的头节点,随着插入新节点,这个层高才逐渐增高while (s->head->level != 0) s->head = s->head->down;return s;
}//返回的是找到值所在节点的最上层的节点
Node *find(Skiplist *s, int x){Node *p = s->head;while (p && p->key !=x){if (p->next->key <= x) p = p->next;else p = p->down;}return p;
}double randDouble(){#define MAX_RAND_N 1000000return (rand() % MAX_RAND_N) * 1.0 / MAX_RAND_N;#undef MAX_RAND_N
}//生成层数
int randLevel(Skiplist *s){int level = 1;double p = 1.0 / 2.0;while (randDouble() < p) level += 1; //层数越高,生成该层数的概率越小,生成n层的概率是p^n
#define min(a, b) ((a) > (b) ? (b) : (a))return min(s->max_level, level);
#undef min
}void insert(Skiplist *s, int x){int level = randLevel(s);printf("rand level = %d\n", level);while (s->head->level + 1 < level) s->head = s->head->up; //这里保证头指针的层数一定不小于生成的node节点的层数Node *node = getNewNode(x, level);Node *p = s->head;printf("insert begin\n");fflush(stdout);while (p->level != node->level) p = p->down;// 头节点和node节点等高,二者对齐while (p) {while (p->next->key < node->key) p = p->next;node->next = p->next;p->next = node; //这里已经完成了本层的节点插入//接着向下继续插入p = p->down;node = node->down; //Node节点也得往下走}return;
}void clearNode(Node *p){if(p == NULL) return;free(p);return;
}void clearSkiplist(Skiplist *s){Node *p = s->head, *q;while(p->level != 0) p = p->down;  // 从最下层开始遍历清除while(p){q = p->next;clearNode(p);p = q;}free(s);return;
}void output(Skiplist *s){Node *p = s->head;int len = 0;for (int i = 0; i <= s->head->level; i++){len += printf("%4d", i);}printf("\n"); //输出层号for (int i = 0; i < len; i++) printf("-");printf("\n");while (p->level > 0) p = p->down; //先到第0层,也就是最底层while (p){bool flag = (p->key != INT32_MIN && p->key != INT32_MAX); //头和尾没有必要输出for (Node *q = p; flag && q; q = q->up){printf("%4d", q->key);}if (flag) printf("\n");p = p->next;}return;
}int main(){srand(time(0));int x;
#define MAX_LEVEL 32Skiplist *s = getNewSkiplist(MAX_LEVEL);
#undef MAX_LEVEL// insertwhile (~scanf("%d", &x)){if (x == -1) break;insert(s, x);output(s);}output(s);// findwhile (~scanf("%d", &x)){Node *p = find(s,x);printf("find result: ");if(p){printf("key = %d, level = %d\n", p->key, p->level);}else{printf("NULL\n");}}clearSkiplist(s);return 0;
}

3.1 输出结果

  • 插入
    在这里插入图片描述

  • 查找
    在这里插入图片描述

3.2 代码细节

  1. 释放内存的部分,其实clearNode()传入只能是多层节点的最底层(当然这里有风险,如果传入的不是最底层,那么free就会出错),看这个例子
#include<stdio.h>
#include<stdlib.h>int *getNewArray(int n){int *p = (int *)malloc(sizeof(int) * n);for(int i = 0; i < n; i++){p[i] = i;}return p + n - 1;
}void clearArray(int *p){if(!p) return;free(p);return;
}int main(){int num = 12;int *a1 = getNewArray(num);printf("%p\n", a1);printf("%d\n", *a1);  // 11printf("%d\n", *(a1 - num + 1)); // 0// free a1clearArray(a1);printf("%d\n", *a1);  // error// free(): invalid pointer//Aborted (core dumped)printf("%d\n", *(a1 - num + 1)); // 0// free a1 - num + 1clearArray(a1-num+1);printf("%d\n", *a1);  // ?printf("%d\n", *(a1 - num + 1)); // 0return 0;
}

原因是:由于 getNewArray 返回了指向数组最后一个元素的指针,当你尝试在 clearArray 中释放这个指针时,会遇到 free(): invalid pointer 错误。因为 free 函数需要传入的指针必须是由 malloc, calloc 或 realloc 分配的原始指针。这里传入的是数组末尾的指针,而不是原始指针。

  1. 初始化跳跃表的时候:创建一个空的跳跃表(此时只有头尾两个具有max_level层的节点),此时的s->head最好应该指向最底层,随着插入逐渐s->head升高。(不是必须,但这样方便维护)

四、总结:

  1. 有的版本跳跃表把Node中的*next, *up, *down换成*next[]

    /*柔性数组,根据该节点层数的不同指向大小不同的数组*next[0]表示该节点的第一层下一节点的索引地址*next[1]表示该节点的第二层下一节点的索引地址*next[n]表示该节点的第n层下一节点的索引地址
    */
    

    这样做就没有up,down指针,这样处理也是一种可以借鉴的思路

  2. 关注Node *getNewNode()函数,这个函数是申请(malloc)n层节点中底层的内存空间,而返回的则是顶层的内存地址,这个应当非常注意。

  3. 封装结构:一个Node是一个封装,getNewNode函数创建的是一个n层的具有高度的数组(其元素是Node类型),而跳表封装了这样的数组。如果把Node换成总结中的第一点*next[],这个其实就是这样的一个具有n层的数组,不再把它抽象成单独的一个Node。这两种不同的定义,就看你怎么抽象了,到底哪个是最小的封装单元。
    Node ——> 单个Node组成的高度(长度)为n的数组 ——> 跳表

    struct skip_list_node
    {/*key是唯一的*/int key;       /*存储的内容*/int value;     /*当前节点最大层数*/int max_level; /*柔性数组,根据该节点层数的不同指向大小不同的数组*next[0]表示该节点的第一层下一节点的索引地址*next[1]表示该节点的第二层下一节点的索引地址*next[n]表示该节点的第n层下一节点的索引地址*/struct skip_list_node *next[];
    };                 
    代码原文链接:https://blog.csdn.net/m0_37845735/article/details/103691814
    
  4. 插入和查找的过程图像要非常清晰,边写代码脑海里要有一个动态的图像。多练,无他唯手熟尔!

参考文献:

  1. 跳表C语言实现详解
  2. 海贼宝藏——数据结构与算法

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

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

相关文章

线上真实案例之执行一段逻辑后报错Communications link failure

1.问题发现 在开发某个项目的一个定时任务计算经销商返利的功能时&#xff0c;有一个返利监测的调度&#xff0c;如果某一天返利计算调度失败了&#xff0c;需要重新计算&#xff0c;这个监测的调度就会重新计算某天的数据。 在UAT测试通过&#xff0c;发布生产后&#xff0c…

CSS动画(css、js动画库:各种动画效果)

第一种方法&#xff1a;文字从上到下显示动画&#xff1b; <div class"text-container"><div class"text">文字从上到下显示</div></div><style scoped> /*确保 keyframes 规则在引用它的任何选择器之前定义&#xff0c;以避…

Android开发:应用百度智能云中的身份证识别OCR实现获取个人信息的功能

百度智能云&#xff1a; 百度智能云是百度提供的公有云平台&#xff0c;于2015年正式开放运营。该平台秉承“用科技力量推动社会创新”的愿景&#xff0c;致力于将百度在云计算、大数据、人工智能的技术能力向社会输出。 百度智能云为金融、城市、医疗、客服与营销、能源、制造…

C语言数据结构之顺序表

目录 1.线性表2.顺序表2.1顺序表相关概念及结构2.2增删查改等接口的实现 3.数组相关例题 1.线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性&#xff08;数据类型相同&#xff09;的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构&#xff…

2024年阿里云服务器明细报价整理总结

2024年阿里云服务器租用费用&#xff0c;云服务器ECS经济型e实例2核2G、3M固定带宽99元一年&#xff0c;轻量应用服务器2核2G3M带宽轻量服务器一年61元&#xff0c;ECS u1服务器2核4G5M固定带宽199元一年&#xff0c;2核4G4M带宽轻量服务器一年165元12个月&#xff0c;2核4G服务…

Zynq 7000 SoC器件的复位系统

Zynq7000 SoC器件中的复位系统包括由硬件、看门狗定时器、JTAG控制器和软件生成的复位。每个模块和系统都包括一个由复位系统驱动的复位。硬件复位由上电复位信号&#xff08;PS_POR_B&#xff09;和系统复位信号&#xff08;PS_SRST_B&#xff09;驱动。 在PS中&#xff0c;有…

JAVA基础面试题(第九篇)中! 集合与数据结构

JAVA集合和数据结构也是面试常考的点&#xff0c;内容也是比较多。 在看之前希望各位如果方便可以点赞收藏&#xff0c;给我点个关注&#xff0c;创作不易&#xff01; JAVA集合 11. HashMap 中 key 的存储索引是怎么计算的&#xff1f; 首先根据key的值计算出hashcode的值…

隧道代理的优势与劣势分析

“随着互联网的快速发展&#xff0c;网络安全已经成为一个重要的议题。为了保护个人和组织的数据&#xff0c;隧道代理技术逐渐成为网络安全的重要工具。隧道代理通过在客户端和服务器之间建立安全通道&#xff0c;加密和保护数据的传输&#xff0c;有效地防止黑客入侵和信息泄…

15-partition table (分区表)

ESP32-S3的分区表 什么是分区表&#xff1f;&#x1f914; ESP32-S3的分区表是用来确定在ESP32-S3的闪存中数据和应用程序的布局。每个ESP32-S3的闪存可以包含多个应用程序&#xff0c;以及多种不同类型的数据&#xff08;例如校准数据、文件系统数据、参数存储数据等&#x…

Scala 第一篇 基础篇

Scala 第一篇 基础篇 一、变量与常量 1、变量2、常量 二、数据类型 1、数据基本类型概览2、元组的声明与使用3、Range介绍和使用4、Option 类型的使用和设计5、类型别名 三、运算符四、程序逻辑 1、一切都是表达式2、分支语句3、循环语句 五、集合 1、List2、Set3、Map4、Arra…

MySQL高级(索引-性能分析-explain执行计划)

explain 或者 desc 命令获取 MySQL 如何执行 select 语句的信息&#xff0c;包括在 select 语句执行过程中表如何连接和连接的顺序。 -- 直接在 select 语句之前加上关键字 explain / desc explain select 字段列表 from 表名 where 条件 &#xff1b; explain select * …

电机控制专题(一)——最大转矩电流比MTPA控制

文章目录 电机控制专题(一)——最大转矩电流比MTPA控制前言理论推导仿真验证轻载1Nm重载30Nm 总结 电机控制专题(一)——最大转矩电流比MTPA控制 前言 MTPA全称为Max Torque Per Ampere&#xff0c;从字面意思就可以知道MTPA算法的目的是一个寻优最值问题&#xff0c;可以从以…

SQL Server 2022 安装及使用

SQL Server 2022 前言一、安装SQL Server 2022下载SQL Server 2022安装SQL Server 2022配置SQL Server 2022 二、安装SQL Server Management Studio下载SQL Server Management Studio安装SSMS-Setup-CHS 三、使用SQL Server 2022四、解决连接到服务器报错问题 前言 SQL Serve…

git 快问快答

我在实习的时候&#xff0c;是用本地开发&#xff0c;然后 push 到 GitHub 上&#xff0c;之后再从 Linux 服务器上拉 GitHub 代码&#xff0c;然后就可以了。一般程序是在 Linux 服务器上执行的&#xff0c;我当时使用过用 Linux 提供的命令来进行简单的性能排查。 在面试的时…

应用编程之进程(三-通信篇)

所谓进程间通信指的是系统中两个进程之间的通信&#xff0c;不同的进程都在各自的地址空间中、相互独立、隔离&#xff0c;所以它们是处在于不同的地址空间中&#xff0c;因此相互通信比较难&#xff0c;Linux 内核提供了多种进程间通信的机制。 大部分的程序是不要考虑进程间…

Microchip逆市扩张,接连收购2家公司

尽管年初传来降薪停工的消息&#xff0c;全球领先的半导体解决方案供应商Microchip并未因此停下扩张的脚步。相反&#xff0c;该公司在短短的一个月内&#xff0c;接连宣布收购两家公司&#xff0c;展现了其坚定的市场布局和前瞻的战略眼光。 4月11日&#xff0c;Microchip成功…

二进制OpenStack

二进制搭建OpenStack 1.环境准备 1.1机器的准备 主机名服务器配置操作系统IP地址controller-node4C8Gcentos7.9172.17.1.117computer-node4C8Gcentos7.9172.17.1.118 1.2网络架构 [rootcotroller-node ~]# ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noque…

Java JNI调用本地方法1(调用C++方法)

一、基础概念 1、JNI&#xff08;Java Native interface&#xff09;:sun公司提供的JNI是Java平台的一个功能强大的接口&#xff0c;实现java和操作系统本地代码的相互调用功能&#xff0c;系统本地代码通常是由其他语言编写的&#xff0c;如C。 二、JNI使用步骤 1、定义一个J…

选定进行压缩的卷可能已损坏。请使用chkdsk来修复损坏问题,然后尝试再次压缩该卷

Windows Server 2008R2环境下&#xff0c;进行磁盘重新分区时&#xff0c;想要对系统盘进行“压缩卷”&#xff0c;结果报错提示“选定进行压缩的卷可能已损坏。请使用Chkdsk来修复损坏问题&#xff0c;然后尝试再次压缩该卷。”这是硬盘出现了坏道导致的&#xff0c;硬盘出错无…

中仕公考:教师编制和事业单位d类一样吗?

教师编制和事业单位D类在考试内容、专业要求、晋升途径等方面有很大的不同中仕为大家介绍一下&#xff1a; 考试内容&#xff1a;教师编的考试包括教育综合知识和学科专业知识&#xff0c;有的地区会额外考公共基础知识。事业单位D类的考试更侧重于职业能力倾向测验和综合应用…