C++ | 红黑树以及map与set的封装

目录

前言

一、红黑树

1、红黑树的基本概念

2、红黑树相关特性

3、红黑树结点的定义

4、红黑树的查找

5、红黑树的插入

6、二叉树的拷贝构造与析构

7、红黑树的检测

8、红黑树总结

二、map与set的封装

1、红黑树的结点

2、红黑树迭代器

3、set的封装

4、map的封装

三、源码仓库 


前言

        前面我们讲过普通的二叉搜索树以及特殊的二叉搜索树 --- AVL树,今天,我们也要实现一种特殊的二叉搜索树,该树被广泛使用在STL库中的map与set的封装,这就是我们的 红黑树 ,怎么样,听名字就知道很霸气吧;本文就带着大家实现一个简单版本的红黑树以及用这个版本的红黑树对map与set进行封装;

一、红黑树

1、红黑树的基本概念

        红黑树是二叉搜索树的一种,它与普通二叉搜索树不同之处是它有一些规则对其进行了限制,使其最长路径结点个数不会超出最短路径结点个数的两倍,下面我们就来看看红黑树相关特性;

2、红黑树相关特性

1、每个结点不是红色就是黑色

2、根节点是黑色的

3、如果一个节点是红色的,则它的两个孩子结点是黑色的(不会有连续的红色结点)

4、对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 、包含相同数目的黑色结点(每条路径上的黑色结点数量相同)

5、每个叶子结点都是黑色的(此处的叶子结点指的是空结点

 思考:为什么满足以上特性,最长路径的结点个数不会超出最短路径节点个数的两倍呢?

        根据上述5个条件,假设每条路径的黑色结点个数为3,那么最短路径应就是全是黑色结点,最长是黑红将间的路径结点;

        既然最长路径与最短路径我们都可以得到,根据上述,我们不难求证出最长路径的结点个数不会超出最短路径节点个数的两倍;

3、红黑树结点的定义

        我们首先枚举出颜色,然后我们的红黑树结点用三叉链的方式进行连接,方便后续旋转调整;这里,其实有一个问题,我们构造的结点默认是红色还是黑色呢?

        其实这个问题本质就是,对于新插入的结点,你更愿意违反规则3来进行调整,还是愿意违法规则4来进行后续的调整呢?答案是肯定的,当然愿意违反规则3,如果出现了连续的红色结点,我们可能仅仅只需要调整该红色结点所在子树;而违反规则4,我们需要调整整棵树,因为一颗树的黑色节点增多了,其他该所在路径也会增多,调整更麻烦;因此构造函数我们更愿意默认将结点置为红色;

	enum Color
	{
		RED,
		BLACK
	};
	template<class K, class V>
	struct RBTreeNode
	{
		RBTreeNode(const std::pair<K, V> kv)
			:_left(nullptr)
			, _right(nullptr)
			, _parent(nullptr)
			, _color(RED)
			, _kv(kv)
		{}

		RBTreeNode<K, V>* _left;
		RBTreeNode<K, V>* _right;
		RBTreeNode<K, V>* _parent;
		Color _color;
		std::pair<K, V> _kv;
	};

4、红黑树的查找

        红黑树的查找方式与普通二叉搜索树并无二异,这里就不做过多介绍了;此处顺便将红黑树的框架写出来了;

	template<class K, class V>
	class RBTree
	{
	public:
		typedef RBTreeNode<K, V> Node;
		// 查找
		bool find(const K& key)
		{
			if (_root == nullptr)
			{
				return false;
			}
			Node* cur = _root;
			while (cur)
			{
				if (key < cur->_kv.first)
				{
					cur = cur->_left;
				}
				else if (key > cur->_kv.first)
				{
					cur->_right;
				}
				else
				{
					// 找到了
					return true;
				}
			}
			return false;
		}
	private:
		Node* _root = nullptr;
	};

5、红黑树的插入

        红黑树的插入操作才是关键,也是本文讲解红黑树的核心;红黑树插入数据与以前的二叉搜索树插入数据的代码相同,主要是后面的调整操作,调整主要分为三种情况,我们先将插入逻辑的框架搭建起来,框架如下所示;

		// 插入
		bool insert(const std::pair<K, V> kv)
		{
			// 首次插入
			if (_root == nullptr)
			{
				_root = new Node(kv);
				_root->_color = BLACK;
				return true;
			}
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (kv.first < cur->_kv.first)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (kv.first > cur->_kv.first)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					//有对应的key,返回false
					return false;
				}
			}
			// 找到插入位置了,插入数据
			cur = new Node(kv);
			if (kv.first < parent->_kv.first)
			{
				parent->_left = cur;
			}
			else
			{
				parent->_right = cur;
			}
			cur->_parent = parent;

            // 以下为调整代码......暂未填充
            // .......


            return true;
        }

        接下来我带着大家逐步分析以下的三种情况;情况一是cur为红,parent为红,grandfather为黑,uncle为红,以下分别简称为c、p、g、u;

注意:我们举例三种情况均为parent为grandfather的左孩子,右孩子的情况与左孩子类似

        以下为具象图,这种情况可以一只沿着祖父往上调整,可能会调整几次,如下就调整了两次;

        接着就是情况二,情况二看起来分为两种,实际就是一种情况,以下分别情况二的两种具象图;

注意:情况二调整完以后直接退出循环,无需继续往上调整;

        这里的右单旋就是我们前面AVL树的右单旋,代码也一模一样;如果parent是grandfather的左边就是左单旋,所以上面说我们进分析一边即可;

        情况三实则是由情况一变成的,我们可以把情况三变成情况二,进行处理,因为cur的位置不同,而必须进行双旋;

注意:情况三调整完以后直接退出循环,无需继续往上调整;

        上述就是三种调整方式,我们根据上图可写出如下代码;

		// 插入
		bool insert(const std::pair<K, V> kv)
		{
			// 首次插入
			if (_root == nullptr)
			{
				_root = new Node(kv);
				_root->_color = BLACK;
				return true;
			}
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (kv.first < cur->_kv.first)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (kv.first > cur->_kv.first)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					//有对应的key,返回false
					return false;
				}
			}
			// 找到插入位置了,插入数据
			cur = new Node(kv);
			if (kv.first < parent->_kv.first)
			{
				parent->_left = cur;
			}
			else
			{
				parent->_right = cur;
			}
			cur->_parent = parent;
			// 调整
			while (parent && parent->_color == RED)
			{
				// grandfather必然存在,不用判空,因为parent为红,不可能为根
				Node* grandfather = parent->_parent;
				if (grandfather->_left == parent)
				{
					// 情况一 uncle 存在且为红
					Node* uncle = grandfather->_right;
					if (uncle && uncle->_color == RED)
					{
						// 直接变色
						grandfather->_color = RED;
						parent->_color = BLACK;
						uncle->_color = BLACK;
						// 继续向上调整
						cur = grandfather;
						parent = cur->_parent;
					}
					else // 情况二 + 情况三
					{
						// 情况二
						if (parent->_left == cur)
						{
							rotateR(grandfather);
							parent->_color = BLACK;
							grandfather->_color = RED;
						}
						else // 情况三
						{
							rotateL(parent);
							rotateR(grandfather);
							cur->_color = BLACK;
							grandfather->_color = RED;
						}
						break;
					}
				}
				else // grandfather->right == parent
				{
					Node* uncle = grandfather->_left;
					// 情况一
					if (uncle && uncle->_color == RED)
					{
						// 变色
						parent->_color = BLACK;
						uncle->_color = BLACK;
						grandfather->_color = RED;
						// 继续像上调整
						cur = grandfather;
						parent = cur->_parent;
					}
					else // 情况二 + 情况三
					{
						// 情况二
						if (parent->_right == cur)
						{
							rotateL(grandfather);
							grandfather->_color = RED;
							parent->_color = BLACK;
						}
						else
						{
							rotateR(parent);
							rotateL(grandfather);
							cur->_color = BLACK;
							grandfather->_color = RED;
						}
						break;
					}
				}
				// 上面可能会将root改为红色
				_root->_color = BLACK;
			}
			return true;
		}

        补充一下我们的旋转代码; 

		void rotateL(Node* parent)
		{
			Node* subR = parent->_right;
			Node* subRL = subR->_left;
			// subRL挂在parent的右边
			parent->_right = subRL;
			if (subRL)
				subRL->_parent = parent;

			// 提前保存parent的父亲结点
			Node* pparent = parent->_parent;

			// parent挂到subR的左边
			subR->_left = parent;
			parent->_parent = subR;

			// 将subR与上层链接
			if (_root == parent)
			{
				_root = subR;
				subR->_parent = nullptr;
			}
			else
			{
				if (pparent->_left == parent)
				{
					pparent->_left = subR;
					subR->_parent = pparent;
				}
				else
				{
					pparent->_right = subR;
					subR->_parent = pparent;
				}
			}
		}

		void rotateR(Node* parent)
		{
			Node* subL = parent->_left;
			Node* subLR = subL->_right;
			// 将subLR挂到parent的左边
			parent->_left = subLR;
			if (subLR)
				subLR->_parent = parent;

			// 提前保存parent的父亲结点
			Node* pparent = parent->_parent;

			// 将parent挂到subL的右边
			subL->_right = parent;
			parent->_parent = subL;

			// 将这颗子树的根结点与上面结点链接
			if (parent == _root)
			{
				_root = subL;
				subL->_parent = nullptr;
			}
			else
			{
				if (pparent->_left == parent)
				{
					pparent->_left = subL;
					subL->_parent = pparent;
				}
				else
				{
					pparent->_right = subL;
					subL->_parent = pparent;
				}
			}
		}

 

6、二叉树的拷贝构造与析构

        二叉树的拷贝构造我们通过先序的方式进行拷贝构造;

		RBTree(const RBTree& rbt)
		{
			if (this != &rbt)
			{
				_root = _construction(rbt._root, nullptr);
			}
		}
		Node* _construction(Node* root, Node* parent)
		{
			if (root == nullptr)
				return nullptr;

			Node* newroot = new Node(root->_val);
			newroot->_color = root->_color;
			newroot->_parent = parent;
			parent = newroot;
			newroot->_left = _construction(root->_left, parent);
			newroot->_right = _construction(root->_right, parent);
			return newroot;
		}

        二叉树的析构我们则需用类似后序遍历的方式进行逐个结点析构;

		~RBTree()
		{
			destruction(_root);
		}
		void destruction(Node* root)
		{
			if (root == nullptr)
				return;
			destruction(root->_left);
			destruction(root->_right);
			delete root;
		}

7、红黑树的检测

        前面代码我们完成了对红黑树的各种操作,现在我们来检测一下我们的红黑树是否为正确的红黑树;具体代码如下;

		void inorder()
		{
			_inorder(_root);
			std::cout << std::endl;
		}

		bool is_balance()
		{
			if (_root->_color == RED)
				return false;
			int maxBlackNode = -1;
			return _is_balance(_root, 0, maxBlackNode);
		}

		int height()
		{
			return _height(_root);
		}

		int _height(Node* root)
		{
			if (root == nullptr)
			{
				return 0;
			}
			int left_height = _height(root->_left);
			int right_height = _height(root->_right);
			return left_height > right_height ? left_height + 1 : right_height + 1;
		}

		bool _is_balance(Node* root, int curBlackNode, int& maxBlackNode)
		{
			if (root == nullptr)
			{
				// 首次进入更新最大黑节点数量
				if (maxBlackNode == -1)
				{
					maxBlackNode = curBlackNode;
					//cout << "黑色结点个数:" << maxBlackNode << endl;
				}

				if (curBlackNode != maxBlackNode)
				{
					cout << "路径上的黑色结点数量不一致" << endl;
					return false;
				}
				//cout << curBlackNode << endl;
				return true;
			}

			if (root->_color == BLACK)
			{
				curBlackNode++;
			}

			if (root->_color == RED && root->_parent->_color == RED)
			{
				//cout << root->_parent->_kv.first << "颜色异常" << endl;
				return false;
			}
			return _is_balance(root->_left, curBlackNode, maxBlackNode) 
                && _is_balance(root->_right, curBlackNode, maxBlackNode);
		}

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

        我们可以通过如上的is_balance函数检测红黑树是否有误,如果还不放心我们可以通过如下代码进行测试;同时也对比了与AVL树的差别;

void test_RBTree2()
{
	srand((unsigned int)time(nullptr));
	const int N = 1000000;
	MySpace::RBTree<int, int> rb1;
	MySpace::AVLTree<int, int> avl1;
	for (int i = 0; i < N; i++)
	{
		int num = rand() + i;
		rb1.insert(make_pair(num, num));
		avl1.insert(make_pair(num, num));
	}
	cout << "RB max height :" << rb1.height() << endl;
	cout << "RB is balance :" << rb1.is_balance() << endl;

	cout << "AVL max height :" << avl1.height() << endl;
	cout << "AVL is balance :" << avl1.is_balance() << endl;
}

8、红黑树总结

        有人发现我们红黑树并没有AVL树那样完全平衡,那么红黑树的意义是什么呢?实际上,我们的map与set都是使用红黑树来进行封装的,那么有的小伙伴就疑惑了,那么为什么不用AVL树进行封装呢?AVL树不是更加平衡,搜索效率更高么?实际上的确如此,搜索效率,有时候红黑树确实不如AVL树,可是我们不能但从查找来评判一棵树的好坏,我们的红黑树总体效率实际上要比我们的AVL树高的,因为我们的红黑树不是绝对平衡的,因此我们旋转的次数并不会有AVL树那么频繁,总体效率是比AVL树高的;

二、map与set的封装

        实际上,我们的map与set仅仅只是套了一层红黑树的外壳接下来,我们稍稍修改上述的红黑树,对map与set完成封装;

1、红黑树的结点

        这里的模板参数变成了T,当我们的set来的时候,传入的是Key,当我们的map来的时候传入的是pair<Key, Value>;

	template<class T>
	struct RBTreeNode
	{
		RBTreeNode(const T& val)
			:_left(nullptr)
			, _right(nullptr)
			, _parent(nullptr)
			, _color(RED)
			, _val(val)
		{}

		RBTreeNode<T>* _left;
		RBTreeNode<T>* _right;
		RBTreeNode<T>* _parent;
		Color _color;
		T _val;
	};

2、红黑树迭代器

        我们要实现map与set的迭代器,就得先实现红黑树的迭代器;

	template<class T, class Ref, class Ptr>
	struct RBTree_iterator
	{
		typedef RBTreeNode<T> Node;
		typedef RBTree_iterator<T, Ref, Ptr> Self;
		typedef RBTree_iterator<T, T&, T*> iterator;
		RBTree_iterator(Node* node)
			:_node(node)
		{}
		// 1、如果该类是普通迭代器,则作用为拷贝构造
		// 2、如果该类是const迭代器,则作用为用普通迭代器取构造const迭代器
		RBTree_iterator(const iterator& it)
			:_node(it._node)
		{}

		Ref operator*()
		{
			return _node->_val;
		}

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

		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}

		Self& operator++()
		{
			Node* cur = _node;
			if(cur)
			{
				if (cur->_right != nullptr)
				{
					// 右子树的最左节点
					cur = cur->_right;
					while (cur->_left)
					{
						cur = cur->_left;
					}
					_node = cur;
				}
				else
				{
					// 找当前结点是父亲左结点的父亲
					Node* parent = cur->_parent;
					while (parent && parent->_right == cur)
					{
						cur = parent;
						parent = parent->_parent;
					}
					_node = parent;
				}
			}
			return *this;
		}
		Self& operator--()
		{
			Node* cur = _node;
			if (cur)
			{
				if (cur->_left != nullptr)
				{
					// 左子树的最右结点
					Node* subR = cur->_left;
					while (subR->_right)
					{
						subR = subR->_right;
					}
					_node = subR;
				}
				else
				{
					// 找当前结点是父亲的右节点的父亲
					Node* parent = cur->_parent;
					while (parent && parent->_left)
					{
						parent = parent->_parent;
					}
					_node = parent;
				}
			}
			return *this;
		}

		Node* _node;
	};

3、set的封装

        接下来我们仅仅只需要将红黑树封装成set即可,直接套一层外壳;

	template<class Key, class Value>
	struct SetKeyOfT
	{
		const Key& operator()(const Value& val)
		{
			return val;
		}
	};

	template<class Key>
	class set
	{
	public:
		typedef Key key_type;
		typedef Key value_type;
		typedef RBTree<key_type, value_type, SetKeyOfT<key_type, value_type>> Tree;

		typedef typename RBTree<key_type, value_type, SetKeyOfT<key_type, value_type>>::const_iterator iterator;
		typedef typename RBTree<key_type, value_type, SetKeyOfT<key_type, value_type>>::const_iterator const_iterator;

		pair<iterator, bool> insert(const value_type& val)
		{
			return _t.insert(val);
		}

		iterator begin()
		{
			return _t.begin();
		}
		iterator end()
		{
			return _t.end();
		}

		const_iterator begin() const
		{
			return _t.begin();
		}
		const_iterator end() const
		{
			return _t.end();
		}

	private:
		Tree _t;
	};

        解释一下这里的KeyOfT,这里只是一个仿函数,我们仅仅想通过这个取出Key值,因为红黑树的T也不确定是Key还是KeyValue模型;

注意:由于set的结点中的Key在树中不能被修改,因此这里set的普通迭代器和const迭代器都是红黑树的const迭代器;

4、map的封装

        map的封装同样如此,我们仅仅只是套了一层红黑树的外壳;

	template<class Key, class T>
	struct MapKeyOfT
	{
		const Key& operator()(const T& val)
		{
			return val.first;
		}
	};

	template<class Key, class Value>
	class map
	{
	public:
		typedef Key key_type;
		typedef std::pair<Key, Value> value_type;
		typedef RBTree<const Key, value_type, MapKeyOfT<key_type, value_type>> Tree;

		typedef typename RBTree<const key_type, value_type, MapKeyOfT<key_type, value_type>>::iterator iterator;
		typedef typename RBTree<const key_type, value_type, MapKeyOfT<key_type, value_type>>::const_iterator const_iterator;
		
		iterator begin()
		{
			return _t.begin();
		}
		iterator end()
		{
			return _t.end();
		}

		const_iterator begin() const
		{
			return _t.begin();
		}
		const_iterator end() const
		{
			return _t.end();
		}

		std::pair<iterator, bool> insert(const value_type& val)
		{
			return _t.insert(val);
		}


	private:
		Tree _t;
	};

三、源码仓库 

        关于红黑树、map与set的所有源码小编都放进代码仓库中供大家参考;以下为源码仓库;

源码仓库

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

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

相关文章

一、前端高德地图注册、项目中引入、渲染标记(Marker)and覆盖物(Circle)

首先说明一下&#xff0c;下面的流程只是个人摸索and看文档梳理出来的&#xff0c;并不作为完全正确的流程。 首先&#xff0c;注册 高德开放平台 没有注册的可以点击右上角点击注册&#xff1b; 我们点个人的就ok&#xff1b; 信息完善之后我们到控制台&#xff0c;点击 应…

使用Feign出现空指针异常

说明&#xff1a;本文记录一次偶然出现的空指针异常&#xff0c;在微服务架构中&#xff0c;一个服务在调用另一个服务时&#xff0c;出现了空指针异常。 业务描述&#xff1a;在做订单超时功能时&#xff0c;大家都知道&#xff0c;可以使用RabbitMQ延迟队列&#xff0c;下单…

二、SQL-6.DCL-2).权限控制

*是数据库和表的通配符&#xff0c;出现在数据库位置上表示所有数据库&#xff0c;出现在表名位置上&#xff0c;表示所有表 %是主机名的通配符&#xff0c;表示所有主机。 e.g.所有数据库&#xff08;*&#xff09;的所有表&#xff08;*&#xff09;的所有权限&#xff08;a…

关于Docker的基本概念和使用

关于Docker的基本概念和使用 一、Docker 概述1、Dcker的概念2、容器的优势3、Docker与虚拟机的区别4、容器在内核中支持2种重要技术5、Docker核心概念 二、安装 Docker1、安装依赖包2、设置阿里云镜像源3、安装 Docker-CE并设置为开机自动启动3、查看 docker 版本信息4、docker…

程序员,必须要知道的热门开源项目!

&#x1f3c6; 文章目标&#xff1a;了解热门开源项目 &#x1f340; 入门篇&#xff1a;程序员,必须要知道的热门开源项目! ✅ 创作者&#xff1a;熊猫Jay ✨ 个人公众号: 熊猫Jay字节之旅 (文末有链接) &#x1f341; 展望&#xff1a;若本篇讲解内容帮助到您&#xff0c;请帮…

Java并发编程面试题

Author 郑金维 并发编程面试题1 一、原子性高频问题&#xff1a; 1.1 Java中如何实现线程安全? 多线程操作共享数据出现的问题。 锁&#xff1a; 悲观锁&#xff1a;synchronized&#xff0c;lock乐观锁&#xff1a;CAS 可以根据业务情况&#xff0c;选择ThreadLocal&am…

PHP 3des加解密新旧方法可对接加密

一、旧3des加解密方法 <?php class Encrypt_3DES {//加密秘钥&#xff0c;private $_key;private $_iv;public function __construct($key, $iv){$this->_key $key;$this->_iv $iv;}/*** 对字符串进行3DES加密* param string 要加密的字符串* return mixed 加密成…

配置IPv4 over IPv6隧道示例

IPv4 over IPv6隧道&#xff1a; 在IPv4 Internet向IPv6 Internet过渡后期&#xff0c;IPv6网络被大量部署后&#xff0c;而IPv4网络只是散布在世界各地的一些孤岛。利用隧道技术可以在IPv6网络上创建隧道&#xff0c;从而实现IPv4孤岛的互联&#xff0c;IPv4孤岛能通过IPv6公…

基于STM32CubeMX和keil采用STM32F407的基本定时器中断实现LED闪烁

文章目录 前言1. 电路原理图理解2. 基本定时器2.1 STM32定时器中断的流程&#xff1a;2.2 部分参数详解2.2.1 时钟源2.2.2 预分频系数2.2.3 自动重装载值 3. STM32CubeMX参数配置3.1GPIO配置3.2 时钟配置3.2 配置定时器相关参数3.3 Debug配置3.4 中断配置3.5 代码生成 4. keil代…

关于Java中的Lambda变量捕获

博主简介&#xff1a;想进大厂的打工人博主主页&#xff1a;xyk:所属专栏: JavaEE进阶 目录 一、Lambda表达式语法 二、Lambda中变量捕获 一、Lambda表达式语法 基本语法: (parameters) -> expression 或 (parameters) ->{ statements; } Lambda表达式由三部分组成&a…

小米新专利曝光:解决升降摄像头痛点,隐藏式摄像头指日可待

根据国家知识产权局官方网站的最新消息&#xff0c;小米移动软件公司的“摄像头隐藏式电子设备及隐藏式摄像头”的专利申请在今天获得了授权。 这款电子设备的主要组成部分包括壳体、摄像模组和可伸缩的反射组件。壳体上设有一个开口&#xff0c;可以让反射组件向外伸出。反射组…

美容店预约小程序制作教程详解

现在&#xff0c;制作一个专属于美容店的预约小程序不再需要编程经验&#xff0c;通过乔拓云网提供的后台管理系统&#xff0c;你可以轻松地完成整个制作过程。下面&#xff0c;我将为你详细介绍如何DIY一个美容店预约小程序。 首先&#xff0c;登录乔拓云网的后台管理系统&…

redis到底几个线程?

通常我们说redis是单线程指的是从接收客户端请求->解析请求->读写->响应客户端这整个过程是由一个线程来完成的。这并不意味着redis在任何场景、任何版本下都只有一个线程 为何用单线程处理数据读写&#xff1f; 内存数据储存已经很快了 redis相比于mysql等数据库是…

Qt完成文本转换为语音播报与保存(系统内置语音引擎)(二)

一、前言 随着人工智能技术的不断发展,语音技术也逐渐成为人们关注的焦点之一。语音技术在很多领域都有着广泛的应用,例如智能家居、智能客服、语音识别等等。其中,语音转文字技术是语音技术中的一个重要分支,它可以将语音转换成可编辑的文本,为人们的生活和工作带来了更…

中缀表达式转后缀表达式,使用逆波兰计算。可以计算小数

1、使用方法 传递一个分开保存符号与数字的List即可&#xff1a;List SumNumber; 获取参数的构造方法如下&#xff1a; public ReversePolish(List<String> sumNumber) {SumNumber sumNumber;}要求的List保存数据的方式如下&#xff1a; 例如&#xff1a;123 然后使用…

MFC表格控件CListCtrl的改造及用法

1、目的 简单描述MFC的表格控件使用方法。Qt适用习惯了以后MFC用的比较别扭&#xff0c;因此记录一下以备后续复制代码使用。由于MFC原生的CListCtrl比较局限&#xff0c;比如无法改变表格的背景色、文字颜色等设定&#xff0c;因此先对CListCtrl类进行重写&#xff0c;以便满足…

Jenkins 拉取 GitHub 私有仓库失败问题

添加仓库的时候提示 stderr: fatal: Cannot prompt because user interactivity has been disabled. 把在 GitHub账户设置中生成的个人访问令牌填到地址里

第54步 深度学习图像识别:MLP-Mixer建模(Pytorch)

基于WIN10的64位系统演示 一、写在前面 &#xff08;1&#xff09;MLP-Mixer MLP-Mixer&#xff08;Multilayer Perceptron Mixer&#xff09;是Google在2021年提出的一种新型的视觉模型结构。它的主要特点是完全使用多层感知机&#xff08;MLP&#xff09;来处理图像&#…

Docker Compose(九)

一、背景&#xff1a; 对于现代应用来说&#xff0c;大多数都是通过很多的微服务互相协同组成一个完整的应用。例如&#xff0c;订单管理、用户管理、品类管理、缓存服务、数据库服务等&#xff0c;他们构成了一个电商平台的应用。而部署和管理大量的服务容器是一件非常繁琐的事…

Sentinel Dashboard集成Nacos

1.前言 当项目上Sentinel Dashboard做流量监控的时候&#xff0c;我们可以通过Sentinel控制台修改限流配置&#xff0c;但当我们使用Nacos作为配置中心动态配置流控规则的时候&#xff0c;问题就来了。 首先我们要明白&#xff0c;Sentinel Dashboard的配置是从机器的内存中加…
最新文章