ʕ • ᴥ • ʔ

づ♡ど

 🎉 欢迎点赞支持🎉

个人主页:励志不掉头发的内向程序员;

专栏主页:C++语言;


文章目录

前言

一、二叉搜索树的概念

二、二叉搜索树的性能分析

三、二叉搜索树的插入

四、二叉搜索树的查找

五、二叉搜索树的删除

六、二叉搜索树的析构

七、二叉搜索树的拷贝

八、二叉搜索树 key 和 key/value 使用场景

6.1、key搜索场景

6.2、key/value 搜索场景:

6.3、key/value 二叉搜索树代码实现

总结


前言

这一章节我们来聊聊我们之前的二叉树的一种新形态二叉搜索树,这个形态也有一定的问题,但是它是我们后面讲解 map/set 容器的基础,我们到后面才会讲解改怎么解决二叉搜索树的方式,但是我们本章节搞明白二叉搜索树的原理和实现也非常重要。


一、二叉搜索树的概念

二叉搜索树又称二叉排序树,它要么是一颗空树,要么是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值。
  • 若它的右子树不为空,则右子树上所有结点的值都大于等于根节点的值。
  • 它的左右子树也分别为二叉搜索树。
  • 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习 map/set/multimap/multiset 系列容器底层就是二叉搜索树,其中 map/set 不支持插入相等值,multimap/multiset 支持插入相等值。

我们二叉树的逻辑就是,当插入一个新结点时,从头结点,也就是树的根结点开始做比较,如果比新结点大,就走到我们的右结点。如果比新小节点小,就走到我们的左节点,然后再和这些结点进行比较,一直到结点走到底,没有结点比较时就插入即可。

我们学习数据结构的时候可能有听说过我们二叉树如果单独存储数据是没有意义的。要想有意义,我们得在二叉树的基础上加入一些东西,这样才有意义,就比如在二叉树的基础上加入搜索二叉树的规则。

二、二叉搜索树的性能分析

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:log2(N)。

最差情况下,二叉树退化为单支树(或者类似单支),其高度为:N。

所以综合而言二叉搜索树增删查改时间复杂度为:O(N)。

这样的效率显然是无法满足我们需求的,我们后面章节会继续讲解二叉搜索树的变形,平衡二叉搜索树AVL树和红黑树,才能适用于我们在内存中存储和搜索数据。

另外,我们二分查找也可以实现 O( log2(N) ) 级别的查找效率,但是二分查找有两大缺陷:

  • 需要存储在支持下标随机访问的结构中,并且有序。
  • 插入和删除数据效率很低,因为存储在下标随机访问的数据结构中,插入和删除数据一般需要挪动数据。

三、二叉搜索树的插入

插入的具体过程如下:

  • 树为空,则直接新增结点,赋值给 root 指针。
  • 树不为空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
  • 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点(要注意的是要保持逻辑一致性,插入相等的值不要一会儿往左走,一会儿往右走)。

就如上图一样的插入方式。

我们先把二叉搜索树的框架搭建出来,创建一个结点类,这个结点包含左结点的指针和右结点的指针,以及一个要保存的类型。

namespace zxl
{template <class K>struct BSTNode{BSTNode<K>* _left;BSTNode<K>* _right;K _key;BSTNode(const K& key): _key(key),_left(nullptr),_right(nullptr){}};template <class T>class BSTree{typedef BSTNode<T> Node;public:private:Node* _root = nullptr;};
}

我们设计一个插入的成员函数,它的返回值可以是布尔类型来查看是否插入成功。我们可以通过while 循环比较的方式一直比较到 nullptr 时让循环停下来,但是此时我们的 cur 指向的是一个空指针,我们应该指向它的上一个指针,所以我们还得创建一个变量来记录上一个变量,通过 cur 的上一个变量来插入我们的新结点。我们这里实现的是不允许有相等的情况出现的,如果想要实现有相等的情况可以在比较那里改一下即可。

我们可以来看看下面的动画来帮助理解。

bool insert(const T& key)
{if (_root == nullptr){_root = new Node(key);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(key);if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;
}

此时我们可以写一个中序遍历来看看我们实现的怎么样,由于我们的二叉搜索树的结构,这导致我们的中序遍历出来的是一个从小到大排序好的数据,这样就可以验证我们的插入实现的是否成功。

我们把中序遍历写在私有位置,这样就可以不把根结点暴露出去。我们想要调用递归,就必须传Node 的指针,如果我们把我们的中序遍历写在公有,那它有默认的 this 指针,可以调用 _root,但是此时去下一层就得传下一层的地址,但是由于我们之前是靠 this 指针而得到的 _root。所以此时我们就无法传值到递归下一层。但是如果我们想要写一个传值的成员函数,不写在私有的话因为要传指针所以就得暴露我们的 _root。所以为了避免我们干脆直接写成私有的。

private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}

此时我们就有一个问题,那就是我们在外界没办法调用我们的中序遍历,此时我们可以在类内公有位置创建一个成员函数,让这个成员函数去调用我们的中序遍历即可。

public:void InOrder(){_InOrder(_root);cout << endl;}

此时我们就可以尝试去测试我们的插入了。

int main()
{zxl::BSTree<int> t;int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };for (int i = 0; i < sizeof(a) / sizeof(int); i++){t.insert(a[i]);}t.InOrder();return 0;
}

四、二叉搜索树的查找

我们查找的逻辑就和搜索的逻辑一样。

  • 从根开始比较,查找 x,x 比根的值大则往右边查找,x 比根小则往左边查找。
  • 最多查找高度次,走到空还没找到这个值就不存在。
  • 如果不支持插入相等的值,找到 x 即可返回。
  • 如果支持插入相等的值,意味着有多个 x 存在,一般要求查找中序的第一个 x。
bool Find(const T& key)
{Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else{return true;}}return false;
}

五、二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回 false。

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为 N)

  1. 要删除结点 N 左右孩子结点均为空。
  2. 要删除结点 N 左孩子结点为空,右孩子结点不为空。
  3. 要删除结点 N 右孩子结点为空,左孩子结点不为空。
  4. 要删除结点 N 左右孩子结点均不为空。

第一种情况:

第二种情况:

第三种情况:

第四种情况:

对应以上四种情况的解决方案:

  1. 由于 N 结点左右孩子都是空,所以我们直接销毁掉我们的这个结点即可,但是为了让它的父亲结点不会指向野指针,我们在销毁这个结点后要记得把指向它的父亲结点指向 nullptr。
  2. 把N结点的父亲对应孩子指针指向N的右孩子,然后就像 1 一样删除掉 N 结点即可。
  3. 把N结点的父亲对应孩子指针指向N的左孩子,然后就像 1 一样删除掉 N 结点即可。
  4. 无法直接删除 N 结点,因为 N 的两个孩子无处安放,只能用替换法删除。找 N 左子树的值最大结点 R(最右结点)或者 N 右子树的值最小结点 L(最左结点)代替 N,因为这两个结点中任意一个,放到 N 的位置,都满足二叉搜索树的规则。替代 N 的意思就是 N 和 R 的两个结点的值的交换,转而变成删除 R 结点,R 结点符号情况 2 或 情况 3,可以直接删除。

我们先用循环去找到我们的 key 值,逻辑和上面一样,当我们的找到我们 key 时,我们就开始讨论这 4 种情况,我们的情况 1 其实可以融合在情况 2 或 3中,所以这里就只讨论 3 种情况。

第一种情况:

第二种情况:

bool Erase(const T& key)
{Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{// 0-1个孩⼦的情况// 删除情况1 2 3均可以直接删除,改变⽗亲对应孩⼦指针指向即可if (cur->_left == nullptr){if (parent == nullptr){_root = cur->_right;}else{if (parent->_left == cur)parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;return true;}else if (cur->_right == nullptr){if (parent == nullptr){_root = cur->_left;}else{if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;}delete cur;return true;}else{// 2个孩⼦的情况// 删除情况4,替换法删除// 假设这⾥我们取右⼦树的最⼩结点作为替代结点去删除// 这⾥尤其要注意右⼦树的根就是最⼩情况的情况的处理,// 对应课件图中删除7的情况// ⼀定要把cur给rightMinP,否会报错。Node* rightMinP = cur;Node* rightMin = cur->_right;while (rightMin->_left){rightMinP = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;if (rightMinP->_left == rightMin)rightMinP->_left = rightMin->_right;elserightMinP->_right = rightMin->_right;delete rightMin;return true;}}}return false;
}

六、二叉搜索树的析构

我们二叉搜索树的析构可以使用递归,向我们的中序遍历一样得让递归函数私有,然后再在公有的地方实现析构函数以此来保护我们的根结点。

public:~BSTree(){Destroy(_root);_root = nullptr;}private:void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}

七、二叉搜索树的拷贝

和我们的析构同理,也是用递归来实现。

public:BSTree(const BSTree<K, V>& t){_root = Copy(t._root);}private:Node* Copy(Node* root){if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_key, root->_value);newRoot->_left = Copy(root->_left);newRoot->_right = Copy(root->_right);return newRoot;}

剩下的像默认构造没有必要实现,而赋值重载用我们的交换大法即可轻松完成,这里就不过多赘述。

八、二叉搜索树 key 和 key/value 使用场景

6.1、key搜索场景

只有 key 作为关键码,结构中只需要存储 key 即可,关键码即为需要搜索到的值,搜索场景只需要判断 key 在不在。key 的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改 key 破坏搜索树机构了。

场景1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则体现非本小区车辆,无法进入。

场景2:检查一篇英文文档拼写是否正确,将词库中所有的单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示。

6.2、key/value 搜索场景:

每⼀个关键码 key,都有与之对应的值 value,value 可以任意类型对象。树的结构中 (结点) 除了需要存储 key 还要存储对应的 value,增/删/查还是以 key 为关键字⾛⼆叉搜索树的规则进行比较,可以快速查找到 key 对应的 value。key/value 的搜索场景实现的⼆叉树搜索树⽀持修改,但是不⽀持修改 key,修改 key 破坏搜索树性质了,可以修改 value。

场景1:简单中英互译字典,树的结构中 (结点) 存储 key (英⽂) 和 vlaue (中⽂),搜索时输入英⽂,则同时查找到了英⽂对应的中⽂。

场景2:商场无人值守车库,入口进场时扫描车牌,记录⻋牌和入场时间,出口离场时,扫描车牌,查找入场时间,⽤当前时间 - 入场时间计算出停车时长,计算出停⻋费⽤,缴费后抬杆,车辆离场。

场景3:统计⼀篇⽂章中单词出现的次数,读取⼀个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数。

6.3、key/value 二叉搜索树代码实现

我们 key/value 的结构和我们上面的 key 结构相比,无非就是多了一个 value,我们在结点创建时多创建一个 value,然后在增删查的时候我们只要让我们传过来的值中的 key 进行比较,然后 value 不管,插入时把 key/value 都插入即可实现,我们可以通过下面的 key/value 的代码和上面的 key 作比较就能看出其中的相似。

namespace zxl
{template<class K, class V>struct BSTNode{// pair<K, V> _kv;K _key;V _value;BSTNode<K, V>* _left;BSTNode<K, V>* _right;BSTNode(const K& key, const V& value):_key(key), _value(value), _left(nullptr), _right(nullptr){}};template<class K, class V>class BSTree{typedef BSTNode<K, V> Node;public:BSTree() = default;BSTree(const BSTree<K, V>& t){_root = Copy(t._root);}BSTree<K, V>& operator=(BSTree<K, V> t){swap(_root, t._root);return *this;}~BSTree(){Destroy(_root);_root = nullptr;}bool Insert(const K& key, const V& value){if (_root == nullptr){_root = new Node(key, value);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(key, value);if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;}Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else{return cur;}}return nullptr;}bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{if (cur->_left == nullptr){if (parent == nullptr){_root = cur->_right;}else{if (parent->_left == cur)parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;return true;}else if (cur->_right == nullptr){if (parent == nullptr){_root = cur->_left;}else{if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;}delete cur;return true;}else{Node* rightMinP = cur;Node* rightMin = cur->_right;while (rightMin->_left){rightMinP = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;if (rightMinP->_left == rightMin)rightMinP->_left = rightMin->_right;elserightMinP->_right = rightMin->_right;delete rightMin;return true;}}}return false;}void InOrder(){_InOrder(_root);cout << endl;}private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << ":" << root->_value << endl;_InOrder(root->_right);}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}Node* Copy(Node* root){if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_key, root->_value);newRoot->_left = Copy(root->_left);newRoot->_right = Copy(root->_right);return newRoot;}private:Node* _root = nullptr;};}


总结

以上便是我们二叉搜索树的概念和实现了,二叉搜索树是一个非常厉害的思想,但是它却还是有一些弊端,那就是有的时候没有办法形成一个完美的完全搜索二叉树,为了解决这个问题,我们后面会讲解决的办法,所以我们一定要把这一章节搞明白。

🎇坚持到这里已经很厉害啦,辛苦啦🎇

ʕ • ᴥ • ʔ

づ♡ど

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

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

相关文章

【Linux】线程概念与控制

一. 线程的概念1.什么是线程线程是进程内部的一个执行流&#xff0c;是进程调度的基本单位。它具有轻量的特点&#xff0c;它的创建和销毁所消耗的资源更少&#xff0c;线程间切换比进程间切换消耗的资源更少&#xff1b;它与进程共享一张虚拟地址空间表&#xff0c;通过进程来…

双轴倾角传感器厂家与物联网角度传感器应用全解析

本文主要探讨双轴倾角传感器厂家的核心技术优势&#xff0c;以及物联网角度传感器在智能监测中的创新应用。同时&#xff0c;也详细介绍了水平监测传感器厂家的解决方案特点&#xff0c;并分析了专业进口倾角传感器代理所提供的原厂品质保障与本地化服务支持。以深圳瑞惯科技有…

容器-资源隔离机制

一. 引言&#xff1a; 大家都知道&#xff0c;在一台机器上&#xff0c;可以运行任意(根据系统资源)个容器实例。且各容器间是相互独立&#xff0c;不做任何关联的。那么&#xff0c;docker是通过什么方式来实现容器隔离的呢&#xff1f; 接下来我们了解下。 二. 关于容器隔离…

Agentic RL Survey: 从被动生成到自主决策

Agentic RL Survey: 从被动生成到自主决策 本文将系统解读《The Landscape of Agentic Reinforcement Learning for LLMs: A Survey》这篇综述。该综述首次将智能体强化学习&#xff08;Agentic RL&#xff09;与传统LLM-RL范式正式区分&#xff0c;通过MDP/POMDP理论框架梳理…

彻底禁用 CentOS 7.9 中 vi/vim 的滴滴声

在 VMware 虚拟机中安装的 CentOS 7.9 系统&#xff0c;即使通过修改 /etc/inputrc 禁用了终端铃声&#xff08;set bell-style none&#xff09;&#xff0c;vi 或 vim 编辑时仍可能发出滴滴声。这是因为 vi/vim 有自己独立的铃声控制机制。以下是解决方法&#xff1a;方法 1&…

基于A2A和ADK的内容规划代理

项目概述 Content Planner Agent 是一个基于 Google Agent Development Kit (ADK) 和 Python A2A SDK 构建的智能内容规划代理。该代理能够根据高层次的内容描述&#xff0c;创建详细的内容大纲。 什么是A2A Protocol A2A Protocol&#xff08;Agent2Agent 协议&#xff09;…

Linux-条件变量

文章目录条件变量概述条件变量的优缺点条件变量相关函数pthread_cond_init函数pthread_cond_destroy函数pthread_cond_wait函数pthread_cond_signal函数测试生产者和消费者模型条件变量 概述 与互斥锁不同&#xff0c;条件变量是用来等待而不是用来上锁的&#xff0c;条件变量…

[硬件电路-166]:Multisim - SPICE与Verilog语言的区别

SPICE与Verilog语言在电子设计领域中扮演不同角色&#xff0c;SPICE是电路仿真语言&#xff0c;用于精确模拟电路行为&#xff1b;Verilog是硬件描述语言&#xff0c;用于描述数字电路的结构和行为。以下是两者的详细区别&#xff1a;一、核心定位与用途SPICE&#xff1a;电路仿…

玩转Docker | 使用Docker部署Umbrel操作系统

玩转Docker | 使用Docker部署Umbrel操作系统 前言 一、 Umbrel 介绍 Umbrel简介 Umbrel主要特点 二、系统要求 环境要求 环境检查 Docker版本检查 检查操作系统版本 三、部署Umbrel服务 下载Umbrel镜像 编辑部署文件 创建容器 检查容器状态 检查服务端口 安全设置 四、访问Umbr…

Flink Task线程处理模型:Mailbox

Task的线程 和 MailboxProcessor 的绑定executingThread 是 Task 类&#xff08;StreamTask 的父类&#xff09;在构造时创建的物理线程。MailboxProcessor 是 StreamTask 用来处理异步事件和驱动其主要处理逻辑&#xff08;processInput&#xff09;的核心组件。它们之间的绑定…

OpenCV 银行卡号识别

目录 一、项目原理与核心技术 二、环境准备与工具包导入 1. 环境依赖 2. 工具包导入 三、自定义工具类 myutils.py 实现 四、主程序核心流程&#xff08;银行卡识别.py&#xff09; 1. 命令行参数设置 2. 银行卡类型映射 3. 辅助函数&#xff1a;图像展示 五、步骤 1…

计算机二级Python

一.静态语言和脚本语言高级语言根据计算机执行机制的不同分为两类&#xff1a;静态语言和脚本语言静态语言的核心特征&#xff1a;变量的类型在编译时&#xff08;写代码时&#xff09;就必须确定并固定下来&#xff0c;即在使用一个变量前必须显式地声明它地类型一旦声明&…

Mybatis Log Plugin打印日志,会导致CPU升高卡死

原因 大量日志输出:MyBatis Log Plugin 会打印大量的 SQL 日志,包括 SQL 语句及其参数。如果项目中 SQL 查询频繁且复杂,日志量会非常大,导致 CPU 使用率升高,甚至卡死。 日志级别设置不当:如果将日志级别设置为 DEBUG 或 TRACE,MyBatis 会输出非常详细的日志信息,这会…

鸿蒙:深色模式适配和浅色模式的切换

前言&#xff1a; 有些时候我们需要对应用进行深色模式的适配处理&#xff0c;并且在不需要的时候切换到浅色状态&#xff0c;下面和大家一起照着官方文档来学习。 下面是官方文档的链接&#xff1a; https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-dark-…

Coze源码分析-资源库-删除插件-后端源码-数据访问和基础设施层

5. 数据访问层 5.1 仓储接口定义 插件仓储接口 文件位置&#xff1a;backend/domain/plugin/repository/plugin.go type PluginRepository interface {// DeleteDraftPlugin 删除插件草稿DeleteDraftPlugin(ctx context.Context, pluginID int64) error// DeleteAPPAllPlugins …

案例一: 对基础选择器的使用【网页盒子】

【1】样例&#xff1a;首先&#xff0c;观察到&#xff0c;几个元素竖着排列的&#xff0c;所以使用块级元素&#xff0c;而不是行内元素。【2】代码演示<head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width,…

爬虫项目优化:如何用 Redis 实现 “断点续爬”?避免重复采集电商数据

在电商数据采集场景中&#xff0c;爬虫常因网络波动、服务器重启、IP 封禁等问题中断。若缺乏断点续爬机制&#xff0c;重启后需从头开始&#xff0c;不仅浪费带宽与时间&#xff0c;还可能因重复采集导致数据冗余。Redis 凭借其高性能、原子操作、多样数据结构的特性&#xff…

决策树概念与原理

决策树简介决策树是一种树形结构树中每个内部节点表示一个特征上的判断&#xff0c;每个分支代表一个判断结果的输出&#xff0c;每个叶子节点代表一种分类结果(仅举例无其他意义或隐喻)就像一个女孩去相亲&#xff0c;那么首先询问是否大于30&#xff0c;大于则不见&#xff0…

SQL面试题及详细答案150道(116-135) --- 高级查询与函数篇

《前后端面试题》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,MySQL,Linux… 。 前后端面试题-专栏总目录 文章目录 一、本文面试题目录 116. 如何使用CASE语句实…

VeRL:强化学习与大模型训练的高效融合框架

本文由「大千AI助手」原创发布&#xff0c;专注用真话讲AI&#xff0c;回归技术本质。拒绝神话或妖魔化。搜索「大千AI助手」关注我&#xff0c;一起撕掉过度包装&#xff0c;学习真实的AI技术&#xff01; 1 概述&#xff1a;VeRL的起源与核心价值 VeRL&#xff08;Versatile…