【C++】:搜索二叉树

news2025/2/22 8:59:30

朋友们、伙计们,我们又见面了,本期来给大家解读一下有关多态的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

​ 

目录

1. 搜索二叉树

1.1 概念

1.2 搜索二叉树操作

2. 模拟实现搜索二叉树 

2.1 非递归版本

2.1.1 基本构造

2.1.2 插入

2.1.3 删除

2.1.4 查找

2.2 递归版本

2.2.1 插入

2.2.2 删除

2.2.3 查找

2.2.4 中序遍历

3. 完整代码


1. 搜索二叉树

1.1 概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

1.2 搜索二叉树操作

 

1. 二叉树的查找

a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到空,还没找到,这个值不存在。

2. 二叉树的插入

插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

3. 二叉树的删除 

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点

2. 模拟实现搜索二叉树 

搜索二叉树有两种模型:

1. Key模型:节点中只存在一个值key,并且这个值不可以修改,比如后面学习到的set

2. Key_Value模型:节点中存在两个值,一个是key,不可修改,另一个是与key对应的value,可以修改,比如后面学习到的map

在这里我们只实现Key模型的搜索二叉树

2.1 非递归版本

2.1.1 基本构造
//节点
template<class K>
struct BSTreeNode
{
	BSTreeNode* _left;
	BSTreeNode* _right;

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

//搜索二叉树
template<class K>
class BSTree
{
public:
	typedef BSTreeNode<K> Node;

	//构造
	BSTree() //给定了缺省值
	{}
	//拷贝构造
	BSTree(const BSTree<K>& tmp)
	{
		_root = Copy(tmp._root);
	}
	//operator=
	BSTree<K> operator=(BSTree<K> tmp)
	{
		swap(_root, tmp._root);
		return *this;
	}
	//析构
	~BSTree()
	{
		Destroy(_root);
	}
private:
    //递归拷贝左右子树
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* newNode = new Node(root->_key);
		newNode->_left = Copy(root->_left);
		newNode->_right = Copy(root->_right);
		return newNode;
	}
    //后序遍历删除
	void Destroy(Node*& root)
	{
		if (root == nullptr)
		{
			return;
		}
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;
	}

private:
	Node* _root = nullptr;  //缺省值给空即可
}; 
2.1.2 插入

插入时如果为空直接插入即可,若存在节点,需要先进行判断,比根节点大的插入到它的右子树,比根节点小的插入左子树即可,这时需要注意的插入的节点需要与它的父节点进行链接,这时在往下比较的过程中就需要记录一下它的父节点。

//插入
	bool Insert(const K& key)
	{
		//如果为空可以直接插入链接
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

        //记录父节点
		Node* parent = nullptr;
		Node* cur = _root;
        //遍历找到合适的节点进行插入链接
		while (cur)
		{
			parent = cur;
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
				return false;
		}
		//链接
		cur = new Node(key);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		return true;
	}
2.1.3 删除

首先找到需要删除的节点,删除的时候需要注意分下面几种情况:

  • 左子树为空,可以直接删除
  • 右子树为空,可以直接删除
  • 左右子树都不为空需要使用替换法删除
  • 替换法:使用左子树最大的节点替换需要删除的节点,或者使用右子树最小的节点替换需要删除的节点,替换之后直接删除被替换的节点即可完成删除。
  • 需要注意的是在这个过程中需要记录父节点,在删除之后需要及时链接,并且要注意的是删除的节点是根节点的时候可以直接将它的左右子树直接链接。
//删除
	bool Erase(const K& key)
	{
		Node* cur = _root;
		//记录父亲
		Node* parent = nullptr;
		while (cur)
		{
			//找到要删除的key
			if (cur->_key > key)
			{
				//更新父亲
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				//开始删除
				if (cur->_left == nullptr) //左为空  //直接删除
				{
					//先判断是否为根节点
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else  //不为根节点
					{
						if (cur == parent->_left) 
						{
							parent->_left = cur->_right;
						}
						else if (cur == parent->_right)
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;
				}
				else if (cur->_right == nullptr)   //右为空
				{
					//先判断是否为根节点
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)
						{
							parent->_left = cur->_left;
						}
						else if (cur == parent->_right)
						{
							parent->_right = cur->_left;
						}
					}
					delete cur;
				}
				else    //左右子树都不为空  //使用替换法
				{
					Node* parent = cur;
					//右树的最小节点进行替换或者左树的最大节点
					Node* subRight = cur->_right;
					while (subRight->_left)  //找到右树的最小节点
					{
						parent = subRight;
						subRight = subRight->_left;
					}

					swap(cur->_key, subRight->_key);  //替换两个节点

					//将删除节点的右树链接在它的父亲
					if (parent->_left == subRight)
					{
						parent->_left = subRight->_right;
					}
					else 
					{
						parent->_right = subRight->_right;
					}
					delete subRight;
				}
				return true;
			}
		}
		return false;
	}
2.1.4 查找
//查找
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else
				return true;
		}
		return false;
	}

2.2 递归版本

2.2.1 插入

递归插入时也需要进行一层封装,在里面传递root,在这里采用引用传参比较好,可以不用额外的链接,遇到空直接创建一个节点即可,直接在原数上进行操作。

//插入
	bool InsertR(const K& key)
	{
		return _InsertR(key, _root);
	}

bool _InsertR(const K& key, Node*& root)
	{
		//树为空直接插入即可
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		//递归左
		if (root->_key > key)
			return _InsertR(key, root->_left);
		else if (root->_key < key)  //递归右
			return _InsertR(key, root->_right);
		else
			return false;
	}
2.2.2 删除

还是采用里面封装一层,在递归删除的时候先递归找到要删除的key,然后判断它的左右子树,如果左右子树只存在一个可以直接进行删除,然后将它的孩子链接在它的节点上,如果左右孩子均存在,使用替换法,用该节点的右子树的最小节点进行替换,先使用循环找到该最小节点,然后与其交换,然后转化为递归该节点右子树的删除问题即可。

//删除
	bool EraseR(const K& key)
	{
		return _EraseR(key, _root);
	}
bool _EraseR(const K& key, Node*& root)
	{
		if (root == nullptr)
			return false;
		//查找key
		if (root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _EraseR(root->_left, key);
		}
		else  //找到了进行删除操作
		{
			if (root->_left == nullptr)  //作为空直接删除
			{
				Node* del = root;
				root = root->_right;
				delete del;
				return true;
			}
			else if (root->_right == nullptr)  //右为空也可以直接进行删除
			{
				Node* del = root;
				root = root->_left;
				delete del;
				return true;
			}
			else   //左右都不为空
			{
				Node* subRight = root;  //找到右树的最小节点
				while (subRight->left)
				{
					subRight = subRight->_left;
				}
				swap(root->_key, subRight->_key);  //交换
				
				return _EraseR(key, root->_right);   //转化为递归右子树的子问题
			}
		}
	}
2.2.3 查找
//查找
	bool FindR(const K& key)
	{
		_FindR(key, _root);
	}
bool _FindR(const K& key, Node* root)
	{
		if (root == nullptr)
			return false;
		if (root->_key > key)
			return _FindR(root->_left);
		else if (root->_key < key)
			return _FindR(root->_right);
		else
			return true;
	}
2.2.4 中序遍历

中序遍历时需要封装一层,在外面不好传递节点,中序遍历:左子树、根、右子树

	//中序遍历
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

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

3. 完整代码

#pragma once
#include <iostream>

using namespace std;

template<class K>
struct BSTreeNode
{
	BSTreeNode* _left;
	BSTreeNode* _right;

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

template<class K>
class BSTree
{
public:
	typedef BSTreeNode<K> Node;

	//构造
	BSTree() //给定了缺省值
	{}
	//拷贝构造
	BSTree(const BSTree<K>& tmp)
	{
		_root = Copy(tmp._root);
	}
	//operator=
	BSTree<K> operator=(BSTree<K> tmp)
	{
		swap(_root, tmp._root);
		return *this;
	}
	//析构
	~BSTree()
	{
		Destroy(_root);
	}

	//非递归版本
	//插入
	bool Insert(const K& key)
	{
		//如果为空可以直接插入链接
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			parent = cur;
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
				return false;
		}
		//链接
		cur = new Node(key);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		return true;
	}
	//删除
	bool Erase(const K& key)
	{
		Node* cur = _root;
		//记录父亲
		Node* parent = nullptr;
		while (cur)
		{
			//找到要删除的key
			if (cur->_key > key)
			{
				//更新父亲
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				//开始删除
				if (cur->_left == nullptr) //左为空  //直接删除
				{
					//先判断是否为根节点
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else  //不为根节点
					{
						if (cur == parent->_left)
						{
							parent->_left = cur->_right;
						}
						else if (cur == parent->_right)
						{
							parent->_right = cur->_right;
						}
					}
					delete cur;
				}
				else if (cur->_right == nullptr)   //右为空
				{
					//先判断是否为根节点
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)
						{
							parent->_left = cur->_left;
						}
						else if (cur == parent->_right)
						{
							parent->_right = cur->_left;
						}
					}
					delete cur;
				}
				else    //左右子树都不为空  //使用替换法
				{
					Node* parent = cur;
					//右树的最小节点进行替换或者左树的最大节点
					Node* subRight = cur->_right;
					while (subRight->_left)  //找到右树的最小节点
					{
						parent = subRight;
						subRight = subRight->_left;
					}

					swap(cur->_key, subRight->_key);  //替换两个节点

					//将删除节点的右树链接在它的父亲
					if (parent->_left == subRight)
					{
						parent->_left = subRight->_right;
					}
					else
					{
						parent->_right = subRight->_right;
					}
					delete subRight;
				}
				return true;
			}
		}
		return false;
	}

	//查找
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else
				return true;
		}
		return false;
	}

	
	//递归版本
	//插入
	bool InsertR(const K& key)
	{
		return _InsertR(key, _root);
	}
	//删除
	bool EraseR(const K& key)
	{
		return _EraseR(key, _root);
	}
	//查找
	bool FindR(const K& key)
	{
		_FindR(key, _root);
	}
	//中序遍历
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

private:
	//插入
	bool _InsertR(const K& key, Node*& root)
	{
		//树为空直接插入即可
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		//递归左
		if (root->_key > key)
			return _InsertR(key, root->_left);
		else if (root->_key < key)  //递归右
			return _InsertR(key, root->_right);
		else
			return false;
	}
	//删除
	bool _EraseR(const K& key, Node*& root)
	{
		if (root == nullptr)
			return false;
		//查找key
		if (root->_key < key)
		{
			return _EraseR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _EraseR(root->_left, key);
		}
		else  //找到了进行删除操作
		{
			if (root->_left == nullptr)  //作为空直接删除
			{
				Node* del = root;
				root = root->_right;
				delete del;
				return true;
			}
			else if (root->_right == nullptr)  //右为空也可以直接进行删除
			{
				Node* del = root;
				root = root->_left;
				delete del;
				return true;
			}
			else   //左右都不为空
			{
				Node* subRight = root;  //找到右树的最小节点
				while (subRight->left)
				{
					subRight = subRight->_left;
				}
				swap(root->_key, subRight->_key);  //交换

				return _EraseR(key, root->_right);   //转化为递归右子树的子问题
			}
		}
	}
	//查找
	bool _FindR(const K& key, Node* root)
	{
		if (root == nullptr)
			return false;
		if (root->_key > key)
			return _FindR(root->_left);
		else if (root->_key < key)
			return _FindR(root->_right);
		else
			return true;
	}
	//中序遍历
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
	//拷贝
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* newNode = new Node(root->_key);
		newNode->_left = Copy(root->_left);
		newNode->_right = Copy(root->_right);
		return newNode;
	}
	//销毁
	void Destroy(Node*& root)
	{
		if (root == nullptr)
		{
			return;
		}
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
		root = nullptr;
	}
private:
	Node* _root = nullptr;
};

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!     

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

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

相关文章

xml文本转Java对象

Java对象转String public static String toData(Object object) throws Exception {JAXBContext jc JAXBContext.newInstance(object.getClass());Marshaller m jc.createMarshaller();StringWriter output new StringWriter(2048);m.marshal(object, output);String data …

jsp 个人网站系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 个人&#xff08;博客&#xff09;网站系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&…

【电子通识】为什么电阻都是2.2、3.3、4.7、5.1这样的小数,而不是整数?

刚开始接触电路设计可能会对市面上已经有的电阻值如&#xff1a;2.2Ω、4.7Ω、5.1Ω、22Ω、47Ω、51Ω&#xff0c;通常都不是整数觉得非常困惑&#xff0c;所以查阅了一些资料&#xff0c;总结如下&#xff1a; 电阻是使用指数分布来设计生产的&#xff0c;即遵循国际电工委…

老年女性认知功能低于男性 |CHARLS CLHLS CFPS公共数据库周报(11.29)

欢迎参加郑老师2023年孟德尔随机化课程即将开始 发表文章后退款&#xff01;郑老师科研统计课程详情 CHARLS公共数据库 CHARLS数据库简介中国健康与养老追踪调查(China Health and Retirement LongitudinalStudy&#xff0c;CHARLS)是一项持续的纵向调查&#xff0c;旨在调查中…

贝锐花生壳3大安全能力,保障网络服务安全远程连接

在没有公网IP的情况下&#xff0c;使用内网穿透工具&#xff0c;将本地局域网服务映射至外网&#xff0c;虽然高效快捷&#xff0c;但信息安全也是不可忽略的方面。 对此&#xff0c;贝锐花生壳提供了多维度的安全防护能力&#xff0c;满足不同场景下用户安全远程访问内网服务的…

使用webstrom编写vue开启提示

1.语言服务器选择 2.文件类型–忽略的文件和文件夹&#xff0c;删去&#xff0c;node_modules&#xff0c;就可以点进去库了 3.禁用JSLint、TSLint 4.开启node辅助 5.如果是vite&#xff0c;开启自动读取&#xff0c;或手动指定 6.如果是Webpack&#xff0c;开启自动读取&#…

【开源】基于Vue和SpringBoot的在线课程教学系统

项目编号&#xff1a; S 014 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S014&#xff0c;文末获取源码。} 项目编号&#xff1a;S014&#xff0c;文末获取源码。 目录 一、摘要1.1 系统介绍1.2 项目录屏 二、研究内容2.1 课程类型管理模块2.2 课程管理模块2…

插入排序与希尔排序(C语言实现)

1.插入排序 由上面的动图可以知道插入排序的逻辑就是从第一个元素开始往后遍历&#xff0c;如果找到比前一个元素小的&#xff08;或者大的&#xff09;就往前排&#xff0c;所以插入排序的每一次遍历都会保证前面的数据是有序的&#xff0c;接下类用代码进行讲解。 我们这里传…

高效便捷的淘宝商品详情关键词搜索API接口

联讯数据可以介绍一些高效便捷的淘宝商品详情关键词搜索API接口。 以下是一些可以考虑使用的API接口&#xff1a; 阿里云搜索引擎API&#xff1a;阿里云搜索引擎API是一个基于云计算技术的搜索引擎&#xff0c;提供商品详情关键词搜索功能。它支持中文搜索&#xff0c;并且具…

【漏洞复现】FLIR AX8红外线热成像仪命令执行漏洞

漏洞描述 eledyne FLIR 设计、开发、制造以及强大的传感和意识技术。自透射热图像、可见光图像、可见频率分析、来自测量和诊断的先进威胁测量系统以及日常生活的创新解决方案。 Teledyne FLIR 提供多种产品用于政府、国防、工业和商业市场。我们的产品,紧急救援人员,军事人…

Billu_b0x

信息收集 #正常进行信息收集就好Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-18 22:07 CST Nmap scan report for 192.168.182.142 (192.168.182.142) Host is up (0.00073s latency).PORT STATE SERVICE 22/tcp open ssh 80/tcp open http | http-cookie-flags:…

mybatis的理解(下)(注解开发)

http://t.csdnimg.cn/kcPCy log4j(日志的配置) http://t.csdnimg.cn/IWa9H mybatis的理解(上)(包括xml配置信息进行CRUD) http://t.csdnimg.cn/weXLx mybatis的理解(中)(Mapper代理开发) 首先我们看一下项目结构 注解进行CRUD,其实和xml文件配置一样&#xff0c;只不过…

ChatGPT新媒体运营神器:轻松驾驭内容创作与传播

文章目录 1. 内容创作2. 社交媒体管理3. 用户互动与客户服务 《巧用ChatGPT轻松玩转新媒体运营》内容简介作者简介目录前言/序言本书内容本书特色本书读者对象获取方式 随着互联网的高速发展&#xff0c;新媒体已经成为了人们获取信息、交流思想的重要渠道。在这个信息爆炸的时…

KNN朴素贝叶斯(根据已知推测未知)

KNN&#xff08;哲学思想&#xff1a;物以类聚&#xff0c;人以群分&#xff09; KNN算法原理及示例1&#xff1a; 向量化 画点&#xff0c;计算欧式距离&#xff1a; 朴素贝叶斯&#xff08;数学工具&#xff1a;贝叶斯公式&#xff09; 1.数学原理&#xff1a; 2.一道大题&…

HTML如何使用图片链接

文章目录 图片链接的使用常见图片类型PNGJPGGIFBMP 图片链接的使用 在 HTML 中&#xff0c;图像由 标签定义。 是空标签&#xff0c;意思是说&#xff0c;它只包含属性&#xff0c;并且没有闭合标签。 语法&#xff1a; <img src图片路径">title“鼠标悬浮在图片上…

五月天“假唱”争议持续升温,歌迷期待真实音符背后的真实交代

在12月3日的夜晚&#xff0c;“五迷”们心中的星辰仿佛黯淡了几分。在社交媒体上&#xff0c;关于五月天演唱会假唱的争论愈演愈烈&#xff0c;歌迷们的心情变得异常复杂。他们愤怒&#xff0c;是因为自己的偶像受到了质疑&#xff1b;他们伤心&#xff0c;是因为可能的假唱让他…

云LIS实验室信息管理系统源码——实验室信息管理解决方案

云LIS&#xff08;Cloud Laboratory Information System&#xff09;是一种为区域医疗提供临床实验室信息服务的计算机应用程序&#xff0c;其主要功能是协助区域内所有临床实验室相互协调并完成日常检验工作&#xff0c;对区域内的检验数据进行集中管理和共享&#xff0c;通过…

基于Springboot+mybatis+mysql+jsp招聘网站

基于Springbootmybatismysqljsp招聘网站 一、系统介绍二、功能展示四、其他系统实现五、获取源码 一、系统介绍 项目类型&#xff1a;Java EE项目 项目名称&#xff1a;基于SPringBoot的照片网站 项目架构&#xff1a;B/S架构 开发语言&#xff1a;Java语言 前端技术&…

react Hooks(useEffect)实现原理 - 简单理解

useEffect 语法&#xff1a; useEffect(setup, dependencies?) 含义: useEffect 是一个 React Hook&#xff0c;它允许你 将组件与外部系统同步。 useEffect 源码简单理解 一、mountEffect 和 upadateEffect useEffect 与其它 hooks 一样分为 mountEffect 和 upadateEffec…

面试题之Docker篇

1、Docker 是什么&#xff1f; Docker一个开源的应用容器引擎&#xff0c;是实现容器技术的一种工具&#xff0c;让开发者可以打包他们的应用以及环境到一个镜像中&#xff0c;可以快速的发布到任何流行的操作系统上。 2、Docker的三大核心是什么? 镜像&#xff1a;Docker的镜…