【C++】二叉树进阶(二叉搜索树)

在这里插入图片描述

目录

  • 一、内容安排说明
  • 二、 二叉搜索树
    • 2.1 二叉搜索树概念
    • 2.2 二叉搜索树操作
      • 2.2.1 二叉搜索树的查找
      • 2.2.2 二叉搜索树的插入
      • 2.2.3 二叉搜索树的删除
    • 2.3 二叉搜索树的代码实现
      • 2.3.1 二叉搜索树的节点设置
      • 2.3.2 二叉搜索树类的框架
      • 2.3.3 二叉搜索树的查找函数
        • 2.3.3.1 非递归方式实现
        • 2.3.3.2 递归方式实现
      • 2.3.4 二叉搜索树的插入函数
        • 2.3.4.1 非递归方式实现
        • 2.3.4.2 递归方式实现
      • 2.3.5 二叉搜索树的中序遍历
      • 2.3.6 二叉搜索树的删除函数
        • 2.3.6.1 非递归实现
        • 2.3.6.2 递归实现
      • 2.3.7 二叉搜索树代码的整体实现
    • 2.4 二叉搜索树的应用
    • 2.5 二叉搜索树的性能分析
  • 三、二叉树进阶题目
    • 3.1 根据二叉树创建字符串[【OJ题目】](https://leetcode.cn/problems/construct-string-from-binary-tree/description/)
    • 3.2 二叉树的层序遍历Ⅰ[【OJ题目】](https://leetcode.cn/problems/binary-tree-level-order-traversal/description/)
    • 3.3 二叉树的层序遍历Ⅱ[【OJ题目】](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/description/)
    • 3.4 二叉树的最近公共祖先[【OJ题目】](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/)
    • 3.5 二叉搜索树与双向链表[【OJ题目】](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&&tqId=11179&rp=1&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking)
    • 3.6 从前序与中序遍历序列构造二叉树[【OJ题目】](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/)
    • 3.7 从中序与后序遍历序列构造二叉树[【OJ题目】](https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/description/)
    • 3.8 二叉树的前序遍历,非递归迭代实现[【OJ题目】](https://leetcode.cn/problems/binary-tree-preorder-traversal/)
    • 3.9 二叉树的中序遍历 ,非递归迭代实现[【OJ题目】](https://leetcode.cn/problems/binary-tree-inorder-traversal/description/)
    • 3.10 二叉树的后序遍历 ,非递归迭代实现 [【OJ题目】](https://leetcode.cn/problems/binary-tree-postorder-traversal/description/)
  • 结尾

一、内容安排说明

二叉树在前面C数据结构阶段已经讲过,本节取名二叉树进阶是因为:

  1. mapset特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
  2. 二叉搜索树的特性了解,有助于更好的理解map和set的特性
  3. 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘
  4. 有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。

因此本节借二叉树搜索树,对二叉树部分进行收尾总结。

大家可以看一下这篇文章进行简单的复习
【数据结构】非线性结构之树结构(含堆)


二、 二叉搜索树

2.1 二叉搜索树概念

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

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

在这里插入图片描述


2.2 二叉搜索树操作

在这里插入图片描述


2.2.1 二叉搜索树的查找

  1. 从根节点开始查找,若查找值比根节点小,继续向左子树寻找,若查找值比根节点打,继续向右子树寻找。
  2. 最多查找高度次,若查找到空,那么在这棵树中没有这个值。

2.2.2 二叉搜索树的插入

  1. 若树为空,创建一个节点,赋值给root。
  2. 若树不为空,按照二叉搜索树的性质查找插入位置,插入新节点
    在这里插入图片描述

2.2.3 二叉搜索树的删除

首先需要查找该节点是否存在,若不存在,则返回不删除,若存在,那么要删除该节点需要分为以下四种情况。

  1. 该节点为叶子节点
  2. 该节点有只有左孩子节点
  3. 该节点有只有右孩子节点
  4. 该节点左孩子节点、右孩子节点都有

看起来有待删除节点有4中情况,实际情况1可以与情况2或者3合并起来,因此真正的删除过程如下:

  • 情况2 :让该节点的父节点指向该节点的左孩子节点并删除该节点 —— 直接删除
    在这里插入图片描述

  • 情况3 :让该节点的父节点指向该节点的右孩子节点并删除该节点 —— 直接删除
    在这里插入图片描述

  • 情况4:找到该节点左树最右节点(最大节点)或者右树的最左节点(最小节点),交换该节点和找到的节点的关键码,再删除交换前找到的节点。
    在这里插入图片描述
    在这里插入图片描述


2.3 二叉搜索树的代码实现

2.3.1 二叉搜索树的节点设置

// 二叉搜索树的节点设置
template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;      // 关键码
	BSTreeNode(const K& key)
		:_key(key)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

2.3.2 二叉搜索树类的框架

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
	// ......
	// 函数的实现
	// ......
public:

	// 插入函数,非递归版本
	bool Insert(const K& key){};
	// 查找函数,非递归版本
	bool Find(const K& key){};
	// 删除函数,非递归版本
	bool Erase(const K& key)
	// 查找函数
	void InOrder(){};

	// 析构函数
	~BSTree()
	{
		Destory(_root);
	}

	// 构造函数
	// C++ 11新增使用方法
	// BSTree() = default;  
	BSTree(){}

	// 拷贝构造
	BSTree(const BSTree<K>& T)
	{
		_root = Copy(T._root);
	}
	
	// 由于实参传个形参会创建一个新的对象
	// 这个对象是我们想要的,而原来对象不需要
	// 当函数结束后形参会被释放,将两个对象的地址交换后
	// 形参的内容就改变成我们不需要的内容,操作系统会帮我们释放
	// 而this指向的内容是我们需要的,从而完成赋值
	// 赋值重载
	BSTree<K>& operator=(BSTree<K> T)
	{
		swap(_root, T._root);
		return *this;
	}
private:
	// 封装拷贝构造函数
	Node* Copy(Node* Troot)
	{
		if (Troot == nullptr)
			return nullptr;
			
		Node* root = new Node(Troot->_key);
		root->_left = Copy(Troot->_left);
		root->_right = Copy(Troot->_right);

		return root;
	}
	// 封装析构函数
	void Destory(Node*& root)
	{
		if(root == nullptr)
			return;
		Destory(root->_left);
		Destory(root->_right);
		delete root;
		root = nullptr;
	}
private:
	// 成员变量
	Node* _root = nullptr;
};


2.3.3 二叉搜索树的查找函数

  1. 从根节点开始查找,若查找值比根节点小,继续向左子树寻找,若查找值比根节点打,继续向右子树寻找。
  2. 最多查找高度次,若查找到空,那么在这棵树中没有这个值。
2.3.3.1 非递归方式实现
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
	// ......
	// 函数的实现
	// ......
public:
	// 查找函数,非递归版本
	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;
	}
private:
	// 成员变量
	Node* _root = nullptr;
};


2.3.3.2 递归方式实现
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
	// ......
	// 函数的实现
	// ......
public:
	// 查找 , 递归版本
	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}	
private:
	bool _FindR(Node* root, const K& key)
	{
		// 找不到
		if (root == nullptr)
			return false;
		// 当查找的值比节点的值小,递归子问题在左子树查找
		if (root->_key > key)
			return _FindR(root->_left, key);
		// 当查找的值比节点的值大,递归子问题在右子树查找
		else if (root->_key < key)
			return _FindR(root->_right, key);
		// 找到了
		else
			return true;
	}
	// 成员变量
	Node* _root = nullptr;
};


2.3.4 二叉搜索树的插入函数

  1. 若树为空,创建一个节点,赋值给root。
  2. 若树不为空,按照二叉搜索树的性质查找插入位置,插入新节点
2.3.4.1 非递归方式实现

以非递归方式实现,在寻找该节点需要放的位置时,需要一个变量记录他当前位置的父亲节点,这样找到位置时,更方便的链接这个新节点。

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	// 插入,非递归版本
	bool Insert(const K& key)
	{
		// 树为空则,该节点为根
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* cur = _root;
		// 记录当前节点的父亲节点
		Node* prev = nullptr;

		while (cur)
		{
			prev = cur;
			// 当当前节点的值大于要插入的值,那么向左找
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			// 当当前节点的值小于要插入的值,那么向右找
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			// 二叉搜索树不允许相同的值存在,当这个值已经存在过了,那么返回false,即插入失败
			else
			{
				return false;
			}
		}
		if (prev->_key > key)
		{
			prev->_left = new Node(key);
			return true;
		}
		else
		{
			prev->_right = new Node(key);
			return true;
		}
		return false;
	}
private:
	//成员变量
	Node* _root = nullptr;
}

2.3.4.2 递归方式实现

当使用递归方式实现时,有个很巧妙的函数头设置 bool _InsertR(Node*& root, const K& key) , 这里面的Node*& root使得我们不需要记录当前位置的父亲节点,因为记录父亲节点的目的就是为了方便链接新节点,而这里的&就使得当前节点就是它父亲节点的左孩子节点/右孩子节点,改变这个节点就是改变它父亲节点的左孩子节点/右孩子节点。

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	// 插入,递归版本
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
private:
	bool _InsertR(Node*& root, const K& key)
	{
		// 当树为空时,新插入的节点就是根节点
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		if (root->_key > key)
			return _InsertR(root->_left, key);
		else if (root->_key < key)
			return _InsertR(root->_right, key);
		// 出现相同的数字,返回false
		else
			return false;
	}
	//成员变量
	Node* _root = nullptr;
}

2.3.5 二叉搜索树的中序遍历

二叉树的中序遍历是:先递归遍历左子树,再访问根节点,最后递归遍历右子树。

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	
private:
	// 封装中序遍历
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << ' ';
		_InOrder(root->_right);
	}
private:
	// 成员变量
	Node* _root = nullptr;
}

2.3.6 二叉搜索树的删除函数

2.3.6.1 非递归实现

当我们需要删除一个节点时找到删除节点,需要记录当前节点的父亲节点,防止删除的当前节点时,防止当前的节点的父亲节点中的指针变为野指针。

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	// 删除,非递归版本
	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* prev = nullptr;

		// 找到需要删除的节点
		while (cur)
		{
			if (cur->_key > key)
			{
				prev = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else
			{
				break;
			}
		}

		// 当当前根节点的左树为空时,那么链接右边
		if (cur->_left == nullptr)
		{
			// 当需要删除的节点是根节点,而根节点又没有父亲
			// 那么直接让右孩子节点成为根节点
			if (cur == _root)
			{
				_root = cur->_left;
				delete cur;
				return true;
			}
			else
			{
				if (prev->_left == cur)
					prev->_left = cur->_right;
				else
					prev->_right = cur->_right;
				delete cur;
				cur = nullptr;
				return true;
			}
		}
		// 当当前根节点的右树为空时,那么链接左边
		else if (cur->_right == nullptr)
		{
			// 当需要删除的节点是根节点,而根节点又没有父亲
			// 那么直接让左孩子节点成为根节点
			if (cur == _root)
			{
				_root = cur->_left;
				delete cur;
				return true;
			}
			else
			{
				if (prev->_left == cur)
					prev->_left = cur->_left;
				else
					prev->_right = cur->_left;
				delete cur;
				cur = nullptr;
				return true;
			}
		}
		// 当当前根节点的左树和右树都不为空时
		// 寻找当前节点左树的最右节点(最大节点),或是右树的最左节点(最小节点)
		// 来替换当前根节点,删除根节点
		// 我这里找右树的最左节点(最小节点)
		else
		{
			prev = cur;
			Node* del = cur->_right;
			while (del->_left != nullptr)
			{
				prev = del;
				del = del->_left;
			}

			swap(del->_key, cur->_key);

			 一开始的想法,复用上面的代码
			//if (del->_left == nullptr)
			//{
			//	if (prev->_left == del)
			//		prev->_left = del->_right;
			//	else
			//		prev->_right = del->_right;
			//	delete del;
			//	del = nullptr;
			//	return true;
			//}
			 当当前根节点的右树为空时,那么链接左边
			//else if (del->_right == nullptr)   
			//{
			//	if (prev->_left == del)
			//		prev->_left = del->_left;
			//	else
			//		prev->_right = del->_left;
			//	delete del;
			//	del = nullptr;
			//	return true;
			//}

			// 后面的想法,一棵树的最左节点,这个节点的左树一定为空
			// 一棵树的最左节点并不一定是根节点的左节点,也有可能是根节点
			// 所以这里我们并不能直接判断,使用根节点的左指针链接还是右指针链接
			if (prev->_left == del)
				prev->_left = del->_right;
			else
				prev->_right = del->_right;
			delete del;
			del = nullptr;
			return true;
		}
		return false;
	}

	
private:
	// 成员变量
	Node* _root = nullptr;
};

2.3.6.2 递归实现

这里bool _EraseR(Node*& root, const K& key)&的作用也是通过改变当前节点直接改变父亲节点的指向。

template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	// 删除、递归版本
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}
	
private:
	bool _EraseR(Node*& root, const K& key)
	{
		// 没有需要删除的节点
		if (root == nullptr)
			return false;

		if (root->_key > key)
			return _EraseR(root->_left, key);
		else if (root->_key < key)
			return _EraseR(root->_right, key);
		// 找到了删除的节点
		else
		{
			// 当当前根节点的左树为空时,那么链接右边
			if (root->_left == nullptr)
			{
				Node* del = root;
				root = root->_right;
				delete del;
				del = nullptr;
				return true;
			}
			// 当当前根节点的右树为空时,那么链接左边
			else if (root->_right == nullptr)
			{
				Node* del = root;
				root = root->_left;
				delete del;
				del = nullptr;
				return true;
			}
			else
			{
				// 开始的想法是,与上面的非递归思想相同
				/*Node* prev = root;
				Node* del = root->_right;
				while (del->_left != nullptr)
				{
					prev = del;
					del = del->_left;
				}

				swap(del->_key, root->_key);

				if (prev->_left == del)
					prev->_left = del->_right;
				else
					prev->_right = del->_right;
				delete del;
				del = nullptr;
				return true;*/

				// 后面的想法是,将替代的节点与该删除的节点交换值
				// 再在使用递归子问题,寻找原该删除节点的右树上与删除值相同的节点删除
				Node* del = root->_right;
				while (del->_left != nullptr)
				{
					del = del->_left;
				}

				swap(del->_key, root->_key);

				return _EraseR(root->_right,key);
			}
		}
	}
	// 成员变量
	Node* _root = nullptr;
};

2.3.7 二叉搜索树代码的整体实现

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:
/
	// 插入,非递归版本
	bool Insert(const K& key)
	{
		// 树为空则,该节点为根
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* cur = _root;
		Node* prev = nullptr;

		while (cur)
		{
			prev = cur;
			// 当当前节点的值大于要插入的值,那么向左找
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			// 当当前节点的值小于要插入的值,那么向右找
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			// 二叉搜索树不允许相同的值存在,当这个值已经存在过了,那么返回false,即插入失败
			else
			{
				return false;
			}
		}
		if (prev->_key > key)
		{
			prev->_left = new Node(key);
			return true;
		}
		else
		{
			prev->_right = new Node(key);
			return true;
		}
		return false;
	}

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


/
	// 查找,非递归版本
	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 FindR(const K& key)
	{
		return _FindR(_root, key);
	}


/
	// 删除,非递归版本
	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* prev = nullptr;

		// 找到需要删除的节点
		while (cur)
		{
			if (cur->_key > key)
			{
				prev = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else
			{
				break;
			}
		}

		// 当当前根节点的左树为空时,那么链接右边
		if (cur->_left == nullptr)
		{
			if (cur == _root)
			{
				_root = cur->_left;
				delete cur;
				return true;
			}
			else
			{
				if (prev->_left == cur)
					prev->_left = cur->_right;
				else
					prev->_right = cur->_right;
				delete cur;
				cur = nullptr;
				return true;
			}
		}
		// 当当前根节点的右树为空时,那么链接左边
		else if (cur->_right == nullptr)
		{
			if (cur == _root)
			{
				_root = cur->_left;
				delete cur;
				return true;
			}
			else
			{
				if (prev->_left == cur)
					prev->_left = cur->_left;
				else
					prev->_right = cur->_left;
				delete cur;
				cur = nullptr;
				return true;
			}
		}
		// 当当前根节点的左树和右树都不为空时
		// 寻找当前节点左树的最右节点(最大节点),或是右树的最左节点(最小节点)
		// 来替换当前根节点,删除根节点
		// 我这里找右树的最左节点(最小节点)
		else
		{
			prev = cur;
			Node* del = cur->_right;
			while (del->_left != nullptr)
			{
				prev = del;
				del = del->_left;
			}

			swap(del->_key, cur->_key);
			// 一棵树的最左节点,这个节点的左树一定为空
			// 一棵树的最左节点并不一定是根节点的左节点,也有可能是根节点
			// 所以这里我们并不能直接判断,使用根节点的左指针链接还是右指针链接
			if (prev->_left == del)
				prev->_left = del->_right;
			else
				prev->_right = del->_right;
			delete del;
			del = nullptr;
			return true;
		}
		return false;
	}

	// 删除、递归版本
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

//
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	// 析构函数
	~BSTree()
	{
		Destory(_root);
	}

	// 构造函数
	// C++ 11新增使用方法
	// BSTree() = default;  
	BSTree(){}

	// 拷贝构造
	BSTree(const BSTree<K>& T)
	{
		_root = Copy(T._root);
	}

	// 赋值重载
	BSTree<K>& operator=(BSTree<K> T)
	{
		swap(_root, T._root);

		return *this;
	}

private:
	Node* Copy(Node* Troot)
	{
		if (Troot == nullptr)
			return nullptr;
			
		Node* root = new Node(Troot->_key);
		root->_left = Copy(Troot->_left);
		root->_right = Copy(Troot->_right);

		return root;
	}


	void Destory(Node*& root)
	{
		if(root == nullptr)
			return;
		Destory(root->_left);
		Destory(root->_right);
		delete root;
		root = nullptr;
	}

	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

		if (root->_key > key)
			return _InsertR(root->_left, key);
		else if (root->_key < key)
			return _InsertR(root->_right, key);
		// 出现相同的数字,返回false
		else
			return false;
	}

//

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

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

//
	bool _EraseR(Node*& root, const K& key)
	{
		// 没有需要删除的节点
		if (root == nullptr)
			return false;

		if (root->_key > key)
			return _EraseR(root->_left, key);
		else if (root->_key < key)
			return _EraseR(root->_right, key);
		// 找到了删除的节点
		else
		{
			// 当当前根节点的左树为空时,那么链接右边
			if (root->_left == nullptr)
			{
				Node* del = root;
				root = root->_right;
				delete del;
				del = nullptr;
				return true;
			}
			// 当当前根节点的右树为空时,那么链接左边
			else if (root->_right == nullptr)
			{
				Node* del = root;
				root = root->_left;
				delete del;
				del = nullptr;
				return true;
			}
			else
			{
				// 开始的想法是,与上面的非递归思想相同
				/*Node* prev = root;
				Node* del = root->_right;
				while (del->_left != nullptr)
				{
					prev = del;
					del = del->_left;
				}

				swap(del->_key, root->_key);

				if (prev->_left == del)
					prev->_left = del->_right;
				else
					prev->_right = del->_right;
				delete del;
				del = nullptr;
				return true;*/

				// 后面的想法是,将替代的节点与该删除的节点交换值
				// 再在使用递归子问题,寻找原该删除节点的右树上与删除值相同的节点删除
				Node* del = root->_right;
				while (del->_left != nullptr)
				{
					del = del->_left;
				}

				swap(del->_key, root->_key);

				return _EraseR(root->_right,key);
			}
		}
	}


	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << ' ';
		_InOrder(root->_right);
	}
private:
	// 成员变量
	Node* _root = nullptr;
};

// 测试二叉搜索树
void TestBSTree()
{
	BSTree<int> dict;
	// 8, 3, 1, 10, 6, 4, 7, 14, 13
	int arr[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		dict.InsertR(arr[i]);
	}

	dict.InOrder();

	/*dict.EraseR(3);
	dict.InOrder();

	dict.EraseR(8);
	dict.InOrder();

	dict.EraseR(14);
	dict.InOrder();

	int i = 0;
	while (i < sizeof(arr) / sizeof(arr[0]))
	{
		auto ret = dict.FindR(arr[i]);
		if (ret)
		{
			dict.EraseR(arr[i]);
			dict.InOrder();
		}
		i++;
	}*/

	BSTree<int> copy(dict);
	copy.InOrder();
	BSTree<int> copy1;
	copy1 = copy;
	copy1.InOrder();
}


2.4 二叉搜索树的应用

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

上面2.3中的代码是K模型,下面的代码是KV模型
其实K模型与KV模型的代码大体是一样的,K模型中的节点只有 _key 一个关键码,而KV模型中有_key , _value两个关键码。

template<class K, class V>
struct BSTreeNode
{
	BSTreeNode<K, V>* _left;
	BSTreeNode<K, V>* _right;
	K _key;
	V _value;
	BSTreeNode(const K& key, const V& value)
		:_key(key)
		, _value(value)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

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

	bool Insert(const K& key, const V& value)
	{
		// 树为空则,该节点为根
		if (_root == nullptr)
		{
			_root = new Node(key, value);
			return true;
		}
		Node* cur = _root;
		Node* prev = nullptr;

		while (cur)
		{
			prev = cur;
			// 当当前节点的值大于要插入的值,那么向左找
			if (cur->_key > key)
			{
				cur = cur->_left;
			}
			// 当当前节点的值小于要插入的值,那么向右找
			else if (cur->_key < key)
			{
				cur = cur->_right;
			}
			// 二叉搜索树不允许相同的值存在,当这个值已经存在过了,那么返回false,即插入失败
			else
			{
				return false;
			}
		}
		if (prev->_key > key)
		{
			prev->_left = new Node(key, value);
			return true;
		}
		else
		{
			prev->_right = new Node(key, value);
			return true;
		}
		return false;
	}	

	Node* 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 cur;
			}
		}
		// 找不到
		return nullptr;
	}


	
	// 删除,非递归版本
	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* prev = nullptr;

		// 找到需要删除的节点
		while (cur)
		{
			if (cur->_key > key)
			{
				prev = cur;
				cur = cur->_left;
			}
			else if (cur->_key < key)
			{
				prev = cur;
				cur = cur->_right;
			}
			else
			{
				break;
			}
		}

		// 当当前根节点的左树为空时,那么链接右边
		if (cur->_left == nullptr)
		{
			if (cur == _root)
			{
				_root = cur->_left;
				delete cur;
				return true;
			}
			else
			{
				if (prev->_left == cur)
					prev->_left = cur->_right;
				else
					prev->_right = cur->_right;
				delete cur;
				cur = nullptr;
				return true;
			}
		}
		// 当当前根节点的右树为空时,那么链接左边
		else if (cur->_right == nullptr)
		{
			if (cur == _root)
			{
				_root = cur->_left;
				delete cur;
				return true;
			}
			else
			{
				if (prev->_left == cur)
					prev->_left = cur->_left;
				else
					prev->_right = cur->_left;
				delete cur;
				cur = nullptr;
				return true;
			}
		}
		
		else
		{
			prev = cur;
			Node* del = cur->_right;
			while (del->_left != nullptr)
			{
				prev = del;
				del = del->_left;
			}

			swap(del->_key, cur->_key);

			if (prev->_left == del)
				prev->_left = del->_right;
			else
				prev->_right = del->_right;
			delete del;
			del = nullptr;
			return true;
		}
		return false;
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	~BSTree()
	{
		Destory(_root);
	}

	BSTree() {}

	BSTree(const BSTree<K, V>& T)
	{
		_root = Copy(T._root);
	}

	BSTree<K, V>& operator=(BSTree<K, V> T)
	{
		swap(_root, T._root);

		return *this;
	}

private:
	Node* Copy(Node* Troot)
	{
		if (Troot == nullptr)
			return nullptr;

		Node* root = new Node(Troot->_key);
		root->_left = Copy(Troot->_left);
		root->_right = Copy(Troot->_right);

		return root;
	}

	void Destory(Node*& root)
	{
		if (root == nullptr)
			return;
		Destory(root->_left);
		Destory(root->_right);
		delete root;
		root = nullptr;
	}

	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

		if (root->_key > key)
			return _InsertR(root->_left, key);
		else if (root->_key < key)
			return _InsertR(root->_right, key);
		// 出现相同的数字,返回false
		else
			return false;
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_key << ':' << root->_value << endl;
		_InOrder(root->_right);
	}
	Node* _root = nullptr;
};

void TestBSTree()
{
	BSTree<string, string> dict;
	dict.Insert("insert", "插入");
	dict.Insert("erase", "删除");
	dict.Insert("left", "左边");
	dict.Insert("string", "字符串");

	dict.InOrder();
	string str;
	while (cin >> str)
	{
		auto ret = dict.Find(str);
		if (ret)
		{
			cout << str << ":" << ret->_value << endl;
		}
		else
		{
			cout << "单词拼写错误" << endl;
		}
	}

	string strs[] = { "苹果", "西瓜", "苹果", "樱桃", "苹果", "樱桃", "苹果", "樱桃", "苹果" };
	// 统计水果出现的次
	BSTree<string, int> countTree;
	for (auto str : strs)
	{
		auto ret = countTree.Find(str);
		if (ret == NULL)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}
	countTree.InOrder();
}

2.5 二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:logN(log以2为底N的对数)
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。


三、二叉树进阶题目

3.1 根据二叉树创建字符串【OJ题目】

讲解:本道题目中并不是要求简单的前序遍历,他需要将二叉树转化为一个由括号和整数组成的字符串,并且这个括号有要求:

(1)当左孩子节点和右孩子节点不为空时,那么两个括号都不能省略。
(2)当左孩子节点不为空,并且右孩子节点为空时,那么左孩子的括号可以省略,右孩子的括号不能省略。
(3)当左孩子节点为空,并且右孩子节点不为空时,那么左孩子和右孩子的括号都不可以省略。
(4)当左孩子节点为空,并且右孩子节点为空时,那么左孩子和右孩子的括号可以省略。

在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    string ans;
    void _tree2str(TreeNode* root) 
    {
        if(root == nullptr)
            return;
        // 这里的数字并不是简单的 0 ~ 9 ,所以不能 +=数字 + '0'
        ans += to_string(root->val);
        
        // 这里的条件和巧妙
        // 这里的条件应该是 root->left || (root->left == nullptr && root->right)
        // root 不为 nullptr 就是 nulltr , 如果root 为 nullptr 那么就不会判断第二个条件
        // 剩下一种情况就是 root 必定是 nullptr ,如果第一个条件不符合,
        // 那么第二个条件的第一个条件必定符合,那么这个条件就可以省略了
        
        if(root->left || root->right)
        {
            ans += '(';
            _tree2str(root->left);
            ans += ')';
        }
        if(root->right)
        {
            ans += '(';
            _tree2str(root->right);
            ans += ')';
        }
    }
    string tree2str(TreeNode* root) {
        _tree2str(root);
        return ans;
    }
};

3.2 二叉树的层序遍历Ⅰ【OJ题目】

讲解:层序遍历是按照每一层的顺序来输出节点的遍历方法。
使用一个vector变量来存储遍历结果。
对于层序遍历,我们需要遍历一层节点时,并且遍历当前节点时,需要记录下一层的节点,并且记录节点的时候还要有顺序,这时候就需要队列(queue)来记录每一层的节点。
但是单单这样并不能确定当前节点是上一层的还是当前层的,我们需要一个变量count来记录每一层有多少个节点,循环变量队列中的节点,遍历一个节点count--,当这个count == 0时,那么队列中的节点为下一层的节点。重复上面部分,当队列为空时,那么这棵树则遍历完成。

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        queue<TreeNode*> qu;
        vector<int> v;
        if(root == nullptr)
            return ans;

        qu.push(root);
        int count = 0; // 记录每一层有多少个数据
        while(!qu.empty())
        {
            count = qu.size();
            while(count--)
            {
                TreeNode* top = qu.front();
                qu.pop();
                v.push_back(top->val);
                if(top->left != nullptr)
                    qu.push(top->left);
                if(top->right != nullptr)
                    qu.push(top->right);
            }
            ans.push_back(v);
            v.clear();
        }
        return ans;
    }
};

3.3 二叉树的层序遍历Ⅱ【OJ题目】

讲解:本题思路与上面一题思路相同,只需要按照上面一题的步骤遍历完后,将记录结果的vector逆转一下,就是这个题目的结果。

class Solution {
public:
    vector<vector<int>> levelOrderBottom(TreeNode* root) {
        vector<vector<int>> ans;
        queue<TreeNode*> qu;
        vector<int> v;
        if(root == nullptr)
            return ans;

        qu.push(root);
        int count = 0; // 记录每一层有多少个数据
        while(!qu.empty())
        {
            count = qu.size();
            while(count--)
            {
                TreeNode* top = qu.front();
                qu.pop();
                v.push_back(top->val);
                if(top->left != nullptr)
                    qu.push(top->left);
                if(top->right != nullptr)
                    qu.push(top->right);
            }
            ans.push_back(v);
            v.clear();
        }
        reverse(ans.begin() , ans.end());
        return ans;
    }
};

3.4 二叉树的最近公共祖先【OJ题目】

讲解:
方法一,两个节点在从根节点来看有四种情况
:当两个节点一个节点是根节点,另一个节点在左子树/右子树中,那么根节点就是最近祖先
:两个节点都在左子树,那么最近公共祖先一定在左树中,分解为子问题,向一、四靠拢
:两个节点都在右子树,那么最近公共祖先一定在右树中,分解为子问题,向一、四靠拢
:当两个节点,一个节点在左子树,一个节点在右子树,那么根节点就是最近祖先

方法二,寻找两个节点的路径
再找到相同的节点,那么这个节点就是两个节点的最近的公共祖先

// 方法一
class Solution {
public:
    // 判断该节点是否在树中
    bool Find(TreeNode* root , TreeNode* x)
    {
        if(root == nullptr)
            return false;
        if(root == x)
            return true;

        bool left , right;
        left = Find(root->left , x);
        if(left)    
            return true;
        right = Find(root->right , x);
        if(right)   
            return true;
        return false;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == nullptr)
            return nullptr;
        // 情况一
        if(p == root || q == root)
            return root;

        // 记录两个节点是否在左树
        bool pleft = Find(root->left , p);
        bool qleft = Find(root->left , q);

        // p 在左树,q在右树 || p 在右树 ,q在左树
        if((pleft && !qleft) || (!pleft && qleft))
            return root;
        // 两个节点都在左树
        else if(pleft && qleft)
            return lowestCommonAncestor(root->left,p,q);
        // 两个节点都在右树
        else // (!pleft && !rigth)
            return lowestCommonAncestor(root->right,p,q);
    }
};

// 方法二
class Solution {
public:
    bool Find(stack<TreeNode*>& path , TreeNode* root , TreeNode* x)
    {
        if(root == nullptr)
            return false;
        path.push(root);
        if(root == x)
            return true;

        bool left , right;
        left = Find(path , root->left , x);
        if(left)    
            return true;
        right = Find(path , root->right , x);
        if(right)   
            return true;
        path.pop();
        return false;
    }   
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) 
    {
        stack<TreeNode*> Ppath , Qpath;
        Find(Ppath , root , p);
        Find(Qpath , root , q);
        while(!Ppath.empty())
        {
            if(Ppath.size() > Qpath.size())
            {
                Ppath.pop();
            }
            else if(Ppath.size() < Qpath.size())
            {
                Qpath.pop();
            }
            else
            {
                if(Ppath.top() == Qpath.top())
                {
                    return Ppath.top();
                }
                else
                {
                    Qpath.pop();
                    Ppath.pop();
                }
            }
        }
        return nullptr;
    }
};

3.5 二叉搜索树与双向链表【OJ题目】

讲解:
本题有空间复杂度的限制,所以不能通过vector前序变量记录节点,再链接,也不能创建新节点。
这个题有个很巧妙的方法就是定义一个变量来记录上一个访问的节点,前序遍历当前树,遍历当前节点时用上一个节点的右指针指向当前节点,当前节点的左指针指向上一个节点,当这棵树的遍历完后即完成了二叉搜索树和双向链表的转化。

class Solution {
public:
    TreeNode* prev = nullptr;
    void _Convert(TreeNode* root) 
    {
        if(root == nullptr)
            return;
          
        _Convert(root->left);
        if(prev != nullptr)
        {
			prev->right = root;
            root->left = prev;
        }
        
        prev = root;

        _Convert(root->right);
    }
      
    TreeNode* Convert(TreeNode* pRootOfTree) {
        if(pRootOfTree == nullptr)
            return nullptr;
        _Convert(pRootOfTree);
        TreeNode* cur = pRootOfTree;
        while(cur->left)
        {
            cur = cur->left;
        }
  
        return cur;
    }
};


3.6 从前序与中序遍历序列构造二叉树【OJ题目】

讲解:
学过二叉树的都知道二叉树的前序遍历确定根节点,中序遍历确定左右区间。
那么就可以使用一个变量记录前序遍历的位置,两个变量来记录左右区间的范围。当区间不存在的时候那么当前子树则构建完成,当返回完后,则这棵树构建完成。

class Solution {
public:
    TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder , int& pos , int begin , int end) 
    {
        int div = begin;
        for(int i = begin ; i <= end ; i++)
        {
            if(inorder[i] == preorder[pos])
            {
                div = i;
                break;
            }
        }
        TreeNode* root = new TreeNode(preorder[pos]);
        if(begin <= div - 1)
            root->left = _buildTree(preorder , inorder , ++pos , begin , div - 1);
        if(div + 1 <= end)
            root->right = _buildTree(preorder , inorder , ++pos , div + 1 , end);
        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) 
    {
        int prei = 0;
        return _buildTree(preorder , inorder , prei , 0 , preorder.size()-1);
    }
};

3.7 从中序与后序遍历序列构造二叉树【OJ题目】

讲解:这一题与上面题的思路一致,只是这里是后序确定根节点,中序确定左右区间,并且需要先创建右孩子节点。

class Solution {
public:
    TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder , int& pos , int begin , int end) 
    {
        int div = 0;
        for(int i = begin ; i <= end ; i++)
        {
            if(postorder[pos] == inorder[i])
            {
                div = i;
                break;
            }
        }
        TreeNode* root = new TreeNode(postorder[pos]);
        if(div + 1 <= end)
            root->right = _buildTree(inorder , postorder , --pos , div + 1 , end);
        if(begin <= div - 1)
            root->left = _buildTree(inorder , postorder , --pos , begin , div - 1);
        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int prei = postorder.size()-1;
        return _buildTree(inorder , postorder , prei , 0 , inorder.size()-1);
    }
};

3.8 二叉树的前序遍历,非递归迭代实现【OJ题目】

讲解:二叉树的前序遍历顺序是根、左子树、右子树,非递归的遍历可以看作两步:

  1. 将当前树的左路节点入栈并在入栈时访问当前节点;
  2. 遍历左路节点的右子树。

定义一个栈来存储没访问右子树的左路节点,定义一个vector来存储答案。首先遍历当前树的左路节点,因为这里是前序遍历,需要先遍历根节点,所以每存储一个节点在栈中,就将访问这个节点并将值存入vector中,当前树的所有左路节点都被存储后,那么再从栈中栈顶元素,从栈中取出来的元素就说明这个节点的左树已经访问完了,然后再访问它的右树,重复上面的步骤,直到栈为空并且当前节点也为空时,则完成二叉树前序遍历的非递归的实现。

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> ans;
        TreeNode* cur = root;
        while(cur || !st.empty())
        {
        	// 左路节点的入栈并访问
            while(cur)
            {
                st.push(cur);
                ans.push_back(cur->val);
                cur = cur -> left;
            }
			
			// 子问题访问左路节点的右子树
            cur = st.top()->right;
            st.pop();
        }
        return ans;
    }
};

在这里插入图片描述


3.9 二叉树的中序遍历 ,非递归迭代实现【OJ题目】

讲解:二叉树的中序遍历顺序是左子树、根、右子树,非递归的遍历可以看作两步:

  1. 入栈当前树的左路节点并在出栈时访问当前节点;
  2. 遍历左路节点的右子树。

本题的逻辑与上题的非常相似,只是访问节点的时机不同,前序遍历访问的时间是在节点入栈的时候,而中序遍历访问的时间是在出栈的时候。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> ans;
        TreeNode* cur = root;
        while(cur || !st.empty())
        {
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }
            ans.push_back(st.top()->val);
            cur = st.top()->right;
            st.pop();
        }

        return ans;
    }
};

在这里插入图片描述


3.10 二叉树的后序遍历 ,非递归迭代实现 【OJ题目】

讲解:二叉树的后序遍历顺序是左子树、右子树、根,非递归的遍历可以看作两步:

  1. 入栈当前树的左路节点并在栈顶元素的右为空 或是 栈顶元素的右是上一个访问的节点;
  2. 遍历左路节点的右子树。

定义一个栈来存储没访问右子树的左路节点,定义一个vector来存储答案,定义一个prev来记录上一个访问的节点。首先遍历当前树的左路节点,因为这里是前序遍历,需要先遍历根节点,所以每存储一个节点在栈中,当前树的所有左路节点都被存储后,那么再从栈中栈顶元素,从栈中取出来的元素就说明这个节点的左树已经访问完了,若栈顶元素的右为空 或是 栈顶元素的右是上一个访问的节点,就访问这个栈顶元素并将值存储到vector中,否则就访问访问它的右树,重复上面的步骤,直到栈为空并且当前节点也为空时,则完成二叉树前序遍历的非递归的实现。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> ans;
        TreeNode* cur = root;
        // prev记录上一个访问的节点
        TreeNode* prev = nullptr;

        while(cur || !st.empty())
        {
            // 将当前树的左路节点全部入栈
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }
            
            TreeNode* top = st.top();

            // 若栈顶元素的右为空 或是 栈顶元素的右是上一个访问的节点
            // 说明这棵树的左边已经访问完
            // 并且右边没有节点不需要访问或是右边有节点已经访问过了
            if(top -> right == nullptr || top -> right == prev)
            {
                ans.push_back(top->val);
                prev = top;
                st.pop();
            }
            // 右子树不为空且没访问过
            // 看成子问题访问右树
            else
            {
                cur = st.top()->right;
            }
        }
        return ans;
    }
};


结尾

如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,请大家给一个三连支持一下!!🌹🌹
在这里插入图片描述

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

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

相关文章

跨平台之用VisualStudio开发APK嵌入OpenCV(二)

开始干 新建解决方案&#xff0c;新建动态库&#xff08;Android&#xff09;项目 功能随便选一个吧&#xff0c;就模仿PS&#xff08;Photoshop&#xff09;的透视裁切功能&#xff0c;一个物体&#xff08;比如扑克牌&#xff09;透视图&#xff0c;选4个顶点&#xff0c;转…

2000 年至 2015 年中国(即水稻、小麦和玉米1km 网格)三种主要作物年收获面积的时空变化

摘要 可靠、连续的主要作物收获面积信息对于研究地表动态和制定影响农业生产、土地利用和可持续发展的政策至关重要。然而&#xff0c;中国目前还没有高分辨率的空间明确和时间连续的作物收获面积信息。全国范围内主要农作物收获面积的时空格局也鲜有研究。在本研究中&#xf…

【深度学习】第1章

概论: 机器学习是对研究问题进行模型假设,利用计算机从训练数据中学习得到模型参数,并最终对数据进行预测和分析,其基础主要是归纳和统计。 深度学习是一种实现机器学习的技术,是机器学习重要的分支。其源于人工神经网络的研究。深度学习的模型结构是一种含多隐层的神经…

长安链使用Golang编写智能合约教程(一)

编写前的注意事项&#xff1a; 1、运行一条带有Doker_GoVM的链 2、建议直接用官方的在线IDE去写合约&#xff0c;因为写完可以直接测&#xff0c;缺点只是调试不方便。 3、自己拉环境在本地写合约&#xff0c;编译时注意编译环境&#xff0c;官方有提醒你去Linux下去编译。 …

【2024.5.26 软件设计师】记录第一次参加软考(附资料)

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux &#x1f618;欢迎 ❤️关注 &#x1f44d;点赞 &#x1f64c;收藏 ✍️留言 文章目录 前言考试分析选择题案例分析题话外 软考总结资料 前言 这是我第一次参加软考&#xff0c;其实我并…

到底该用英文括号还是中文括号?

这篇博客写的还挺详细的&#xff0c;不错。

如何使用多种算法解决LeetCode第135题——分发糖果问题

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

美甲店会员预约系统管理小程序的作用是什么

女性爱美体现在方方面面&#xff0c;美丽好看的指甲也不能少&#xff0c;市场中美甲店、小摊不少&#xff0c;也跑出了不少连锁品牌&#xff0c;70后到00后&#xff0c;每个层级都有不少潜在客户&#xff0c;商家需要获取和完善转化路径&#xff0c;不断提高品牌影响力与自身内…

【图解IO与Netty系列】IO的同步与异步、阻塞与非阻塞,Linux五种IO模型

IO的同步与异步、阻塞与非阻塞&#xff0c;Linux五种IO模型 IO的同步与异步&#xff0c;阻塞与非阻塞阻塞IO与非阻塞IO同步IO与异步IO Linux五种IO模型BIONIOIO多路复用信号驱动IOAIO IO的同步与异步&#xff0c;阻塞与非阻塞 我们有时会看到类似于同步阻塞式IO、同步非阻塞式…

【二叉树算法题记录】236. 二叉树的最近公共祖先

题目链接 题目描述 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个节点 p、q&#xff0c;最近公共祖先表示为一个节点 x&#xff0c;满足 x 是 p、q 的祖先且 x 的深度尽可能大&#xff08;一个…

STL---unordered set和unordered multiset【无序集合】

1.1 定义及初始化&#x1f357; 下面列出常用的初始化方式 #include <unordered_set> #include <iostream> using namespace std; //输出s中的所有元素 template<typename T> void Show(const T& s) {for (auto& x : s) …

操作系统实验四:多线程与信号量编程

操作系统实验上机 更多技术请访问&#xff1a;www.xuanworld.top 部分审核不通过的文章将发至个人博客&#xff1a;www.xuanworld.top 欢迎来52破解论坛阅读帖子&#xff1a;https://www.52pojie.cn/thread-1891208-1-1.html 实验名称实验序号实验日期实验人多线程与信号量…

信号量——多线程

信号量的本质就是一个计数器 在多线程访问临界资源的时候&#xff0c;如果临界资源中又有很多份分好的资源&#xff0c;那么就可以通过信号量来表示里面还有多少份资源&#xff0c;且每份资源只有一个线程可以访问 线程申请信号量成功&#xff0c;就一定有一份资源是你的&…

Golang并发编程-协程goroutine的信道(channel)

文章目录 前言一、信道的定义与使用信道的声明信道的使用 二、信道的容量与长度三、缓冲信道与无缓冲信道缓冲信道无缓冲信道 四、信道的初体验信道关闭的广播机制 总结 前言 Goroutine的开发&#xff0c;当遇到生产者消费者场景的时候&#xff0c;离不开 channel&#xff08;…

基于Matplotlib包实现可视化①——折线图绘制

Matplotlib 是一个用于创建静态、动画、 和交互式可视化的第三方库&#xff0c;也是我们在借助Python进行数据可视化时经常使用到的库&#xff0c;具有较强的可视化能力。 为让大家有一个更为直观的认识&#xff0c;这里我随机从其官方网页中摘取了几张图片。 按照惯例&#x…

【安装笔记-20240523-Windows-安装测试 ShareX】

安装笔记-系列文章目录 安装笔记-20240523-Windows-安装测试 ShareX 文章目录 安装笔记-系列文章目录安装笔记-20240523-Windows-安装测试 ShareX 前言一、软件介绍名称&#xff1a;ShareX主页官方介绍 二、安装步骤测试版本&#xff1a;16.1.0下载链接功能界面 三、应用场景屏…

2024年【危险化学品经营单位安全管理人员】考试及危险化学品经营单位安全管理人员考试资料

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品经营单位安全管理人员考试考前必练&#xff01;安全生产模拟考试一点通每个月更新危险化学品经营单位安全管理人员考试资料题目及答案&#xff01;多做几遍&#xff0c;其实通过危险化学品经营单位安全管理…

强化学习4:DQN 算法

看这篇文章之前&#xff0c;建议先了解一下&#xff1a;Q-Learning 算法。 1. 算法介绍 DQN 算法全称为 Deep Q-Network&#xff0c;即深度Q网络。它将 Q-Learning 与 Deep Learning 结合在了一起。 1.1 Q-Network Q-Learning 是使用 Q-table 才存储决策信息的&#xff0c;…