Esko Ukkonen: On-line Construction of Suffix Trees

Esko Ukkonen: On-line Construction of Suffix Trees

文章目录

  • Esko Ukkonen: On-line Construction of Suffix Trees
  • 一、后缀树的概念及应用【详见刘方州同学报告】
    • 1.1 字典树 Trie
    • 1.2 后缀树 Suffix Tree
    • 2 后缀树的应用
  • 二、朴素后缀树构造方法及问题
  • 三、线性时间内后缀树在线构造方法
    • 3.1 概念引入
      • (1)隐式后缀树 Implicit Suffix Tree
      • (2)终止符 $
      • (3)活动点active point、剩余后缀数 remainder
      • (4)后缀链接 Suffix Link
    • 3.2 过程演示
      • Rule 1(活动点更新规则1)
      • Rule 2(后缀链接创建规则)
      • Rule 3(活动点更新规则2)
    • 3.3 复杂度分析

一、后缀树的概念及应用【详见刘方州同学报告】

1.1 字典树 Trie

定义:字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。
用途:用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
优点:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
核心思想:空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
基本性质:
(1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。
(2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
(3)每个节点的所有子节点包含的字符都不相同。

1.2 后缀树 Suffix Tree

后缀树是一种数据结构,能快速解决很多关于字符串的问题。后缀树的概念最早由Weiner于1973年提出,既而由McCreight在1976年和Ukkonen在1992年和1995年加以改进完善。
给定一长度为n的字符串T=T_1 T_2 〖…T〗n和整数i (1<=i<=n),子串T_i T(i+1) 〖…T〗_n便都是字符串T的后缀。以字符串T=abcabxabcd为例,它的长度为10,所以abcabxabcd、bcabxabcd、cabxabcd、…、d都是T的后缀。规定空字串也是后缀。后缀树,就是包含一则字符串所有后缀的压缩字典树,压缩过程如图1所示。把abcabxabcd的所有后缀加入字典树并压缩后,我们得到如图2的后缀树。

图1 后缀字典树到后缀树的压缩过程

图2 abcabxabcd的后缀树

如图2所示,Suffix Tree与Trie的不同在于边不再只代表单个字符,而是每个边可以表示任意的长度,实操过程中用两个指针[from, to]实现,耗费O(1)的空间。

2 后缀树的应用

2.1 最长重复子串
2.2 最长公共子串
2.3 最长回文子串

二、朴素后缀树构造方法及问题

三、线性时间内后缀树在线构造方法

McCreight最初的构造法原则上要按逆序构造,也就是说字符要从末端开始插入。如此一来便不能作为在线算法, 它变得更加难以应用于实际问题,如数据压缩。
20年后,来自赫尔辛基理工大学的Esko Ukkonen把原算法作了一些改动,实现了线性时间内对字符串从左往右的后缀树在线构造方法。
对于所给的文本T,由一棵空树开始逐步构造T的后缀树。以abcabxabcd为例,先由a开始,接着是ab,然后abc,…,不断更新直到构造出abcabxabcd的后缀树。

3.1 概念引入

(1)隐式后缀树 Implicit Suffix Tree

按上述方法在线构造abcabxabcd的后缀树的过程中,我们必然会构造出字符串abcab的后缀树,对其所有后缀枚举如下:abcab、bcab、cab、ab、b。观察发现:后缀ab被包含在abcab中作为其前缀;后缀b被包含在bcab中作为其前缀。
如果为ab或b单独开辟新的分支,则不符合字典树的性质,即每个节点的所有子节点包含的字符都不相同。因此我们选择暂时认为ab、b隐式包含在已有分支里,这样构造出来的后缀树称为隐式后缀树。
在我看来,隐式后缀树是在扫描和构建过程中节省时间的一步操作。因为字符串尚未扫描结束,所以这些操作可以先记录着而暂时不去执行,其代价是需要引入接下来要提及的活动点和剩余后缀数。

(2)终止符 $

问题:如何确定查找后缀树得到的子串是一个后缀还是后缀的一部分?
解决:在文本后添加字母表以外的字符,比如 。这样,当查找到 a b 。这样,当查找到ab 。这样,当查找到ab或b 就说明这是一个后缀,避免了二义性。对 a b c a b 就说明这是一个后缀,避免了二义性。对abcab 就说明这是一个后缀,避免了二义性。对abcab的所有后缀重新枚举如下:xabxa 、 a b x a 、abxa abxa、bxa 、 x a 、xa xa、a 、 、 。观察发现:xa 不再是 x a b x a 不再是xabxa 不再是xabxa的前缀;a 不再是 b c a b 不再是bcab 不再是bcab的前缀。

(3)活动点active point、剩余后缀数 remainder

问题:我们提到了隐式后缀树,隐式什么时候变为显式?怎么变为显式?
比喻:隐式就像瘦子躲在胖子后面,瘦子露出马脚的时候就是变为显式的时候。
举例:以最终构建abcabx为例。第一阶段,我们从左到右在线构建abc的后缀树,如图3所示。
在这里插入图片描述
图3 abc的后缀树
第二阶段,我们需要构建abcab的后缀树,如图4所示。新增字符a新增字符b的情况下,不必大刀阔斧改变树的结构,只要悄悄在每条边后面追加a追加b即可。枚举abcab的后缀集合:abcab、bcab、cab、ab、b。观察发现,abcab、bcab、cab即为三条边,而ab、b隐式地包含其中。

在这里插入图片描述

图4 abcab的后缀树
第三阶段,我们需要构建abcabx的后缀树。新增字符x,x就是露出的马脚,因为已有的后缀树的边里没有包含abx的。露出马脚之时就是变为显式之日,通过裂变出新的边变为显式,如图5所示。
在这里插入图片描述
图5 abcabx的后缀树

问题:如何确定裂变的位置?
解决:引入活动点active point,用于确定裂变的位置。活动点active point是一个包括(active_node, active_edge, active_length)的三元组。该三元组在每次后缀树的搜索中更新。
根据active_node确定结点,根据active_edge确定结点的某一条边,根据active_length确定边上的某个位置。举例来说,如图6所示,active_node为0,即根节点,active_edge为a,即从根节点出发、字符为a的这条边,active_length为2,即该边索引为2的地方,即符号|所在位置。
在这里插入图片描述
图6 active point的含义解释

问题:裂变具体怎么做?
解决:如图7所示,以abcab->abcabx为例,裂变时进行以下操作:
① 根据活动点确定裂变位置(ab|cab),在|处添加节点。
② 在新增的节点后分裂出一个边为x的节点。

在这里插入图片描述
图7 裂变的过程
至此由于添加abx后缀引发的裂变完成。
问题:只裂变一次就够了吗?
思考:未必够。以abcab到abcabx为例,abcab的后缀树表示如图8所示。
在这里插入图片描述
图8 abcab的后缀树
已知abcabx的后缀集合:abcabx、bcabx、cabx、abx、bx、x。
如果仅在已有的边后面添加x,只能覆盖abcabx、bcabx、cabx,剩下abx、bx、x。由于每次裂变能得到1个新后缀,直觉上来说需要进行3次裂变,事实上也是如此。上述过程只完成了添加abx后缀引发的裂变。
解决:引入剩余后缀数remainder,表示还需要插入多少个新的后缀,即还需要裂变几次。remainder=0时,表示当前新增字符处理完毕,继续扩展下一字符。

(4)后缀链接 Suffix Link

根据上述问题思考已经明确了两个事实:
① 活动点active point指向要裂变的位置。
② 要裂变的位置可能不止一个。
问题:每次都重新从根节点搜索下一裂变处吗?
解决:不需要。引入后缀链接Suffix Link,用于快速找到下一裂变处。
问题:后缀链接怎么创建?
规则:每进行一次裂变,会新增非叶节点和新边。后缀链接从该新增的非叶节点出发,指向下一次裂变新增的非叶节点,用虚线连接。
问题:为什么后缀链接可以快速找到下一裂变处?
观察:后缀链接连接的节点存在的关系:如果有一个从A指向B的后缀链接,那么从根节点到A节点表示的子串剔除第首个字符后得到的即为从根节点到B节点表示的子串。
举例:图9中节点4表示字符串ab,节点6表示字符串b,ab剔除掉首个字符a后得到b。实际处理时,每一次裂变意味着一个待添加后缀的成功插入(abx),下一次裂变的工作就是插入下一个待添加后缀(bx),下一个待添加后缀(bx)与当前成功插入的后缀(abx)的关系也是这样。
好处:可以方便地从一个后缀跳到另一个后缀,降低算法的时间复杂度。
在这里插入图片描述

3.2 过程演示

图10 后缀树构建过程演示Step 1-空树

图11 后缀树构建过程演示Step 2-a

图12 后缀树构建过程演示Step 3-ab

图13 后缀树构建过程演示Step 4-abc

图14 后缀树构建过程演示Step 5-abca

注意到已经存在的第一条边前缀中隐式包含了a。隐式包含在实操过程中的做法就是更新活动点active point和剩余后缀数remainder。
单就后缀树而言,这颗后缀树没有准确地描述当前读到的字符串abca。如何保证最后扫描结束的时候后缀树可以准确表示呢?回扣前文终结符的引入:在遇到 并做完相应操作后,后缀树可以准确地描述 a b c a 并做完相应操作后,后缀树可以准确地描述abca 并做完相应操作后,后缀树可以准确地描述abca

图15 后缀树构建过程演示Step 6-abcab

更新active point:a后面有b,length+1,往后挪;还在同一条边上,node和edge不变;
更新remainder+1=2:表示需要插入两个后缀ab、b。
问题:为什么remainder+1?
因为上一步的a实际上没有被插入树中,所以它被remain了下来,而后我们又继续处理b了,所以上一步没插入的a延长成了ab。加入的1即需要插入新的后缀b。
问题:再看active point和remainder记录信息似乎是缺失的:记录了后缀ab即将添加的位置,记录了2个待添加后缀,却没有记录b即将添加的位置?为什么呢?
先说结论:“既然第一条边有ab|cab,那在第一条边后面(也就是第二条第三条边里)一定存在去掉a以b开头的后缀”。
再详细解释:回顾一下这里第二条边是如何产生的?正是因为后缀树的逐步构建过程中a后接了b,所以我们先在第一条边的a追加成ab,再为b开辟新的边,即第二条边,所以这个第二条边就是显然的以b开头的后缀。换句话说,即使a后面不是紧跟的b,而是紧跟的是个x,那也会新开一条边以x开头的。

图16 后缀树构建过程演示Step 7-abcabx-插入abx

remainder=3,表示有3个待添加的后缀:abx、bx、x
我们按之前的逻辑来试图更新active point:ab之后找x,找不到!因此在裂变位置添加新的节点4、新的边x。这一步完成了后缀abx的添加。但是active point怎么更新呢?

Rule 1(活动点更新规则1)

当active_node = root 时遵循:
active_node 保持为 root;
active_edge 被设置为即将被插入的新后缀的首字符;
active_length 减1;

图17 后缀树构建过程演示Step 7-abcabx-插入abx后更新active point

根据Rule 1规则更新active point:
active_node 保持为 root;
active_edge 被设置为即将被插入的新后缀bx的首字符b;
active_length-1=1;
更新remainder:remainder-1=2,表示还有2个后缀待添加:bx、x。

图18 后缀树构建过程演示Step 8-abcabx-插入bx

裂变过程与abx的插入相似,不多赘述。依据Rule 1更新active point为 (0, ‘x’, 0)。
更新remainder-1=1,表示还有1个后缀x没有插入。
注:图里的remainder=0,我认为其意思是在扫描到x时回头处理之前的ab,但其实并没有真的扫描x,就像一个人往前走路只是把脚伸过去但没有迈过去踩实,所以默认观察到了x并处理了之前遗留的a和b,但尚未处理x,现在remainder=0说明a和b处理好了,开始处理下一个字符x,直接在根节点加边x。
除此以外,该步骤还需要创建后缀链接。

Rule 2(后缀链接创建规则)

如果我们分裂一条边并且插入一个新的节点,并且如果该新节点不是创建的第一个节点,则将先前插入的节点与该新节点通过一个特殊的指针连接,称为后缀链接,通过一条虚线来表示。
在图18中,因插入bx新裂变出来的节点6,不是扫描到x后创建的第一个节点了(第一个节点是因插入abx新裂变出来的节点4),因此创建后缀链接:4->6。
我的理解是“前人栽树,后人乘凉”,这条后缀链接在下一次碰到ab时会有用。

图19 后缀树构建过程演示Step 9-abcabx-插入x

active point为 (0, x, 0),length=0,所以在根节点创建边x。
remainder-1=0,表示可以处理下一个字符了。

图20 Step 10 后缀树构建过程演示-abcabxa

图21 后缀树构建过程演示Step 11-abcabxab
图22 后缀树构建过程演示Step 12-abcabxabc

更新active point(4,’c’,1);
更新remainder=3,表示需要插入abc、bc、c三个后缀。

图23 后缀树构建过程演示Step 13-abcabxabcd

更新remainder = 4,需要插入abcd、bcd、cd、d四个后缀;
更新active point:abc之后找d,找不到!因此在裂变位置添加新的节点9、新的边d。
问题:此时active point如何更新的?

Rule 3(活动点更新规则2)

当从active_node从不为root的节点分裂边时,我们沿着后缀链接的方向寻找节点:
如果存在一个节点,则设置该节点为 active_node;
如果不存在,则设置 active_node 为 root。
上述两种情况的active_edge 和 active_length 保持不变。

根据Rule 3,图23的active point为(6, c, 1)。
更新remainder = 3,并且开始处理下一个剩余后缀bcd。
bc后并没有没出现d,裂变。

图24 后缀树构建过程演示Step 14-abcabxabcd-插入bcd

裂变创建了节点11和边d。除了更新之外,根据Rule2创建后缀链接:9->11。
此时,我们观察两个后缀链接:
4 (ab) -> 6 (b)
9 (abc) -> 11 (bc)
remainder-1=2,表示还需要插入cd、d;
根据Rule 3更新active point为(root, c, 1)。

图25 后缀树构建过程演示Step 15-abcabxabcd-插入cd

更新active point时:由于找不到后续d,因此裂变:新建节点13和边d;
更新remainder-1 = 1;
依据Rule 2创建后缀链接:11 -> 13。

图26 后缀树构建过程演示Step 16-abcabxabcd-插入d

更新active point时:由于找不到后续d,因此裂变,新建边d;
更新remainder-1=0。

图27 后缀树构建过程演示Step 17-abcabxabcd$

对树结构的改变仅需在根节点上插入一条新边$。
整个后缀树构建完成。

3.3 复杂度分析

关于隐式后缀树,如果扫描发现要被插入的字符已经存在于树中,则无需修改树的结构。原因是要被插入的字符实际上已经隐式地被包含在了当前的树中。而active point和remainder的更新记录了这部分信息,确保了在后续的操作中会进行处理。

问题:算法结束时remainder > 0的情况存在吗?
解决: remainder > 0意味着当前仍为隐式后缀树,虽然隐式包含着后缀信息,但是整个树并没有准确表达所有后缀。
我们在字符串末尾添加 以避免这种情况。由于 以避免这种情况。由于 以避免这种情况。由于一定不同于字符串中任一字符的特性,使得在扫描到$时一定将之前所有隐式包含的后缀暴露出来并处理,处理的过程中remainder递减至0,最终完成整个后缀树的构建。
remainder记录了还余下多少后缀需要插入。这些插入操作将逐个的与当前位之前的后缀进行对应,我们需要一个接着一个的处理。每次插入需要O(1)时间。active point记录了插入(裂变)的位置。仅需在该位置新增一个字符,因为其他字符都被隐式地包含了。每次插入后:对于remainder,相应减少;对于active point:如果存在后缀连接的node续接至下一个节点;如果不存在则返回root节点;如果已经是在root节点了,则依据Rule1来修改活动点。无论哪种情况,仅需O(1)时间。
如果这些插入操作中,如果发现要被插入的字符已经存在于树中,则什么也不做,即使 remainder>0。原因是要被插入的字符实际上已经隐式地被包含在了当前的树中。而 remainder>0 则确保了在后续的操作中会进行处理。

整体算法复杂度是多少?
如果输入字符串的长度为n,则有n步需要执行,加上$有n+1步。在每一步中:
要么消耗O(1) 时间更新active point和remainder;
要么消耗O(1) 时间执行裂变插入操作;
因此整体算法复杂度为O(n)。

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

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

相关文章

怎么办xgp会员一年多少钱xgp会员怎么开轻松教你xgp会员开通教程

怎么办&#xff1f;xgp会员一年多少钱&#xff1f;xgp会员怎么开&#xff1f;轻松教你xgp会员开通教程 XGP平台是由微软公司开发的xbox游戏平台的pc版本&#xff0c;为电脑玩家提供了一个游玩微软游戏的平台&#xff0c;XGP平台因其独特的会员服务而广受玩家们好评&#xff0…

git log 和git reflog命令的区别是什么

git log和git reflog是Git中用于查看仓库历史的两个不同命令&#xff0c;它们各自有不同的用途和显示的信息。 git log git log命令用于显示当前分支的提交历史。它会列出每次提交的SHA-1哈希、作者信息、提交日期和提交信息等。git log提供了许多选项来定制输出的历史记录&a…

《深度学习在医学图像分析中的应用(第二版)》

书籍&#xff1a;Deep Learning for Medical Image Analysis, 2nd Edition 作者&#xff1a;S. Kevin Zhou&#xff0c;Hayit Greenspan&#xff0c;Dinggang Shen 出版&#xff1a;Academic Press书籍下载-《深度学习在医学图像分析中的应用&#xff08;第二版&#xff09;》本…

采用php vue2 开发的一套医院安全(不良)事件管理系统源码(可自动生成鱼骨图)

采用php vue2 开发的一套医院安全&#xff08;不良&#xff09;事件管理系统源码&#xff08;可自动生成鱼骨图&#xff09; 医院安全&#xff08;不良&#xff09;事件管理系统采用无责的、自愿的填报不良事件方式&#xff0c;有效地减轻医护人员的思想压力&#xff0c;以事件…

Linux开发板配置静态IP

1、查看网口信息&#xff0c;易知eth0无IP地址 ifconfig2、首先分配一个IP地址 sudo ifconfig eth0 192.168.5.8 up3、此时配置的IP地址只是临时的&#xff0c;当你reboot重启板子上电后&#xff0c;ip地址会消失&#xff0c;因此需要为板子配置静态ip&#xff0c;避免每次上…

快速排序算法(Python版)

1、什么是快速排序算法 快速排序&#xff08;Quick Sort&#xff09;是一种经典的排序算法&#xff0c;属于比较排序的一种。它的基本思想是通过分治的策略将原始的无序序列划分成较小的子序列&#xff0c;然后分别对这些子序列进行排序&#xff0c;最终合并得到有序的序列。 …

【prometheus学习过程】

目录 一、linux服务器监控常用的监控指标 二、监控docker1、使用CAdvisor2.配置prometheus采集docker的样本数据3.添加触发器3.添加触发器 三、监控MySQL容器1.创建普通用户向数据库授予权限2.Docker部署部署MySQLD Exporter二进制安装mysqld-exporter 四、进程监控1、process …

一次违法网站的渗透经历

0x01 前言 在一次攻防演练中&#xff0c;我发现了一个有趣的渗透路径。在信息收集阶段&#xff0c;我注意到目标网站和用户资产网站共享相同的IP网段。这意味着它们可能在同一台服务器上托管&#xff0c;或者至少由同一家互联网服务提供商管理。这种情况为我们的渗透测试提供了…

System.Exception有哪些类型分别什么意思

在C#中&#xff0c;System.Exception 类是所有异常类的基类。它是处理错误和异常情况的基础。当程序执行期间发生错误时&#xff0c;通常会抛出&#xff08;throw&#xff09;一个异常对象。这个对象随后被捕获&#xff08;catch&#xff09;并处理&#xff0c;或者如果没有被捕…

【window环境、Linux环境、QT三种方法实现TCP通信】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、Windows环境下实现TCP通信1.服务器2.客户端3.运行 二、Linux环境下实现TCP通信1.服务端2.客户端 三、Qt实现TCP通信1.服务端1.客户端 总结 前言 大多数项目…

datax与多种数据库间数据类型映射

文章目录 前言一、背景二、映射HiveHBaseMongoDBOraclePostgreSQLSqlServerMysqlODPSOSS 总结 前言 datax内置的enumType数据类型与不同数据库间都有些差异&#xff0c;本文整理出相应的映射关系&#xff0c;在配置自定义json时可以参考做一些类型转换 一、背景 DataX 是阿里…

告别SQL注入攻击之扰!揭秘强大防护策略,筑牢网站安全防线,畅享无忧体验!

SQL注入攻击是一种极具破坏性的Web漏洞&#xff0c;它利用应用程序对用户输入的处理不当&#xff0c;让恶意用户能够执行非授权的SQL查询&#xff0c;进而对数据库造成巨大损害。这种攻击方式可能导致数据泄露、系统崩溃等严重后果&#xff0c;因此必须引起高度重视。 为了有效…

(二)小程序学习笔记——初识:标签、数据绑定、指令介绍

1、rpx&#xff1a;是微信小程序的自适应的单位&#xff0c;根据不同设备的屏幕宽度进行自适应缩放。 2、小程序规定任何型号的手机的屏幕宽度都为 750rpx。 3、小程序中常用的组件&#xff1a;view、swiper&#xff08;滑块视图容器—轮播图的盒子&#xff09;和swipe-item&…

语义分割模型——浅谈U-net相关理论

目录 1 U-net简介 1.1 U-net是什么 1.2 U-net的创新点及优势 2 U-net改进思路 2.1 编码器优化 2.2 跳跃连接优化 2.3 解码器优化 2.4 其他优化方式 2.5 注意事项 1 U-net简介 1.1 U-net是什么 Ronneberger等人于2015年基于FCN&#xff08;全卷积神经网络&#xff09…

预测房屋价格(使用SGDRegressor随机梯度下降回归)

线性回归&#xff1a;预测未来趋势01&#xff08;预测房屋价格&#xff09; 文章目录 线性回归&#xff1a;预测未来趋势01&#xff08;预测房屋价格&#xff09;前言一、案例介绍&#xff1a;二、架构图&#xff1a;&#xff08;流程图&#xff09;三、使用了什么技术&#xf…

代码随想录:二叉树18(Java)

目录 105.从前序与中序遍历序列构造二叉树 题目 代码 106.从中序与后序遍历序列构造二叉树 题目 代码 105.从前序与中序遍历序列构造二叉树 题目 给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的…

企业网络安全的全方位解决方案

从安全域划分到云端管理&#xff0c;全面构建企业网络安全防线 在数字化浪潮席卷全球的今天&#xff0c;企业网络安全已经成为商业运营中不可忽视的一部分。随着企业数字资产价值的不断攀升&#xff0c;网络安全挑战也愈发严峻。数据泄露、勒索病毒、挖矿木马等高危风险频繁发…

利用Django中的缓存系统提升Web应用性能

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 在构建现代Web应用时&#xff0c;性能通常是至关重要的考虑因素之一。为了提高用户体验和应…

Java-基础知识-包-重命名文件/目录

在 IntelliJ IDEA 或其他 Java IDE 中&#xff0c;当你遇到“Package name does not correspond to file path”这样的错误提示时&#xff0c;它意味着你的源代码文件所在的目录结构与其声明的包名&#xff08;package name&#xff09;不匹配。这通常是由于以下几个原因造成的…

Linux中V4L2使用

Video for Linuxtwo(Video4Linux2)简称V4L2&#xff0c;是V4L的改进版。V4L2是linux操作系统下用于采集图片、视频和音频数据的API接口&#xff0c;配合适当的视频采集设备和相应的驱动程序&#xff0c;可以实现图片、视频、音频等的采集。在远程会议、可视电话、视频监控系统和…