【C++修炼之路:二叉搜索树】

目录:

  • 二叉搜索树的概念
  • 构建一颗二叉树
    • 二叉树的查找
      • 二插树的插入
  • 二叉树的删除
      • 删除右子树的最小节点
    • 写一个中序来走这个二叉搜索树
        • 递归版删除(recursion)
          • 递归版插入(recursion)
            • 递归版查找(recursion)
  • BSTree.h的代码
    • test.cpp的代码
      • 二叉树的应用
        • 二叉树的性能分析

二叉搜索树的概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
1.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
2.若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3.它的左右子树也分别为二叉搜索树

在这里插入图片描述

构建一颗二叉树

在这里插入图片描述

二叉树的查找


	bool Find(const K& key)//查找
	{
		Node* cur = _root;
		while (cur)//如果比cur大走右边
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)//如果比cur小走左边
			{
				cur = cur->_left;
			}
			else//如果相等就找到了
			{
				return true;
			}
		}

		return false;//如果走到空找不到
	}

二插树的插入

	bool Insert(const K& key)
	{
		if (_root == nullptr)//如果是一颗空树
		{
			_root = new Node(key);
			return true;
		}

		Node* parent = nullptr;//定义一个父亲节点的指针
		Node* cur = _root;//定义一个cur指针找这个节点插入的位置
		while (cur)
		{
			if (cur->_key < key)//如果key值大就往右边走
			{
				parent = cur;//cur往下走的时候先给给我们parent
				cur = cur->_right;
			}
			else if (cur->_key > key)//如果key值小就往左边走
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;//如果相等就不插入
			}
		}

		cur = new Node(key);
		//cur是一个局部变量,需要和父亲节点链接起来
		//如果我的key大就链接到右边
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		//如果我的key小就链接在左边
		else
		{
			parent->_left = cur;
		}

		return true;
	}
	

二叉树的删除

	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
			{
				// 1、左为空
				// 2、右为空
				// 3、左右都不为空,替换删除
				if (cur->_left == nullptr)//左为空
				{
					if (parent == nullptr)
					{
						_root = cur->_right;
					}
					else
					{
						//如果我是父亲的左,那就让父亲的左指向我的右
						if (parent->_left == cur)
						{
							parent->_left = cur->_right;
						}
						//如果我是父亲的右,那就让父亲的右指向我的右
						else
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;//释放cur
				}
				else if (cur->_right == nullptr)//右为空
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						//如果我是父亲的左,那就让父亲的左指向我的左
						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						//如果我是父亲的右,那就让父亲的右指向我的左
						else
						{
							parent->_right = cur->_left;
						}

					}
					delete cur;//释放cur
				}
				else
				{
					//右子树的最小节点
					Node* parent = cur;
					Node* minRight = cur->_right;
					while (minRight->_left)//不等于空继续
					{
						parent = minRight;
						minRight = minRight->_left;
					}

					cur->_key = minRight->_key;//赋值
					if (minRight == parent->_left)
					{
						parent->_left = minRight->_right;//父亲的左指向它的右
					}
					else
					{
						parent->_right = minRight->_right;
					}

					delete minRight;//删除
				}

				return true;
			}
		}

		return false;//找不到
	}

删除右子树的最小节点

依次删除7、14、3、8,3和14属于直接删除的场景,那么删除3和8两个节点麻烦一点,就需要替换法进行删除的场景,代码和图示如下:
在这里插入图片描述

	
		else{
					//右子树的最小节点
					Node* parent = cur;//记录父亲
					//最开始父亲有可能就是最左节点,所以父亲不能为空,为空就不会进入循环了
					Node* minRight = cur->_right;//在右树里面找最左节点
					while (minRight->_left)//不等于空继续
					{
						parent = minRight;//最左节点给父亲
						minRight = minRight->_left;//往下走
					}

					cur->_key = minRight->_key;//赋值
					//判断最左节点在父亲的左边还是右边
					//因为parent有能在左边也有可能在右边
					if (minRight == parent->_left)
					{
						parent->_left = minRight->_right;//父亲的左指向它的右
					}
					else
					{
						parent->_right = minRight->_right;//父亲的右指向它的右
					}

					delete minRight;//替换删除
				}

				return true;
			}

写一个中序来走这个二叉搜索树

套一层:由于根是私有的调不动需要写一个子函数,让子函数去递归,因此我们需要套一层,也可以自己写一个getroot

	void InOrder()
	{
		_InOrder(_root);//调子函数递归
		cout << endl;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)//空树
			return;

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

递归版删除(recursion)

	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _EraseR(root->_left, key);
		}
		//相等就删除
		else
		{
			Node* del = root;//定义一个指针删掉原来的root
			if (root->_right == nullptr)
			{
				root = root->_left;
			}
			else if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else
			{
				Node* minRight = root->_right;
				while (minRight->_left)
				{
					minRight = minRight->_left;
				}
				//调用递归
				swap(root->_key, minRight->_key);
				return _EraseR(root->_right, key);

			}
			delete del;//释放
			return true;

		}

	}
递归版插入(recursion)
	//用引用链接父亲
	bool _InsertR(Node*& root, const K& key)
	{
		//插入
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		if (root->_key < key)
			return _InsertR(root->_right, key);

		else if (root->_key > key)
			return _InsertR(root->_left, key);
		else
			return false;
	}

	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;

		if (root->_key < key)
			return _FindR(root->_right, key);
		else if (root->key > key)
			return _FindR(root->_left, key);
		else
			return true;
	}
递归版查找(recursion)
	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;

		if (root->_key < key)
			return _FindR(root->_right, key);
		else if (root->key > key)
			return _FindR(root->_left, key);
		else
			return true;
	}

BSTree.h的代码

#pragma once
//二叉搜索树
template<class K>//类模板参数
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		:_key(key)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	BSTree()
		:_root(nullptr)
	{}//构造函数

	//树的拷贝构造
	BSTree(const BSTree<K>& t)
	{
		_root = Copy(t._root);
	}
	//树的赋值
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}


	//析构函数
	~BSTree()
	{
		Destroy(_root);
		_root = nullptr;
	}

	bool Insert(const K& 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;
	}

	bool Find(const K& key)//查找
	{
		Node* cur = _root;
		while (cur)//如果比cur大走右边
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)//如果比cur小走左边
			{
				cur = cur->_left;
			}
			else//如果相等就找到了
			{
				return true;
			}
		}

		return false;//如果走到空找不到
	}

	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
			{
				// 1、左为空
				// 2、右为空
				// 3、左右都不为空,替换删除
				if (cur->_left == nullptr)//左为空
				{
					if (parent == nullptr)
					{
						_root = cur->_right;
					}
					else
					{
						//如果我是父亲的左,那就让父亲的左指向我的右
						if (parent->_left == cur)
						{
							parent->_left = cur->_right;
						}
						//如果我是父亲的右,那就让父亲的右指向我的右
						else
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;//释放cur
				}
				else if (cur->_right == nullptr)//右为空
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						//如果我是父亲的左,那就让父亲的左指向我的左
						if (parent->_left == cur)
						{
							parent->_left = cur->_left;
						}
						//如果我是父亲的右,那就让父亲的右指向我的左
						else
						{
							parent->_right = cur->_left;
						}

					}
					delete cur;//释放cur
				}
				else
				{
					//右子树的最小节点
					Node* parent = cur;//记录父亲
					//最开始父亲有可能就是最左节点,所以父亲不能为空,为空就不会进入循环了
					Node* minRight = cur->_right;//在右树里面找最左节点
					while (minRight->_left)//不等于空继续
					{
						parent = minRight;//最左节点给父亲
						minRight = minRight->_left;//往下走
					}

					cur->_key = minRight->_key;//赋值
					//判断最左节点在父亲的左边还是右边
					//因为parent有能在左边也有可能在右边
					if (minRight == parent->_left)
					{
						parent->_left = minRight->_right;//父亲的左指向它的右
					}
					else
					{
						parent->_right = minRight->_right;//父亲的右指向它的右
					}

					delete minRight;//替换删除
				}

				return true;
			}
		}

		return false;//找不到
	}
	//套一层
	//由于根是私有的调不动需要写一个子函数,让子函数去递归,因此我们需要套一层,也可以自己写一个getroot
	void InOrder()
	{
		_InOrder(_root);//调子函数递归
		cout << endl;
	}
	//递归
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}


private:

	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);
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);
		return newRoot;
	}

	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _EraseR(root->_left, key);
		}
		//相等就删除
		else
		{
			Node* del = root;//定义一个指针删掉原来的root
			if (root->_right == nullptr)
			{
				root = root->_left;
			}
			else if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else
			{
				Node* minRight = root->_right;
				while (minRight->_left)
				{
					minRight = minRight->_left;
				}
				//调用递归
				swap(root->_key, minRight->_key);
				return _EraseR(root->_right, key);

			}
			delete del;//释放
			return true;

		}

	}


	//用引用链接父亲
	bool _InsertR(Node*& root, const K& key)
	{
		//插入
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		if (root->_key < key)
			return _InsertR(root->_right, key);

		else if (root->_key > key)
			return _InsertR(root->_left, key);
		else
			return false;
	}

	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;

		if (root->_key < key)
			return _FindR(root->_right, key);
		else if (root->key > key)
			return _FindR(root->_left, key);
		else
			return true;
	}
	//写一个中序来走这个二叉搜索树
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)//空树
			return;

		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}

private:
	Node* _root = nullptr;
};


void TestBSTree1()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };

	BSTree<int> t;
	for (auto e : a)a的第一个元素和最后一个元素就是e的范围
	{
		t.InsertR(e);
	}

	t.InOrder();//前序遍历
	BSTree<int> Copyt(t);
	Copyt.InOrder();
	t.InOrder();

	//t.EraseR(9);
	// t.InOrder();

	t.EraseR(14);
	t.InOrder();

	t.EraseR(3);
	t.InOrder();

	t.EraseR(8);
	t.InOrder();

	for (auto e : a)
	{
		t.Erase(e);
		t.InOrder();//根是私有调不动
	}
}



test.cpp的代码

#include<iostream>
using namespace std;
#include "BSTree.h"

int main()
{
	TestBSTree1();
	system("pause");
	return 0;
}

我们看下运行结果,这棵树就被我们删完了:
在这里插入图片描述

二叉树的应用

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。比如:给一个单词word,判断该单词是否拼写正确,以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
  2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

二叉树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为: l o g 2 N log_2 N log2N

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为: N 2 \frac{N}{2} 2N

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续学习的AVL树和红黑树就可以上场了。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python+java+nodejs基于vue的企业人事工资管理系统

根据系统功能需求分析&#xff0c;对系统功能的进行设计和分解。功能分解的过程就是一个由抽象到具体的过程。 作为人事数据库系统&#xff0c;其主要实现的功能应包括以下几个模块&#xff1a; 1.登录模块 登录模块是由管理员、员工2种不同身份进行登录。 2.系统管理模块 用户…

中级软件设计师备考---软件工程1

目录 经典的模型敏捷开发方法【的分类】信息系统开发方法【的分类】结构化设计---内聚与耦合结构化设计---系统结构/模块结构 需求的分类 经典的模型 瀑布模型&#xff1a;最早的一类、适用于需求明确的项目、结构化的典型代表 原型模型&#xff1a;先构造一个建议的系统原型再…

【系统集成项目管理工程师】计算题专题一

一、决策树和期望货币值 1、项目经理向客户推荐了四种供应商选择方案。每个方案损益值已标在下面的决策树上。根据预期收益值&#xff0c;应选择设备供应商 A.供应商1B.供应商2C.供应商3D.供应商4 解题&#xff1a; 供应商 1&#xff1a;60% * 10000 &#xff08;-30000&am…

Oracle SQL执行计划操作(13)——其他相关操作

该类操作主要包括以上未进行讲解的其他相关操作。根据不同的具体SQL语句及其他相关因素,如下各操作可能会出现于相关SQL语句的执行计划。 1)SELECT STATEMENT 检索表中数据。该操作出现于通过select语句检索表中数据时产生的执行计划。该操作具体如图15-1中节点0所示。 图1…

RISC-V OS(老师的OS) 基于 汪辰老师的视频笔记

前言 最后面没写完&#xff0c;以后再补。。。 RISC-V OS RVOS 介绍 操作系统定义 操作系统&#xff08;英语&#xff1a;Operating System&#xff0c;缩写&#xff1a;OS&#xff09;是一组系统软件程序&#xff1a; 主管并控制计算机操作、运用和运行硬件、软件资源。提…

SPSS如何进行对应分析之案例实训?

文章目录 0.引言1.对应分析2.多重对应分析 0.引言 因科研等多场景需要进行数据统计分析&#xff0c;笔者对SPSS进行了学习&#xff0c;本文通过《SPSS统计分析从入门到精通》及其配套素材结合网上相关资料进行学习笔记总结&#xff0c;本文对对应分析进行阐述。 1.对应分析 &a…

55、RK3588使用MPP编码yuv到h264、解码h264到yuv模块开发和测试

基本思想&#xff1a;需要使用独立模块代码去实现自己的逻辑功能&#xff0c;所以在基于官方源码基础上&#xff0c;和参考附录几个官方链接&#xff0c;搞出一版rk3588编码测试和解码测试demo 测试视频/生成h264/生成yuv 链接: https://pan.baidu.com/s/1HbpeqMJb8HcgFpzaKh…

【Linux学习】多线程——线程控制 | 线程TCB

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《Linux学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 线程控制 | 线程TCB &#x1f9f0;线程控制&#x1f3b4;线程创建&#x1f3b4;线程结束&#x1…

写作业用白光还是暖光?盘点色温4000K的护眼台灯

台灯的白光或者暖光指的是台灯的色温&#xff0c;低色温的光线看起来发黄发红&#xff0c;高色温的光线发白发蓝。 如果灯光的光源是高品质光源&#xff0c;本身没有蓝光问题&#xff0c;那么色温的选择对护眼的影响是比较少的&#xff0c;更多的是对人学习工作状态&#xff0c…

Linux 之 vi 文本编辑器(二)

1、文本编辑器简介 Linux 中最常用的文本编辑器&#xff1a; vi&#xff1a;类 Unix 系统中默认的文本编辑器 vim&#xff1a;vi 编辑器的增强版本&#xff0c;习惯上也称 vi vi 文本编辑器的作用和特性&#xff1a; vi 可以执行插入、删除、查找、替换等众多文本操作&…

Leetcode268. 丢失的数字

Every day a leetcode 题目来源&#xff1a;268. 丢失的数字 解法1&#xff1a;排序 代码&#xff1a; /** lc appleetcode.cn id268 langcpp** [268] 丢失的数字*/// lc codestart class Solution { public:int missingNumber(vector<int> &nums){int n nums.s…

ESP32设备驱动-Si1145红外接近-紫外 (UV) 指数和环境光传感器驱动

Si1145红外接近-紫外 (UV) 指数和环境光传感器驱动 文章目录 Si1145红外接近-紫外 (UV) 指数和环境光传感器驱动1、Si1145介绍2、硬件准备3、软件准备4、驱动实现1、Si1145介绍 Si1145/46/47 是一款低功耗、基于反射的红外接近、紫外 (UV) 指数和环境光传感器,具有 I2C 数字接…

【一起撸个DL框架】4 反向传播求梯度

CSDN个人主页&#xff1a;清风莫追 欢迎关注本专栏&#xff1a;《一起撸个DL框架》 文章目录 4 反向传播求梯度&#x1f965;4.1 简介4.2 导数与梯度4.3 链式法则4.4 示例&#xff1a;y2x1的梯度 4 反向传播求梯度&#x1f965; 4.1 简介 上一篇&#xff1a;【一起撸个DL框架】…

【OpenCV】 2D-2D:对极几何算法原理

2D-2D匹配: 对极几何 SLAM十四讲笔记1 1.1 对极几何數學模型 考虑从两张图像上观测到了同一个3D点&#xff0c;如图所示**。**我们希望可以求解相机两个时刻的运动 R , t R,t R,t。 假设我们要求取两帧图像 I 1 , I 2 I_1,I_2 I1​,I2​之间的运动,设第一帧到第二帧的运动为…

全国快递物流 API 实现快递单号自动识别的原理解析

概述 全国快递物流 API 是一种提供快递物流单号查询的接口&#xff0c;涵盖了包括申通、顺丰、圆通、韵达、中通、汇通等600快递公司的数据。该 API 的目标是为快递公司、电商、物流平台等提供便捷、快速、准确的快递物流信息查询服务。 数据采集和处理 全国快递物流 API 的…

自定义控件 (?/N) - 颜料 Paint

参考来源 一、颜色 1.1 直接设置颜色 1.1.1 setColor( ) public void setColor(ColorInt int color) paint.setColor(Color.RED) paint.setColor(Color.parseColor("#009688")) 1.1.2 setARGB( ) public void setARGB(int a, int r, int g, int b) paint.se…

Packet Tracer – 研究 VLAN 实施

Packet Tracer – 研究 VLAN 实施 地址分配表 设备 接口 IP 地址 子网掩码 默认网关 S1 VLAN 99 172.17.99.31 255.255.255.0 不适用 S2 VLAN 99 172.17.99.32 255.255.255.0 不适用 S3 VLAN 99 172.17.99.33 255.255.255.0 不适用 PC1 NIC 172.17.10.2…

数字化转型导师坚鹏:数字化转型背景下的企业人力资源管理

企业数字化转型背景下的企业人力资源管理 课程背景&#xff1a; 很多企业存在以下问题&#xff1a; 不清楚企业数字化转型目前的发展阶段与重要应用&#xff1f; 不知道企业数字化转型给企业人力资源管理带来哪些机遇与挑战&#xff1f; 不知道企业数字化转型背景下如何…

SQL注入攻防入门详解

毕业开始从事winform到今年转到 web &#xff0c;在码农届已经足足混了快接近3年了&#xff0c;但是对安全方面的知识依旧薄弱&#xff0c;事实上是没机会接触相关开发……必须的各种借口。这几天把sql注入的相关知识整理了下&#xff0c;希望大家多多提意见。 &#xff08;对于…

系统集成项目管理工程师 下午 真题 及考点(2020年下半年)

文章目录 2020年下半年试题一&#xff1a;第10章 项目质量管理&#xff0c;规划质量管理过程的输入试题二&#xff1a;第9章 项目成本管理&#xff0c;典型&#xff1a;EAC ACETC AC&#xff08;BAC-EV&#xff09;/CPI BAC/CPI试题三&#xff1a;第18章 项目风险管理&#x…