【数据结构】二叉搜索树--BST,Binary Search Tree

文章目录

  • 二叉搜索树
    • 1. 二叉搜索树的概念
    • 2. 二叉搜索树的接口
      • 2.1 查找
        • 非递归查找
        • 递归查找
      • 2.2 中序遍历
      • 2.3 插入
        • 非递归插入
        • 递归插入
      • 2.4 删除
        • 非递归删除
        • 递归删除
    • 3. 二叉搜索树的应用
      • key搜索模型
      • kv搜索模型
    • 5. oj题


二叉搜索树

1. 二叉搜索树的概念

二叉搜索树又称二叉排序树,它可以是一颗空树,或者是一个满足如下性质的二叉树:

  • 如果左子树不为空,左子树上所有节点的值都小于根节点
  • 如果右子树不为空,右子树上所有节点的值都大于根节点

且它的左右子树,也满足上述性质。

举个栗子吧,以6为根节点的BST树结构:

template<class K>
struct bstree_node
{bstree_node<K>(K key): _left(nullptr), _right(nullptr), _key(key){}bstree_node<K>* _left;bstree_node<K>* _right;K _key;
};template<class K>
class bstree
{
public:typedef bstree_node<K> node;
private:node* _root = nullptr;
};

两个又相同数组构成的二叉树,但是却形成了两个不同树结构。二叉搜索树最大的问题是会退化,比如右单枝,顺序插入时就会退化成一个链表。

搜索二叉树的效率体现在搜索上,最坏情况搜索高度次。所以树的高度越低,性能越好。所以一般不会使用单纯的搜索二叉树,而是使用升级版的AVL 树(平衡二叉搜索树)红黑树

大概了解一下
AVL 树(平衡二叉搜索树)
AVL树
红黑树
在这里插入图片描述

 

2. 二叉搜索树的接口

2.1 查找

若根节点不为空:

  • 如果节点值等于 k e y key key,返回 t r u e true true
  • 如果节点值小于 k e y key key,到其右子树中查找,
  • 如果节点值大于 k e y key key,到其左子树中查找,

走到空树还没找到,则返回 f a l s e false false

非递归查找
bool find(const K& key)
{// 从根节点开始查找node* cur = _root;// 循环遍历树,直到当前节点为空(即没有找到)while (cur){// 如果查找的键值小于当前节点的键值if (key < cur->_key){// 移动到当前节点的左子节点,因为左子节点的键值更小cur = cur->_left;}// 如果查找的键值大于当前节点的键值else if (key > cur->_key){// 移动到当前节点的右子节点,因为右子节点的键值更大cur = cur->_right;}// 如果查找的键值等于当前节点的键值else{return true;}}return false;
}

二叉搜索树的查找非常的迅速,在二叉树相对平衡的情况下,时间复杂度高度次 O ( l o g N ) O(logN) O(logN)。最差是线性状态,为 O ( N ) O(N) O(N)

递归查找

递归函数 find_r 会沿着树的结构向下搜索,每次比较 key 和当前节点的键值,决定向左或向右移动。这种递归调用会一直持续到找到匹配的键值或者遍历完整个树。

基本情况(终止条件):
如果 root 是 nullptr,表示已经到达了树的末端,没有找到匹配的键值,因此返回 false。

递归调用:
如果 key 小于 root->_key,说明要查找的键值可能在左子树中,因此递归调用 _find_r(root->_left, key)。
如果 key 大于 root->_key,说明要查找的键值可能在右子树中,因此递归调用 _find_r(root->_right, key)。

找到匹配键值:
如果 key 等于 root->_key,表示找到了匹配的键值,立即返回 true。

bool find_r(const K& key)
{return _find_r(_root, key);
}bool _find_r(node* root, const K& key)
{if (root == nullptr)return false;if (key < root->_key)return _find_r(root->_left, key);else if (key > root->_key)return _find_r(root->_right, key);elsereturn true;
}

2.2 中序遍历

二叉搜索树的中序遍历结果,就是树中元素排成升序的结果。

void inorder()
{_inorder(_root);std::cout << std::endl;
}void _inorder(node* root)
{if (root == nullptr)return;_inorder(root->_left);std::cout << root->_key << " ";_inorder(root->_right);
}

一般成员函数在类外调用时,无法直接传入成员变量作参数。故可以将主体逻辑包装成子函数,再由成员函数去调用即可。

void TestBSTree() 
{BSTree<int> t;int a[] = { 7,1,3,6,4,8,7,9,3,2,5 }; //排序数组for (auto e : a) {t.Insert(e); //插入二叉搜索树}t.Inorder(); //中序遍历
}

如上代码,相当于利用二叉搜索树排序数组,而二叉搜索树结构天然具有排序加去重的功能。

2.3 插入

二叉搜索树的插入也很简单,共分两种情况:

  1. 树为空,则直接插入,
  2. 树不为空,则按性质查找到插入位置,再插入新节点。

cur指针进行比较并向下移动的同时,父指针parent始终指向cur的父节点。当cur走到空的时候,就创建新节点并链接到父节点的指针上。

  1. 插入的值比当前节点的值小,则插入到左子树中,
  2. 反之,比当前节点的值大,则插入到右子树中。
非递归插入

非递归插入方法通过迭代遍历二叉搜索树来找到新节点的插入位置。首先检查树是否为空,如果是,则直接将新节点设为根节点。如果树不为空,则从根节点开始,使用一个当前节点指针 cur 和一个父节点指针 parent 来跟踪遍历过程。在遍历过程中,根据新键值与当前节点键值的比较结果,决定向左子树还是右子树移动。如果新键值小于当前节点键值,则移动到左子节点;如果大于,则移动到右子节点。如果遇到相同键值的节点,则插入失败并返回 false。当 cur 为空时,表示已经到达了插入位置,此时根据新键值与 parent 节点键值的比较结果,在父节点的左或右子节点位置插入新节点。这种方法直观且易于理解,不需要额外的栈空间,适用于大多数情况下的树操作。

bool insert(const K& key)
{// 如果根节点为空,直接将新节点作为根节点if (_root == nullptr){_root = new node(key);return true;}node* cur = _root;node* parent = nullptr;// 循环找到合适的插入位置while (cur){if (key < cur->_key){parent = cur;cur = cur->_left;}else if (key > cur->_key){parent = cur;cur = cur->_right;}elsereturn false; // 如果存在相同键值的节点,不插入}// 根据键值大小插入新节点if (key < parent->_key)parent->_left = new node(key);elseparent->_right = new node(key);return true;
}

可见,二叉搜索树的每次插入都是把节点放在叶节点的位置上。

递归插入

递归插入方法通过递归调用函数自身来遍历二叉搜索树,并找到新节点的插入位置。递归函数 _insert_r 接受当前节点指针的引用和要插入的键值作为参数。如果当前节点为空,表示到达了插入位置,此时创建一个新节点并将其赋值给当前节点指针,然后返回 true 表示插入成功。如果新键值小于当前节点键值,则递归调用函数在左子树中继续查找插入位置;如果新键值大于当前节点键值,则递归调用函数在右子树中查找。如果遇到相同键值的节点,则返回 false 表示插入失败。递归插入方法简洁优雅,代码量少,但可能会因为递归调用导致栈空间的使用增加,对于大型树结构可能存在性能问题。然而,对于小型或中型树结构,递归插入是一种高效且易于实现的方法。

bool insert_r(const K& key)
{return _insert_r(_root, key);
}bool _insert_r(node*& root, const K& key)
{// 如果当前节点为空,创建新节点并返回trueif (root == nullptr){root = new node(key);return true;}// 根据键值大小递归插入左子树或右子树if (key < root->_key)return _insert_r(root->_left, key);else if (key > root->_key)return _insert_r(root->_right, key);elsereturn false; // 如果存在相同键值的节点,不插入
}

参数类型是节点指针的引用,使得root不仅是节点指针,还是其父节点的左或右孩子指针,修改root也就修改了父节点的左右孩子。

2.4 删除

二叉搜索树的难点在于删除,因为节删除节点需要维护剩余节点的链接关系。

删除叶节点很容易,释放节点并置空父节点的指针即可。删除非叶结点,可以将子树中满足条件的节点替换上来。

如果节点不存在先返回 f a l s e false false,如果存在,则分以下几种情况:
1.直接删除:

直接删除解释
删除的节点有单个子节点左为空就让父节点指向右子树,右为空就让父节点指向左子树
删除的节点无子节点归类到上一类处理

2.替换删除:
在这里插入图片描述

替换删除解释
删除的节点左右子节点都有用左树的最大节点,或右树的最小节点,替换被删节点。
  1. 先将左树最大节点的左孩子托,或是右树最小节点的右孩子托付给父节点。
  2. 再将左树最大节点覆盖待删除的节点。
非递归删除
bool erase(const K& key)
{node* parent = nullptr; // 父节点指针初始化为nullptrnode* cur = _root; // 当前节点指针指向根节点while (cur) // 循环遍历直到当前节点为空{if (key < cur->_key) // 如果搜索键小于当前节点键值{parent = cur; // 更新父节点指针为当前节点cur = cur->_left; // 移动到左子节点}else if (key > cur->_key) // 如果搜索键大于当前节点键值{parent = cur; // 更新父节点指针为当前节点cur = cur->_right; // 移动到右子节点}else // 如果搜到到匹配的键值{if (!cur->_left) // 如果当前节点没有左子节点{if (parent == nullptr)_root = _root->_right;else if (cur == parent->_left)parent->_left = cur->_right;elseparent->_right = cur->_right;delete cur; // 删除当前节点}else if (!cur->_right) // 如果当前节点没有右子节点{if (parent == nullptr)_root = _root->_left;else if (cur == parent->_left)parent->_left = cur->_left;elseparent->_right = cur->_left;delete cur; // 删除当前节点}else // 如果当前节点有左右子节点{
#ifdef 方式1 // 采用方式1删除节点--直接删除node* min = cur->_right; // 当前节点的右子树中找到最小节点while (min->_left)min = min->_left;K minkey = min->_key;erase(min->_key);cur->_key = minkey;
#endif#ifdef 方式2 // 采用方式2删除节点--替换删除node* min_parent = cur; // 最小节点的父节点node* min = cur->_right; // 当前节点的右子树中找到最小节点while (min->_left){min_parent = min;min = min->_left;}cur->_key = min->_key; // 更新当前节点的键值为最小节点的键值if (min == min_parent->_left)min_parent->_left = min->_right;elsemin_parent->_right = min->_right;delete min; // 删除最小节点
#endif}return true;}}return false;
}
递归删除
bool erase_r(const K& key)
{// 调用递归删除函数,传入根节点指针和搜索键值return _erase_r(_root, key);
}bool _erase_r(node*& root, const K& key)
{// 如果当前子树根节点为空,返回falseif (root == nullptr)return false;// 根据搜索键值与当前节点键值的比较,决定进入左子树或右子树进行递归删除if (key < root->_key)return _erase_r(root->_left, key);else if (key > root->_key)return _erase_r(root->_right, key);else{// 保存当前节点指针node* del = root;// 如果当前节点没有左子节点,将右子节点替换当前节点if (!root->_left)root = root->_right;// 如果当前节点没有右子节点,将左子节点替换当前节点else if (!root->_right)root = root->_left;else{// 寻找右子树中的最小节点node* min = root->_right;while(min->_left)min = min->_left;root->_key = min->_key; // 更新当前节点键值为最小节点键值// 递归删除最小节点,并返回结果return _erase_r(root->_right, min->_key);}// 释放删除节点delete del;return true; // 返回删除成功}
}

 

3. 二叉搜索树的应用

搜索树有两种应用,key搜索模型和key/value搜索模型。此外,二叉搜索树插入重复值会失败,所以自带去重功能。

key搜索模型

key搜索模型查找返回真假,只能用来判断数据是否存在。应用场景如存储车牌号判断是否放行等。

kv搜索模型

通过key查找对应value,两个值是强相关的映射关系。 应用场景如中英互译,身份绑定等。

cppCopy code
// 结构体定义包括键值对节点和左右子节点指针
template<class K, class V>
struct bstree_node
{bstree_node<K, V>* _left;bstree_node<K, V>* _right;K _key;V _val;// 构造函数初始化键、值和左右子节点指针bstree_node<K, V>(const K& key, const V& val): _key(key), _val(val), _left(nullptr), _right(nullptr){}
};// 二叉搜索树类
template<class K, class V>
class bstree
{typedef bstree_node<K, V> node; // 节点类型定义为树节点结构体
public:// 插入节点函数,传入键值和值,返回插入是否成功bool insert(const K& key, const V& val){if (_root == nullptr) // 如果根节点为空{_root = new node(key, val); // 创建新节点作为根节点return true;}node* parent = nullptr; // 父指针初始化为空node* cur = _root; // 当前节点指针指向根节点while (cur) // 循环查找插入位置{if (key < cur->_key) // 如果插入键值小于当前节点键值{parent = cur; // 更新父指针为当前节点cur = cur->_left; // 移动到左子节点}else if (key > cur->_key) // 如果插入键值大于当前节点键值{parent = cur; // 更新父指针为当前节点cur = cur->_right; // 移动到右子节点}elsereturn false; // 如果键值相等,返回失败}if (key < parent->_key) // 根据父节点键值判断位置插入左子节点或右子节点parent->_left = new node(key, val); // 创建新节点作为左子节点else if (key > parent->_key)parent->_right = new node(key, val); // 创建新节点作为右子节点return true; // 返回插入成功}// 查找键值对应节点,返回找到的节点指针,如果不存在返回空指针node* find(const K& key){node* cur = _root; // 当前节点指针指向根节点while (cur) // 循环查找节点{if (key < cur->_key) // 如果搜索键值小于当前节点键值cur = cur->_left; // 移动到左子节点else if (key > cur->_key) // 如果搜索键值大于当前节点键值cur = cur->_right; // 移动到右子节点elsereturn cur; // 找到匹配键值时返回当前节点}return nullptr; // 没找到返回空指针}// 删除节点函数,传入键值,返回删除是否成功bool erase(const K& key){node* parent = nullptr; // 父节点指针初始化为空node* cur = _root; // 当前节点指针指向根节点while (cur) // 循环查找删除节点{if (key < cur->_key) // 如果键值小于当前节点键值{parent = cur; // 更新父节点指针为当前节点cur = cur->_left; // 移动到左子节点}else if (key > cur->_key) // 如果键值大于当前节点键值{parent = cur; // 更新父节点指针为当前节点cur = cur->_right; // 移动到右子节点}else // 找到匹配键值的节点{if (!cur->_left) // 如果当前节点没有左子节点{if (!parent) // 如果父节点为空,说明当前节点为根节点_root = _root->_right; // 将根节点替换为右子节点else if (cur == parent->_left) // 如果当前节点是父节点的左子节点parent->_left = cur->_right; // 将父节点的左子节点替换为当前节点的右子节点elseparent->_right = cur->_right; // 将父节点的右子节点替换为当前节点的右子节点delete cur; // 删除当前节点}else if (!cur->_right) // 如果当前节点没有右子节点{if (!parent) // 如果父节点为空,说明当前节点为根节点_root = _root->_left; // 将根节点替换为左子节点else if (cur == parent->_left) // 如果当前节点是父节点的左子节点parent->_left = cur->_left; // 将父节点的左子节点替换为当前节点的左子节点elseparent->_right = cur->_left; // 将父节点的右子节点替换为当前节点的左子节点delete cur; // 删除当前节点}else // 如果当前节点有左右子节点{node* min_parent = cur; // 最小节点的父节点初始化为当前节点node* min = cur->_right; // 最小节点指针初始化为当前节点的右子节点while (min->_left) // 寻找右子树中的最小节点{min_parent = min;min = min->_left;}cur->_key = min->_key; // 更新当前节点键值为最小节点键值cur->_val = min->_val; // 更新当前节点值为最小节点值if (min == min_parent->_left) // 将最小节点的父节点指向最小节点右子节点min_parent->_left = min->_right;elsemin_parent->_right = min->_right;delete min; // 删除最小节点}return true; // 返回删除成功}}return false; // 未找到匹配键值,返回删除失败}// 中序遍历函数,输出键值对void inorder(){_inorder(_root); // 调用内部中序遍历函数cout << endl; // 换行}// 中序遍历辅助函数void _inorder(node* root){if (!root) // 如果当前节点为空,返回return;_inorder(root->_left); // 递归遍历左子树cout << root->_key << "-" << root->_val << endl; // 输出当前节点键值对_inorder(root->_right); // 递归遍历右子树}private:node* _root = nullptr; // 根节点指针初始化为空
};

 

5. oj题

OJ链接题解链接
606. 根据二叉树创建字符串题解
102. 二叉树的分层遍历题解
107. 二叉树的层序遍历 II题解
236. 二叉树的最近公共祖先题解
剑指 Offer 36. 二叉搜索树与双向链表题解
105. 从前序与中序遍历序列构造二叉树题解
106. 从中序与后序遍历序列构造二叉树题解
144. 二叉树的前序遍历题解
94. 二叉树的中序遍历题解
145. 二叉树的后序遍历题解

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

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

相关文章

12-学生们参加各科测试的次数(高频 SQL 50 题基础版)

12-学生们参加各科测试的次数 -- 学生表中&#xff0c;id是唯一的&#xff0c;将他作为主表 -- CROSS JOIN产生了一个结果集&#xff0c;该结果集是两个关联表的行的乘积 -- 2行表,与3行表使用cross join,得到2*36行数据 select st.student_id, st.student_name,su.subject_na…

Vxe UI vxe-upload vue上传组件,显示进度条的方法

vxe-upload vue 上传组件 查看官网 https://vxeui.com 显示进度条很简单&#xff0c;需要后台支持进度就可以了&#xff0c;后台实现逻辑具体可以百度&#xff0c;这里只介绍前端逻辑。 vue 上传附件 相关参数说明&#xff0c;具体可以看文档&#xff1a; multiple 是否允许…

现代密码学-国密算法

商用密码算法种类 商用密码算法 密码学概念、协议与算法之间的依赖关系 数字签名、证书-公钥密码、散列类算法 消息验证码-对称密码 &#xff0c;散列类 安全目标与算法之间的关系 机密性--对称密码、公钥密码 完整性--散列类算法 可用性--散列类、公钥密码 真实性--公…

项目雅景临居---模块2 标签管理

一、 [根据类型]查询标签列表 Autowiredprivate LabelInfoService service;Operation(summary "&#xff08;根据类型&#xff09;查询标签列表")GetMapping("list")public Result<List<LabelInfo>> labelList(RequestParam(required false)…

Linux学习笔记6 进程角度看内存泄露

一&#xff0c;从进程角度看堆区内存申请与释放问题 1&#xff0c;c语言中的内存泄漏 内存溢出&#xff1a;申请内存时&#xff0c;没用足够的内存可以使用。 内存泄露&#xff1a;严格来说&#xff0c;只有对象不会再被程序用到了&#xff0c;但是GC又不能回收它们的情况&…

ue项目更改项目缓存 防止跑ue项目c盘占用过多内存

原本项目缓存 在ue引擎所在的文件夹里找好baseEnging.ini 搜索install 找到第7个install 更改缓存路径到指定位置

【Text2SQL 论文】QDecomp:探索 CoT-style 的 prompt 来解决 Text2SQL

论文&#xff1a;Exploring Chain of Thought Style Prompting for Text-to-SQL ⭐⭐⭐⭐ EMNLP 2023, arXiv:2305.14215 一、论文速读 本文通过对 LLM 使用 CoT-style 的 prompting 方法来解决 Text2SQL 问题&#xff0c;试图回答下面两个问题&#xff1a; 哪种 prompting s…

性能工具之 JMeter 常用组件介绍(二)

文章目录 一、Thread Group二、断言组件1、Response Assertion&#xff1a;响应断言2、Response Assertion&#xff1a;响应断言3、Duration Assertion&#xff1a;响应时间断言4.、JSON Assertion&#xff1a;json断言 一、Thread Group 线程组也叫用户组&#xff0c;是性能测…

通过血清拉曼光谱进行COVID-19的高效初步筛查

通过血清拉曼光谱进行COVID-19的高效初步筛查 原创 小王搬运工 时序课堂 2024-06-04 20:04 四川 论文地址&#xff1a;https://analyticalsciencejournals.onlinelibrary.wiley.com/doi/full/10.1002/jrs.6080 论文源码&#xff1a;无 期刊&#xff1a;JOURNAL OF RAMAN SPE…

LangChain学习之 Question And Answer的操作

1. 学习背景 在LangChain for LLM应用程序开发中课程中&#xff0c;学习了LangChain框架扩展应用程序开发中语言模型的用例和功能的基本技能&#xff0c;遂做整理为后面的应用做准备。视频地址&#xff1a;基于LangChain的大语言模型应用开发构建和评估。 2. Q&A的作用 …

Echarts 中type是value的X轴在设置了interval间隔后没有展示

文章目录 问题分析问题 Echarts中type是value的X轴在设置了interval间隔后没有展示 分析 之前代码是这样写的:axisLabel 属性中设置了 interval ,但未起作用,原因如下 在 ECharts 中,interval 属性是用于类目型(category)轴的刻度间隔设置,并不适用于数值型(value)…

解决 clickhouse jdbc 偶现 failed to respond 问题

背景 Clickhouse集群版本为 Github Clickhouse 22.3.5.5&#xff0c; clickhouse-jdbc 版本为 0.2.4。 问题表现 随着业务需求的扩展&#xff0c;基于Clickhouse 需要支持更多任务在期望的时效内完成&#xff0c;于是将业务系统和Clickhouse交互的部分都提交给可动态调整核心…

windows上安装MongoDB,springboot整合MongoDB

上一篇文章已经通过在Ubuntu上安装MongoDB详细介绍了MongoDB的各种命令用法。 Ubuntu上安装、使用MongoDB详细教程https://blog.csdn.net/heyl163_/article/details/133781878 这篇文章介绍一下在windows上安装MongoDB&#xff0c;并通过在springboot项目中使用MongoDB记录用户…

Go语言交叉编译

Golang 支持交叉编译&#xff0c; 在一个平台上生成然后再另外一个平台去执行。 以下面代码为例 build ├── main.go ├── go.mod main.go内容 package mainimport "fmt"func main() {fmt.Println("hello world") }windows系统上操作 1.cmd窗口编译…

java新特性--03-1--Stream---Collectors工具类

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1.stream 收集Collectors工具类注意区分 Collections工具类 练习1&#xff1a;查找工资大于6000的员工&#xff0c;结果返回为一个List练习2&#xff1a;查找年龄小…

【设计模式】结构型-组合模式

前言 在软件开发中&#xff0c;设计模式是一种被广泛应用的解决问题的方法论。其中&#xff0c;结构性设计模式是一类特别重要的模式&#xff0c;它们用于处理类或对象之间的组合关系&#xff0c;其中之一就是组合模式。组合模式允许客户端统一对待单个对象和对象的组合&#…

【前端技术】 ES6 介绍及常用语法说明

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

使用wheelnav.js构建酷炫的动态导航菜单

目录 前言 一、WheelNav是什么 1、项目地址 2、关于开源协议 3、相关目录介绍 二、如何使用wheelnav.js 1、新建html页面 2、设置style样式 3、创建展示元素实现动态导航 三、参数即方法介绍 1、参数列表 2、运行方法 3、实际成果 四、总结 前言 用户体验永远是一…

玩转盲盒潮流:从0到1搭建小程序平台

玩转盲盒潮流并搭建一个从0到1的小程序平台来创作内容是一个充满挑战但有趣的过程。以下是一个步骤指南&#xff0c;帮助你实现这一目标&#xff1a; 1. 市场调研与定位 了解盲盒市场&#xff1a;研究当前盲盒市场的趋势、消费者喜好和成功案例。确定目标用户&#xff1a;明确…

软件质量保障——三、四

三、黑盒测试 1.黑盒测试概述 1.1 如何理解黑盒测试&#xff1f; 1.2 黑盒测试有什么特点&#xff1f; 1.3 如何实施黑盒测试&#xff1f; 2. 黑盒测试用例设计和生成方法&#xff08;这里还是要自己找题做&#xff09; 2.1 等价类划分法 步骤&#xff1a; 1.选择划分准…