跳表的理解以及使用

news2024/11/17 10:42:20

文章目录

  • 背景
    • 数组-链表
    • 优化链表随机访问的方法
  • 介绍
    • 跳表的理解
    • 层数随机
      • 为什么随机可以保证效率
      • 实现细节
    • 跳表与二分查找
    • 跳表与红黑数
    • 跳表与HASH
  • 使用
  • 实现
    • 随机层数的实现
    • 跳表实现以及测试

背景

数组-链表

  • 数组优点

随机访问速度较快(基于下标访问)。
实现简单,使用简单。
内存地址连续,对cpu缓存很友好

  • 数组缺点

内存连续可以是优点,也可以是缺点,如果在内存紧张的情况下,数组将会被大大限制。 插入和删除的时候会导致元素的移动(数据拷贝),较慢。
数组大小固定,大大的限制了元素的个数,对于很多动态的数据不友好。

  • 链表有点

链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。
删除插入不用移动其他元素。
不受元素大小限制,可以随意扩展。
有序链表插入、删除值需要调整指针

  • 链表缺点

不支持直接通过索引进行快速访问
失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
随机访问效率相对数组来说较低。

优化链表随机访问的方法

与数组不同,链表中的节点是通过指针(或引用)连接起来的,每个节点只包含到下一个节点(以及在双向链表中到前一个节点)的链接。因此,要访问链表中的特定元素,通常需要从头节点开始逐个遍历,直到到达目标位置。这种访问方式的时间复杂度为O(n),其中n是要访问的节点的位置。

过以下方法来加速对特定位置的访问
链表加哈希表
以在链表之外维护一个数组或哈希表,用来存储指向链表中重要节点的指针。这样可以在一定程度上实现近似的随机访问,但会增加内存消耗和更新数据结构的开销
分块技术
将链表分成多个小块,并为每个块创建一个索引。这种方式类似于数据库中的索引,可以在一定程度上提高访问速度,但仍然不如数组那样高效
跳表
跳表是一种特殊的链表,它通过多级索引来加快查找速度。虽然不是真正的随机访问,但它提供了比普通链表更快的查找性能

介绍

跳表(SkipList),是一种随机化的数据结构,可以看作是一种可以进行二分查找的有序链表。跳表通过增加多级索引(即“跳跃”的层)来优化链表的查找、插入和删除操作,使得这些操作的时间复杂度降低到O(logn)。跳表在Redis和LevelDB等系统中都有应用,其实现和维护相对简单,但性能上却与红黑树、AVL树等平衡树不相上下。

跳表的理解

数据结构

  • 跳表由多层链表组成,每一层都是一个有序链表。
  • 底层链表包含所有元素,保持有序。
  • 上层链表的元素是底层链表元素的子集,层数越高,元素越少。
  • 每一层链表中的元素都通过指针(或称为“向前指针”)连接,同时,上层链表的节点还通过指针指向下层链表中的对应节点(或称为“向下指针”)。
    在这里插入图片描述

索引机制

  • 跳表通过随机技术决定哪些节点应增加向前指针以及在该节点中应增加多少个指针。
  • 索引层级的分配是随机的,但通常遵循一定的概率分布,以保证跳表的平衡性。
    操作原理
  • 查找:从顶层链表开始,逐层向下查找,直到找到目标元素或确定目标元素不存在。
  • 插入:首先通过查找确定插入位置,然后在原始链表中插入新节点,并根据随机概率决定是否将该节点添加到上层索引链表中。
  • 删除:通过查找找到目标节点,然后依次从各层索引链表中删除该节点,最后从原始链表中删除该节点。

层数随机

跳表(Skip List)中的层之所以是随机的,主要是为了保持数据结构的平衡性,并且保证平均情况下能够达到O(log n)的时间复杂度进行查找、插入和删除操作。通过引入随机性,可以确保每一层的节点数量减少大约一半,从而形成类似于二分查找的效果。

为什么随机可以保证效率

通过概率分布来决定每个节点的高度(即它出现在哪些层级),可以使得跳表的层级结构趋于一种自然平衡状态,从而保证了平均情况下的高效查找、插入和删除操作
均匀分布
当我们以一定的概率p(通常为0.5)来决定是否增加一层时,实际上是在模拟一个几何分布。这种分布的特点是随着层数的增加,出现的概率呈指数级下降。
这意味着大多数节点只会出现在较低的层级,而只有少数节点会出现在较高的层级。这样的分布有助于保持整个结构的扁平化。
对数时间复杂度:
由于每一层上的节点数量大约是下一层的一半,因此从顶层到底层的查找过程类似于二分查找。这样就能够在O(log n)的时间内完成查找。
例如,如果最底层有n个节点,那么上一层大约有n/2个节点,再上一层大约有n/4个节点,以此类推。这与完全二叉树的结构相似,但跳表是一个动态的数据结构,允许高效的更新操作。
避免最坏情况:
如果不是随机分配高度,而是使用固定的模式或规则,可能会导致某些特定输入序列导致非常不平衡的结构,进而影响性能。例如,如果总是将新节点添加到最高层,那么跳表就会变得非常高且不均衡。
随机性帮助跳表避免了这种最坏情况的发生,即使在连续插入具有相同值的节点时,也能保持相对良好的平衡性。

实现细节

  • 每层都是有序列表
  • 生成随机层数:当插入一个新节点时,首先确定该节点的层数。这个层数是通过一个随机过程生成的,比如以0.5的概率决定是否增加一层。
  • 维护多层索引:每个节点都可能出现在多个层级中,从最底层到它被随机选择的最高层。每层都是一个链表,高层的链表包含较少的节点,这些节点是从低层链表中选取出来的。
  • 查找操作:从最高层开始向下搜索,直到找到目标值或确定不存在为止。每次比较后,如果当前节点大于目标值,则向右移动;如果小于目标值,则向下移动到下一层继续搜索。

通过这种方式,跳表利用了随机性的力量,创建了一个自适应且平衡的数据结构,能够在动态环境中提供高效的查找、插入和删除操作。随机性在这里起到了关键作用,因为它使得跳表能够自我调整,以适应不断变化的数据集,并保持其性能特性。

跳表与二分查找

跳表(Skip List)和二分查找(Binary Search)都是用于在有序数据集中进行高效查找的数据结构或算法,但它们的工作原理、适用场景以及实现方式都有所不同。

二分查找

定义:二分查找是一种在有序数组中查找特定元素的搜索算法。
工作原理:通过每次比较中间元素与目标值来缩小查找范围。如果中间元素正好是目标值,则查找过程结束;如果目标值大于或小于中间元素,则在数组的一半中继续进行相同的操作。
时间复杂度:O(log n),其中n是数组长度。 空间复杂度:O(1),因为它不需要额外的空间。
适用性:适用于静态且已排序的数组。如果数组经常变动,则需要频繁地重新排序,这会使得效率降低。

跳表与红黑数

尽管红黑树在许多情况下都能提供高效的查找、插入和删除操作,但跳表的存在仍然有其必要性和优势。以下是跳表相对于红黑树的一些优势

  • 简单性:跳表的算法和数据结构相对简单,容易理解和实现。与红黑树相比,跳表不需要复杂的旋转和颜色调整等操作来维持树的平衡,这降低了实现的复杂性和出错的可能性。
  • 灵活性:跳表在插入和删除操作时,只需要调整相邻的指针,而不需要像红黑树那样进行复杂的重新平衡操作。这使得跳表在处理动态数据集时更加灵活和高效。
  • 有序性:跳表维护了元素的有序性,这使得范围查询等操作更加高效。虽然红黑树也可以通过中序遍历实现范围查询,但跳表通过其多层索引结构可以更快地定位到查询范围的起始和结束点。
  • 空间效率:虽然跳表需要存储多级索引,从而消耗比红黑树更多的内存空间,但在某些应用场景下,这种空间消耗是可以接受的。特别是在内存数据库如Redis中,由于数据完全存储在内存中,因此空间效率并不是首要考虑的因素。
  • 性能稳定性:跳表在插入和删除操作中,性能相对稳定,因为只会影响链表中的相邻节点。而红黑树虽然操作也是对数时间复杂度,但在最坏情况下(如连续插入有序元素)可能需要多次旋转和颜色变换来重新平衡树,从而影响性能。
  • 应用场景:跳表特别适用于那些需要频繁进行范围查询、插入和删除操作的有序数据集。例如,Redis的有序集合就使用了跳表来存储数据,以支持高效的查找、插入和删除操作。

跳表与HASH

  • 数据结构特点:跳表通过多级索引结构实现快速查询,适用于有序数据集;哈希表通过哈希函数将键映射到存储位置的索引上,实现快速的键值查找。
  • 查询效率:跳表和哈希表都具有较高的查询效率,但哈希表在键值查找方面通常更快;跳表在有序集合操作和范围查询方面更具优势。
  • 空间复杂度:哈希表通常只占用较少的额外空间(主要用于哈希表的扩容和哈希冲突的处理);跳表需要存储多级索引,因此会占用更多的空间。
  • 应用场景:跳表更适合于需要保持元素有序的场景,如排行榜、有序集合、范围查询等;哈希表则更适用于键值存储、缓存系统、实时数据分析等场景

使用

跳表适用于需要频繁进行插入、删除和查找操作的数据集,尤其是当数据集较大时,跳表能够显著提高这些操作的效率。以下是一些跳表使用的具体场景:

  • 数据库索引:在数据库系统中,跳表可以用作索引结构,以加速对大量数据的查询操作。
  • 缓存系统:在缓存系统中,跳表可以用于管理缓存项的插入、删除和查找,以提高缓存的命中率和访问速度。
  • 分布式系统:在分布式系统中,跳表可以用于实现分布式哈希表(DHT)等数据结构,以支持高效的键值对存储和检索。
  • 有序集合的操作:跳表特别适用于实现有序集合(Sorted Set)的各种操作,如添加元素、删除元素、查找元素、获取排名、获取分数范围内的元素等。由于跳表通过多级索引结构实现快速查询,因此其时间复杂度为O(log n),能够高效地处理有序集合的相关操作。Redis中的有序集合(Zset)就采用了跳表作为底层数据结构之一,以实现快速的查找、插入和删除操作。
  • 排行榜:由于跳表能够快速地定位到数据的位置,并获取其排名,因此非常适合用于实现排行榜功能。无论是网站的用户积分排行榜、游戏的玩家排名榜,还是电商平台的商品-
    销量排行榜等,都可以通过跳表来实现高效的数据处理和展示。
  • 范围查询:跳表的多级索引结构使得范围查询变得非常高效。当需要查询某个分数范围内的所有元素时,跳表可以快速地定位到起始和结束位置,并遍历该范围内的所有元素。

实现

随机层数的实现

在跳表中,每个新插入的节点都会被赋予一个随机的高度(层数)。
这个高度决定了该节点会在哪些层级出现。通常,高度的选择基于一定的概率分布,最常见的做法是使用一个概率p(如0.5),来决定是否增加一层。具体来说:

为新节点分配第一层。
对于每一层之上的一层,以概率p决定是否继续添加。如果决定添加,则该节点也在这一层上出现;否则,停止。

例如,假设p=0.5:

1 节点总是出现在最底层(第0层)。
250%的概率出现在第1层。
3 如果已经出现在第1层,那么再有50%的概率出现在第2层。
4 这个过程一直持续,直到某个层没有被选择为止。

这种随机性的引入使得跳表的结构变得不规则,但总体上仍然保持了良好的平衡性。随着节点数量的增加,跳表的高度也会增长,但是由于每增加一层的概率都是递减的,所以整个结构不会变得太高。

跳表实现以及测试

#include <stdio.h>  
#include <stdlib.h>  
#include <pthread.h>  
#include <time.h>  
  
#define MAX_LEVEL 16  
#define RAND_MAX_FOR_LEVEL (RAND_MAX / MAX_LEVEL)  
  
typedef struct Node {  
    int key;  
    struct Node **forward;  
    struct Node *left, *right; 
} Node;  
  
typedef struct SkipList {  
    pthread_mutex_t lock;  
    int level;  
    Node *header;  
} SkipList;  
  
void skiplist_init(SkipList *list) {
	int i=0;
    pthread_mutex_init(&list->lock, NULL);  
    list->level = 1;  
    list->header = malloc(sizeof(Node));
	if(!list->header) return;
	list->header->forward = malloc(sizeof(Node*) * MAX_LEVEL);
	if(!list->header->forward) return;
    list->header->left = list->header->right = list->header;  
    for (i = 0; i < MAX_LEVEL; i++) {  
        list->header->forward[i] = NULL;  
    }  
}  
  
int randomLevel() {  
    int lvl = 1;  
    while (rand() / RAND_MAX_FOR_LEVEL && lvl < MAX_LEVEL) lvl++;  
    return lvl;  
}  
  
Node* createNode(int level, int key) {
	int i = 0;
    Node *node = malloc(sizeof(Node));
	node->forward = malloc(sizeof(Node*) * level);
    node->key = key;	
    for ( i = 0; i < level; i++) {  
        node->forward[i] = NULL;  
    }  
    node->left = node->right = node;  // 这些字段在此示例中未使用  
    return node;  
}  
  
void skiplist_insert(SkipList *list, int key) {  
    pthread_mutex_lock(&list->lock);  
    int lvl = randomLevel();
	int i = 0;
    if (lvl > list->level) {  
        for ( i = list->level; i < lvl; i++) {  
            list->header->forward[i] = NULL;  
        }  
        list->level = lvl;  
    }  
  
    Node *update[MAX_LEVEL];  
    Node *curr = list->header;
	
    for ( i = lvl - 1; i >= 0; i--) {  
        while (curr->forward[i] && curr->forward[i]->key < key) {  
            curr = curr->forward[i];  
        }  
        update[i] = curr;  
    }  
  
    curr = curr->forward[0];  
    if (curr == NULL || curr->key != key) {  
        Node *newNode = createNode(lvl, key);  
        for ( i = 0; i < lvl; i++) {  
            newNode->forward[i] = update[i]->forward[i];  
            update[i]->forward[i] = newNode;  
        }  
    }  
  
    pthread_mutex_unlock(&list->lock);  
}  
  
Node* skiplist_search(SkipList *list, int key) {  
    pthread_mutex_lock(&list->lock);  
    Node *curr = list->header;  
	int i = 0;
    for ( i = list->level - 1; i >= 0; i--) {  
        while (curr->forward[i] && curr->forward[i]->key < key) {  
            curr = curr->forward[i];  
        }  
    }  
    curr = curr->forward[0];  
    pthread_mutex_unlock(&list->lock);  
    return curr && curr->key == key ? curr : NULL;  
}  
  
void skiplist_delete(SkipList *list, int key) {  
    pthread_mutex_lock(&list->lock);  
    Node *update[MAX_LEVEL];  
    Node *curr = list->header;
	int i = 0;
    for ( i = list->level - 1; i >= 0; i--) {  
        while (curr->forward[i] && curr->forward[i]->key < key) {  
            curr = curr->forward[i];  
        }  
        update[i] = curr;  
    }  
    curr = curr->forward[0];  
  
    if (curr && curr->key == key) {  
        for (int i = 0; i < list->level; i++) {  
            if (update[i]->forward[i] != curr) break;  
            update[i]->forward[i] = curr->forward[i];  
        }  
  
        // 尝试减少跳表的层级(可选优化)  
        while (list->level > 1 && list->header->forward[list->level - 1] == NULL) {  
            list->level--;  
        }  
  
        free(curr);  
    }  
  
    pthread_mutex_unlock(&list->lock);  
}  
  
void skiplist_destroy(SkipList *list) {  
    // 遍历并释放所有节点(略,这里只释放了header)  
    Node *curr = list->header->forward[0];  
    while (curr) {  
        Node *next = curr->forward[0];  
        free(curr);  
        curr = next;  
    }  
    pthread_mutex_destroy(&list->lock);  
    free(list->header);  
}  
  
int main() {  
    srand(time(NULL));  
    SkipList list;  
    skiplist_init(&list);  
    int i = 0;
    // 插入10个随机数据  
    for ( i = 0; i < 10; i++) {  
        int key = rand() % 100;  // 生成0到99之间的随机数  
        printf("Inserting %d\n", key);  
        skiplist_insert(&list, key);  
    }  
  
    // 查找数据  
    int search_key = 50;  
    Node *found = skiplist_search(&list, search_key);  
    if (found) {  
        printf("Found %d\n", found->key);  
    } else {  
        printf("%d not found\n", search_key);  
    }  
  
    // 删除数据  
    search_key = 25;  
    printf("Deleting %d\n", search_key);  
    skiplist_delete(&list, search_key);  
  
    // 清理资源  
    skiplist_destroy(&list);  
  
    return 0;  
}

编译

gcc -g sklist.c -o sklist -lpthread

测试结果
在这里插入图片描述

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

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

相关文章

OpenCV视频I/O(5)视频采集类VideoCapture之从视频流中获取下一帧的函数grab()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 从视频文件或捕获设备中抓取下一帧。 grab() 函数是 OpenCV 中 VideoCapture 类的一个成员函数&#xff0c;用于从视频流中获取下一帧而不立即检…

基于SpringBoot的学生宿舍管理系统【附源码】

基于SpringBoot的高校社团管理系统&#xff08;源码L文说明文档&#xff09; 4 系统设计 一个成功设计的系统在内容上必定是丰富的&#xff0c;在系统外观或系统功能上必定是对用户友好的。所以为了提升系统的价值&#xff0c;吸引更多的访问者访问系统&#xf…

相关数据库类型介绍

数据库类型可以根据不同的维度进行分类&#xff0c;但最常见的分类方式是将其分为关系型数据库&#xff08;Relational Databases&#xff09;和非关系型数据库&#xff08;Non-Relational Databases&#xff09;&#xff0c;也称为NoSQL数据库。下面我将详细介绍这两种类型的数…

[Linux] Linux操作系统 进程的优先级 环境变量

标题&#xff1a;[Linux] Linux操作系统 进程的优先级 个人主页水墨不写bug &#xff08;图片来源于网络&#xff09; 目录 一、进程优先级 1.PRI and NI 2.PRI vs NI 的补充理解 二、命令行参数和环境变量 1. 命令行参数 2.环境变量 I&#xff0c;环境变量是内…

AI大模型算法工程师就业宝典—— 高薪入职攻略与转行秘籍!

从ChatGPT到新近的GPT-4&#xff0c;GPT模型的发展表明&#xff0c;AI正在向着“类⼈化”⽅向迅速发展。 GPT-4具备深度阅读和识图能⼒&#xff0c;能够出⾊地通过专业考试并完成复杂指令&#xff0c;向⼈类引以为傲的“创造⼒”发起挑战。 现有的就业结构即将发⽣重⼤变化&a…

【CSS Tricks】深入聊聊前端编写css的方法论

目录 引言BEM 规范OOCSS 规范结构与样式分离容器与内容分离 SMACSS 规范ITCSS 规范设置层工具层通用层元素层对象层组件层微调层由此分层后的项目代码结构也会相应做修改&#xff0c;主要有两种形式&#xff1a;文件夹形式文件名形式引用方式按照层级顺序引用 ACSS 规范总结 引…

U盘打开提示要格式化:深度剖析、恢复策略与预防指南

U盘打开提示要格式化现象阐述 在日常的数字生活中&#xff0c;U盘作为便携式存储设备的代表&#xff0c;扮演着不可或缺的角色。然而&#xff0c;不少用户都曾遭遇过这样一个令人头疼的问题&#xff1a;当满怀期待地插入U盘&#xff0c;准备访问其中存储的数据时&#xff0c;系…

21天全面掌握:小白如何高效学习AI绘画SD和MJ,StableDiffusion零基础入门到精通教程!快速学习AI绘画指南!

‍‍大家好&#xff0c;我是画画的小强。 今天给大家分享一些我长期以来总结的AI绘画教程和各种AI绘画工具、模型插件&#xff0c;还包含有视频教程 AI工具&#xff0c;免费送&#x1f447;&#x1f447;‍‍ 这份完整版的AI绘画全套学习资料已经上传CSDN&#xff0c;朋友们如…

怎么通过AI大模型开发一个网站?

目录 一、提示词与AI输出 二、网站效果 以前不会代码开发&#xff0c;写网站是不可能的事情&#xff0c;现在有了AI&#xff0c;一切都有了可能。以下是我通过通义千问大模型开发的简单网站。 一、提示词与AI输出 提示词1 你是python程序员&#xff0c;我有一个大的需求&am…

使用代理IP数据采集都需要注意那些?

“在当今大数据时代&#xff0c;数据采集成为了企业决策和个人研究的重要依据。然而频繁访问目标网站往往会引发IP被封锁的风险&#xff0c;这时使用代理IP就显得尤为重要。但代理IP的使用并非毫无风险&#xff0c;以下是使用代理IP进行数据采集时需要注意的几个关键事项。” 一…

Spring Boot助力:小徐影院管理系统

第二章开发技术介绍 2.1相关技术 小徐影城管理系统是在Java MySQL开发环境的基础上开发的。Java是一种服务器端脚本语言&#xff0c;易于学习&#xff0c;实用且面向用户。全球超过35&#xff05;的Java驱动的互联网站点使用Java。MySQL是一个数据库管理系统&#xff0c;因为它…

一些硬件知识(二十四)

如何测量芯片电源的上电时序&#xff1a; FPGA和很多的CPU上电都有一个上电时序&#xff1a;也就是先那部分上电&#xff0c;后那部分上电&#xff0c;这样主板才能正常开机&#xff0c;否则会开机异常&#xff1a; 设置触发参数&#xff0c;选择单次触发&#xff1a; 小米手环…

赋值运算符重载

背景&#xff1a; 在EHR模块进行调试时&#xff0c;发现QVector3D对象进行赋值时&#xff0c;出现变量未赋值成功问题。 问题描述&#xff1a; 在进行代码调试时&#xff0c;发现赋值操作未成功&#xff0c;导致代码逻辑异常&#xff0c;经过分析&#xff0c;发现QVector3D 赋…

数据结构——二叉树的性质和存储结构

二叉树的抽象类型定义 基本操作&#xff1a; CreateBiTree(&T&#xff0c;definition) 初始条件&#xff1a;definition给出二叉树T的定义。 操作结果:按definition构造二叉树T。 PreOrderTraverse(T) 初始条件:二叉树T存在。 操作结果:先序遍历T&#xff0c;对每个结…

springboot农产品销售信息微信小程序—计算机毕业设计源码35557

摘 要 在信息飞速发展的今天&#xff0c;网络已成为人们重要的信息交流平台。每天都有大量的农产品需要通过网络发布&#xff0c;为此&#xff0c;本人开发了一个基于springboot农产品销售信息微信小程序。 对于本农产品销售信息系统的设计来说&#xff0c;它主要是采用后台采…

D21【python接口自动化学习】-python基础之内置数据类型

day21 内置数据类型文档使用 学习日期&#xff1a;20240928 学习目标&#xff1a;内置数据类型--30 内置数据类型参考&#xff1a;如何使用官方文档与帮助&#xff1f; 学习笔记&#xff1a; 使用官方文档 可通过终端查看数据类型的方法 使用帮助 总结 官方文档是体系化的…

端侧Agent系列 | 端侧AI Agent任务拆解大师如何助力AI手机?(详解版)

引言 简介 Octo-planner 规划和执行Agent框架 规划数据集 基准设计 实验设计 结果 全量微调与LoRA 多LoRA训练与合并 不同基础模型的全量微调 不同数据集大小的全量微调 总结 实战 英文 中文示例1&#xff1a; 中文示例2&#xff1a; 0. 引言 人生到处知何似…

简单理解C++在C的基础上的改变

1.C语言的一些不足 我们首先看下面用C语言实现栈 #include<stdio.h> #include<assert.h> #include<stdlib.h> typedef int StackDateType; typedef struct Stack {StackDateType* _ps;size_t _size;size_t _capacity; }Stack; void StackInit(Stack* ps) {…

探索基因奥秘:汇智生物如何利用DNA亲和纯化测序技术革新生物医学研究?

引言&#xff1a; 随着生物医学技术的不断进步&#xff0c;我们对生命奥秘的探索越来越深入。在众多的生物技术中&#xff0c;表观组学分析技术逐渐成为研究的热点。本文将带您走进汇智生物&#xff0c;了解他们如何利用DNA亲和纯化测序技术&#xff08;DAP-seq&#xff09;推…

vue2 将页面生成pdf下载

项目场景&#xff1a; 在项目开发的过程中&#xff0c;经常有下载一些报表&#xff0c;有部分要求文档是pdf格式的文件&#xff0c;这时候可以插件快速地搭建一个将页面生成pdf文件的功能。 依赖支持 本次项目中主要使用的nodejs: 14.20.0&#xff0c;npm版本是6.14.17。 npm…