【C++】红黑树的封装——同时实现map和set

news2024/9/27 13:40:21

目录

  • 红黑树的完善
    • 默认成员函数
    • 迭代器的增加
  • 红黑树的封装
    • 红黑树模板参数的控制
    • 仿函数解决取K问题
    • 对Key的非法操作
  • insert的调整
  • map的[]运算符重载

在list模拟实现一文中,介绍了如何使用同一份代码封装出list的普通迭代器和const迭代器。今天学习STL中两个关联式容器mapset,其底层就是红黑树,而且是同一棵红黑树。所以今天学习如何用同一棵红黑树封装出mapset

红黑树的封装

红黑树的完善

默认成员函数

map和set的底层是红黑树,所以先将红黑树重要的四个默认成员函数完善好。这四个成员函数在介绍搜索二叉树时已进行过介绍,这里不再讲解。

public:
	//RBTree() = default;
	RBTree()
		:_root(nullptr)
	{}

	RBTree(const RBTree& t)//拷贝构造
	{
		_root = Copy(t._root);
	}

	RBTree& operator=(RBTree t)//赋值重载
	{
		swap(_root, t._root);
		return *this;
	}

	~RBTree()
	{
		Destroy(_root);
		_root = nullptr;
	}
private:
	Node* Copy(Node* root)
	{
		if (root == nullptr)
		{
			return nullptr;
		}
		Node* newnode = new Node(root->_kv);
		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;
	}

迭代器的增加

map和set作为关联式容器,迭代器是必不可少的,而map和set都是由同一颗红黑树封装而成,所以,map和set的迭代器其实就是红黑树的迭代器。

map和set的迭代器都是双向迭代器,也就是说都支持++,- -的操作。

map迭代器

set迭代器

红黑树的迭代器的结构和list的是类似的,其成员都只有一个节点类,只有一个构造函数初始化节点。

迭代器:

template<class T, class Ref, class Ptr>
struct RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef RBTreeIterator<T,Ref,Ptr> self;

	Node* _node;//成员
	RBTreeIterator(Node* node)
		:_node(node)
	{}
};

节点类:

template<class T>
struct RBTreeNode
{
	T _kv;
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	Color _col;

	RBTreeNode(const T& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _col(RED)//默认为红色
	{}

};

对于自封装的迭代器:解引用,是否相等的比较是少不了的,而这部分操作还是比较容易的,不理解的可参考list迭代器的实现

  • 注意此时要访问的的对象是_kv
	Ref operator*()
	{
		return _node->_kv;
	}

	Ptr operator->()
	{
		return &_node->_kv;
	}

	bool operator==(const self& s)
	{
		return _node == s._node;
	}

	bool operator!=(const self& s)
	{
		return _node != s._node;
	}

真正的难点是红黑是迭代器的遍历:中序遍历红黑树便能得到一个升序序列,而迭代器访问的顺序也是中序:左根右。采用Inorde来遍历是很简单的,但是它是不可控的,只能一把把红黑树全遍历完。而迭代器必须是一个一个节点访问的,这就增加了它实现的难度。

红黑树迭代器

前置++

对于++,即按中序访问下一个节点,但查找时此时的节点已经为根,所以查找的顺序为根,右;此时关注的点是下一个节点是否为空,由此分为两种情况。

  • 右子树不为空;则,右子树的最左节点即为下一个要访问的节点。
  • 右子树为空,说明当前子树已经访问完了;沿着到根节点的路径查找祖先节点中左孩子为父亲的节点即为下一个要访问的节点。
    • 如果父节点为祖先节点的右孩子,说明递归有一定深度,需要不断向上查找。
	self& operator++()
	{
		if (_node->_right)
		{
			Node* leftMost = _node->_right;
			while (leftMost->_left)
			{
				leftMost = leftMost->_left;
			}
			_node = leftMost;
			//return self(leftMost);
		}
		else//说明当前子树访问完了
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = parent;
				parent = parent->_parent;
			}
			//return self(parent);
			_node = parent;
		}
		return *this;
	}

前置++可以引用返回。

后置++

后置++逻辑与前置是一样的,只不过返回的是++之前的值。所以用ret保留原先的值,再访问下一个节点,最后返回ret即可。此时不能引用返回。

	self operator++(int)
	{
		self ret = *this;
		if (_node->_right)
		{
			Node* leftMost = _node->_right;
			while (leftMost->_left)
			{
				leftMost = leftMost->_left;
			}
			_node = leftMost;
		}
		else//说明当前子树访问完了
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return ret;
	}

前置- -

红黑树的- -即访问当前节点的前一个节点。此时的查找顺序变为根,左;自身就作为根节点,所以此时只需要注意左节点是否为空。

  • 左节点不为空,则按右根左的遍历顺序遍历;访问左子树的最右孩子。
  • 左节点为空,说明当前子树已经访问完了;沿着到根节点的路径查找祖先节点中右孩子为父亲的节点即为下一个要访问的节点。
    • 如果父节点为祖先节点的左孩子,说明递归有一定深度,需要不断向上查找
    self& operator--()
	{
		if (_node->_left)
		{
			Node* rightMost = _node->_left;
			while (rightMost->_right)
			{
				rightMost = rightMost->_right;
			}
			_node = rightMost;
		}
		else//说明当前子树访问完了
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_left)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}

后置- -

返回- -之前的节点。

	self operator--(int)
	{
		Node* ret = *this;
		if (_node->_left)
		{
			Node* rightMost = _node->_left;
			while (rightMost->_right)
			{
				rightMost = rightMost->_right;
			}
			_node = rightMost;
		}
		else//说明当前子树访问完了
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_left)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return ret;
	}

迭代器实现后,我们需要在红黑树的实现当中进行迭代器类型的typedef。需要注意的是,为了让外部能够使用typedef后的迭代器类型iterator,需要在public区域进行typedef。

然后在红黑树当中实现成员函数begin和end:

  • begin函数返回中序序列当中第一个结点的迭代器,即最左结点。
  • end函数返回中序序列当中最后一个结点下一个位置的迭代器,这里直接用空指针构造一个迭代器。

实现时红黑树一层命名为与map和set作区分,首字母大写。

template<class K, class T,class KeyOfT>//map时T为pair,set时为K
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	typedef RBTreeIterator<T, T&, T*> Iterator;
	typedef RBTreeIterator<T, const T&, const T*> Const_Iterator;

	//map和set的迭代器也是使用红黑树的迭代器,但树的结构有所区别。
	//红黑树的迭代器
	Iterator Begin()
	{
		Node* leftMost = _root;
		while (leftMost->_left)
		{
			leftMost = leftMost->_left;
		}
		return leftMost;//此时返回的是节点,而返回类型为迭代器,所以会发生隐式类型转化,调用红黑树的迭代器构造函数构造一个迭代器
	}

	Iterator End()//与库里的带头红黑树不同,我们这里不带头,为空即尾
	{
		return nullptr;
	}

	Const_Iterator Begin()const
	{
		Node* leftMost = _root;
		while (leftMost->_left)
		{
			leftMost = leftMost->_left;
		}
		return leftMost;
	}

	Const_Iterator End()const//与库里的带头红黑树不同,我们这里不带头,为空即尾
	{
		return nullptr;
	}
	
private:
	Node* _root = nullptr;

};

库中红黑树的结构实现

库里的红黑树结构中还有一个头节点header,header的parent指针指向_root,_root的parent指针指向header,header的左右指针分别指向红黑树的最小和最大节点。

在这里插入图片描述
由于结构不同,所以我们迭代器的实现也有一定缺陷,但不妨碍正常使用,所以就不进行完善了。

红黑树的封装

都知道setkey模型,而mapkey_value模型,如何才能使用同一棵红黑树满足二者的需求呢?这就是封装的魅力。

map

set
了解map和set之后,两者的冲突点主要有:

  1. mapKV模型,setK模型
  2. 获取Key
    • mapset储存数据的方式不一样;红黑树的大多数操作都是需要支持用Key比大小的。
  3. mapsetKey不可修改问题

红黑树模板参数的控制

关于set和map的不同模型问题,先看看库中是如何解决这个问题的。
库中红黑树对map和set的处理
对于红黑树而言,它是不知道上层是map还是set的,但是这些都不重要;底层红黑树利用泛型的思想,将map和set传过来的参数实例化为模板;这样一来,上层map传对应的参数过来,底层红黑树就将这些参数泛化成模板,就能生成对应数据类型的红黑树了;set也是同理。

如简化版的下图:map和set的底层都是一棵红黑树,其中map和set中的红黑树传入的第一个参数都为K;而map的第二个参数传入的是一个键值对pair,这也正是map的数据类型。set的第二个参数继续传入一个K,作为set的数据类型。也正是这样的设计,能够让一棵红黑树同时封装map和set。

这样一来,你上层传的是pair,底层红黑树实例出来的就是map,传入的为K,则为set。

红黑树模板参数的控制

  • 第三个模板参数是解决下一个问题所提供的仿函数。

set中调用的红黑树为什么要传两次K?

  • set中传两次K确实有点多余,但此时是map和set共用一棵红黑树,在map的日常使用中如:find,erase这样的操作,其参数就是一个K类型,所以map中是需要有K的。

  • map_find

  • map_erase

仿函数解决取K问题

所谓取K,就是由于map和set的数据类型不一致,一个是KV的键值对,一个是模板参数K。作为而平衡二叉树的红黑树,Key值的比对是少不了的,如插入,查找等功能都是需要有Key值的比对的。对于set来说,可以直接用传入的K进行比对;而map是pair,需要解引用访问其first才能找到Key,否则直接比对pair不一定是我们想要的比对结果。

pair的比对方式
所以,为了解决这个问题,继续参考上一个问题的解决方式:在map和set调用红黑树传参时传入一个可以取Key的仿函数。仿函数介绍

map的仿函数:要获取是数据是什么类型,仿函数的参数就是什么类型。

		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};

set的仿函数:set的仿函数看起来有点多此一举,但为了适配map的解决,提供一个仿函数也无妨。

		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

取Key
这样一来,set容器传入底层红黑树的就是set的仿函数,map容器传入底层红黑树的就是map的仿函数。

有了仿函数,当底层红黑树需要进行两个结点之间键值的比较时,都会通过传入的仿函数来获取相应结点的键值,然后再进行比较,下面以红黑树的查找函数为例。


	Iterator Find(const T& data)
	{
		KeyOfT kot;
		if (_root == nullptr)
		{
			return nullptr;
		}
		Node* cur = _root;
		//Node* parent = nullptr;
		while (cur)
		{
			if (kot(data) > kot(cur->_kv))
			{
				//parent = cur;
				cur = cur->_right;

			}
			else if (kot(data) < kot(cur->_kv))
			{
				//parent = cur;
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}

对Key的非法操作

mapset都是不支持修改Key的

map对此的解决方案是pair对中的Key用const修饰
Key修改问题

map成员的定义:

private:
		RBTree<K, pair<const K, V>, MapKeyOfT> _rbt;

set将红黑树的第二个参数改为const K好像也能解决问题,但库里并没有采用这种方法。

简易办法:使用const K禁止被修改。目前还没发现什么大问题,如果发现问题请告知
set成员的定义:

	private:
		RBTree<K,const K, SetKeyOfT> _rbt;

第二种就是采用库里的方法。
set无论普通还是const迭代器都使用红黑树的const迭代器

public:
		//库里的方法:
		typedef typename RBTree<K, K, SetKeyOfT>::Const_Iterator iterator;
		typedef typename RBTree<K, K, SetKeyOfT>::Const_Iterator const_iterator;

但是这样就会出现类型转化问题
类型转化
原因如下:

这里的迭代器不是原生指针的迭代器,而是通过封装而来的,借助模板实现了const和非const版的迭代器;也就导致普通的无法转为const迭代器;通过调试发现问题出现在获取迭代器上,由于set的普通迭代器也由const迭代器封装而来,set在获取普通迭代器时,最底层的红黑树返回的是普通迭代器,但set的普通迭代器实际为const迭代器,此时就发生了类型不匹配的问题。

对此,库中提供了如下解决方案:在红黑树迭代器中提供一个构造函数

set的Key问题
在红黑树迭代器中增加一个参数为普通迭代器类型的构造函数。该构造函数取参数中普通迭代器的节点重新构造一个迭代器(const版)。该方法绕过转化,采用构造,生成一个const版的迭代器,自然就没有类型转化的问题。

虽然这个构造函数增加在红黑树的迭代器中,但是map的迭代器不会有影响,这个构造函数只会匹配到set对应的状况。

	typedef RBTreeIterator<T, T&, T*> Iterator;	//声明类型:普通迭代器
	RBTreeIterator(const Iterator& it)//类型为普通迭代器,因为就是由于普通迭代器转化为const迭代器这一环出了                   问题,这里专门针对这一情况处理
		:_node(it._node)//此时需要的是一个const迭代器,(由于模板无法转化?)普通转const直接转化不行,那么我们就在返回时利用隐式类型转化
	{}

具体过程请看下图。
类型转化问题

insert的调整

在AVL树红黑树阶段实现的insert的返回值都为bool,在map和set中则是改为返回键值对pair。

以map为例:
insert介绍

函数声明:

pair<iterator,bool> insert (const value_type& val);

可以看到其返回值为pair,pair的第一个成员为一个迭代器指向新插入的元素,或者已存在的等效元素,第二个成员为bool,用来检测插入是否成功;也就是说insert成功的话会返回一个指向新插入元素的迭代器,其bool值为true的pair,如果insert失败则会返回一个指向容器中原有的K的迭代器,其bool值为false的pair。

map的[]运算符重载

map支持[]访问容器中Key值并返回Key对应的Value;set不支持[]重载,因为只有一个Key。
[]使用

也就是说[]的参数为pair中的第一个成员K,其使用说明如下:

如果K与容器中某个元素的键匹配,则该函数返回K值关联的Value引用。如果K与容器中任何元素的键不匹配,则该函数用该键插入一个新元素,并返回对其映射值的引用。这个新元素会有默认值

调用[]类似于下面的操作:

(*((this->insert(make_pair(k,mapped_type()))).first)).second

也就是说[]的实现是借助于insert实现的。而这样的话[]的用法就有两种:

  1. 插入
    • 如果K是map中不存在的,那么这就是一个插入操作;由于其返回值为第二个模板参数value的引用,所以可以直接修改。
    • []使用1
  2. 修改
    • 如果K是map中已有的值,那么insert将会返回已存在K的迭代器,[]最终返回一个K的引用,相当于支持修改操作。
    • []使用2

所以[]重载运算符的实现如下。

		//返回value的引用
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _rbt.Insert({ key,V() });
			return ret.first->second;//不理解返回second的结合operator->看
		}
  • 返回值类型为第二个参数的引用,所以支持直接修改。
  • 关于返回V&为何是返回it.first->second:it 为接受的是insert返回的pair对,其first为指向元素的迭代器,->调用了迭代器的operator->,此时返回的是存储KV的pair,此时的second为V

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

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

相关文章

DTH11温湿度传感器

DHT11 是一款温湿度复合传感器&#xff0c;常用于单片机系统中进行环境温湿度的测量。以下是对 DHT11 温湿度传感器的详细讲解&#xff1a; 一、传感器概述 DHT11 数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。它应用专用的数字模块采集技术和温湿度传感…

如何在Unity WebGL上实现一套全流程简易的TextureStreaming方案

项目介绍 《云境》是一款使用Unity引擎开发的WebGL产品&#xff0c;有展厅&#xff0c;剧本&#xff0c;Avatar换装&#xff0c;画展&#xff0c;语音聊天等功能&#xff0c;运行在微信小程序和PC&#xff0c;移动端网页&#xff0c;即开即用。 当前问题和现状 当前项目…

Qt-QTableWidget多元素控件(37)

目录 描述 QTableWidget 方法 QTableWidgetItem 信号 QTableWidgetItem 方法 使用 图形化界面操作 代码操作 描述 这是一个表格控件&#xff0c;表格中的每一个单元格&#xff0c;都是一个 QTableWidgetItem 对象 QTableWidget 方法 item(int row,int column)根据⾏数…

[半导体检测-7]:半导体检测技术:无图案晶圆检测与图案晶圆检测

前言&#xff1a; 半导体检测技术中&#xff0c;无图案晶圆检测与图案晶圆检测是两种重要的检测方式&#xff0c;它们在检测原理、应用场景及挑战等方面存在显著差异。以下是对这两种检测技术的详细分析&#xff1a; 一、无图案晶圆检测 1. 检测原理 无图案晶圆检测主要关注…

DRF实操学习——收货地址的设计

DRF实操学习——收货地址的设计 1.行政区划表的设计2. 行政区划表接口演示1.返回所有的省份2. 查询指定上级行政区划的所有子区划&#xff0c;以及展示自身区划 3.行政区划表接口重写补充&#xff1a;前端请求逻辑4. 优化5.收货地址的设计6. 收货地址表接口重写7.优化1. 优化返…

Android 12系统源码_输入系统(三)输入事件的加工和分发

前言 上一篇文章我们具体分析了InputManagerService的构造方法和start方法&#xff0c;知道IMS的start方法经过层层调用&#xff0c;最终会触发Navite层InputDispatcher的start方法和InputReader的start方法。InputDispatcher的start方法会启动一个名为InputDispatcher的线程&…

混拨动态IP代理的优势是什么

在当今互联网时代&#xff0c;隐私保护和网络安全成为了人们关注的焦点。无论是个人用户还是企业&#xff0c;都希望能够在网络上自由、安全地进行各种活动。混拨动态IP代理作为一种新兴的技术手段&#xff0c;正逐渐受到大家的青睐。那么&#xff0c;混拨动态IP代理到底有哪些…

【工具】JDK版本不好管理,SDKMAN来帮你

前言 &#x1f34a;缘由 SDKMAN真是好&#xff0c;JDK切换没烦恼 &#x1f423; 闪亮主角 大家好&#xff0c;我是JavaDog程序狗 今天跟大家能分享一个JDK版本管理工具SDKMAN 当你同时使用JDK 1.8的和JDK 17并行维护两个项目时。每次在两个项目之间并行开发&#xff0c;切…

进阶数据库系列(十三):PostgreSQL 分区分表

概述 在组件开发迭代的过程中&#xff0c;随着使用时间的增加&#xff0c;数据库中的数据量也不断增加&#xff0c;因此数据库查询越来越慢。 通常加速数据库的方法很多&#xff0c;如添加特定的索引&#xff0c;将日志目录换到单独的磁盘分区&#xff0c;调整数据库引擎的参…

2.4卷积3

2.4卷积3 文章学习自https://zhuanlan.zhihu.com/p/41609577&#xff0c;详细细节请读原文。 狄拉克 δ \delta δ 函数&#xff1a; δ ( x ) { ∞ , x 0 0 , x ≠ 0 \delta (x){\begin{cases} \infty ,& x0\\ 0,& x\neq 0\end{cases}} δ(x){∞,0,​x0x0​ 并…

小柴冲刺软考中级嵌入式系统设计师系列二、嵌入式系统硬件基础知识(2)嵌入式微处理器基础

目录 冯诺依曼结构 哈佛结构 一、嵌入式微处理器的结构和类型 1、8位、16位、32位处理器的体系结构特点 2、DSP处理器的体系结构特点 3、多核处理器的体系结构特点 二、嵌入式微处理器的异常与中断 1、异常 2、中断 flechazohttps://www.zhihu.com/people/jiu_sheng …

54 循环神经网络RNN_by《李沐:动手学深度学习v2》pytorch版

系列文章目录 文章目录 系列文章目录循环神经网络使用循环神经网络的语言模型困惑度&#xff08;perplexity&#xff09;梯度剪裁 循环神经网络 使用循环神经网络的语言模型 输入“你”&#xff0c;更新隐变量&#xff0c;输出“好”。 困惑度&#xff08;perplexity&#xff…

【递归】8. leetcode 671 二叉树中第二小的节点

题目描述 题目链接&#xff1a;二叉树中第二小的节点 2 解答思路 注意这句话&#xff1a;该节点的值等于两个子节点中较小的一个 二叉树的根节点的值是整棵树中最小的值 本道题所要求的是二叉树中第二小的节点。因为根节点是最小的节点&#xff0c;那么我们只需要找到第一…

HT5169内置BOOST升压的11W I2S输入D类音频功放

1 特性 ● 电源供电 升压输入VBAT:2.5V-5.5V; 升压输出PVDD可调&#xff0c;最高7.5V DVDD/AVDD分辨率:3.3V ● 音频性能 9.0W (VBAT3.7V, PVDD 7.5V, RL3Ω.THDN10%) 11.0W(VBAT3.7V, PVDD 7.5V, RL2Ω.THDN10% 5.5W (VBAT3.7V, PVDD 6.5V, RL4Ω.THDN10%) ● 灵活的…

红米k60至尊版工程固件 MTK芯片 资源预览 刷写说明 与nv损坏修复去除电阻图示

红米k60至尊版机型代码为:corot。 搭载了联发科天玑9200+处理器。此固件mtk引导为MT6985。博文将简单说明此固件的一些特点与刷写注意事项。对于NV损坏的机型。展示修改校验电阻的图示。方便改写参数等 通过博文了解 1💝💝💝-----此机型工程固件的资源刷写注意事项 2…

css 中 ~ 符号、text-indent、ellipsis、ellipsis-2、text-overflow: ellipsis的使用

1、~的使用直接看代码 <script setup> </script><template><div class"container"><p><a href"javascript:;">纪检委</a><a href"javascript:;">中介为</a><a href"javascript:…

曲线图异常波形检测系统源码分享

曲线图异常波形检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Comput…

【cache】浅析四种常用的缓存淘汰算法 FIFO/LRU/LFU/W-TinyLFU

本文浅析淘汰策略与工作中结合使用、选取&#xff0c;并非针对算法本身如何实现的 文章目录 FIFOLFULRUW-TinyLFU实践与优化监控与调整 FIFO first input first output &#xff0c; 先进先出&#xff0c;即最早存入的元素最先取出&#xff0c; 典型数据结构代表&#xff1a;…

SpringCloud-Netflix第一代微服务快速入门

1.springCloud常用组件 Netflix Eureka 当我们的微服务过多的时候&#xff0c;管理服务的通信地址是一个非常麻烦的事情&#xff0c;Eureka就是用来管理微服务的通信地址清单的&#xff0c;有了Eureka之后我们通过服务的名字就能实现服务的调用。 Netflix Ribbon\Feign : 客…

Python精选200Tips:171-175

深度学习实战项目 P171--CIFAR10数据集图像分类(Image Classification)P172--MS COCO数据集物体检测(Object Detection)P173-- MNIST手写数字数据集DCGAN生成P174--基于EasyOCR的字符识别P175--基于Air Quality数据集的变分自编码器(Variational autoEncoder&#xff0c;VAE) 运…