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

news2024/11/18 4:56:25

在这里插入图片描述

目录

  • 一、内容安排说明
  • 二、 二叉搜索树
    • 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;
    }
};


结尾

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

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

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

相关文章

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

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

企业档案管理系统软件都有哪些分类

企业档案管理系统软件可以根据其功能和特点进行分类。以下是一些常见的分类&#xff1a; 1. 全能类档案管理系统&#xff1a;提供文件存储和检索功能&#xff0c;并支持多种文件类型和格式的管理&#xff0c;如文本文件、图像文件、音频文件等。 2. 电子档案管理系统&#xff1…

源码编译安装LAMP(安装apeche mysql php 论坛 网站 巨详细版)

目录 一.LAMP架构相关概述 1.各组件作用 Linux&#xff08;平台&#xff09; Apache&#xff08;前台&#xff09; MySQL&#xff08;后台&#xff09; PHP/Perl/Python&#xff08;中间连接&#xff09; 总结 二.编译安装Apache httpd服务 1.关闭防火墙&#xff0c;将…

2024年【T电梯修理】考试内容及T电梯修理新版试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【T电梯修理】考试内容及T电梯修理新版试题&#xff0c;包含T电梯修理考试内容答案和解析及T电梯修理新版试题练习。安全生产模拟考试一点通结合国家T电梯修理考试最新大纲及T电梯修理考试真题汇总&#xff0c;…

UI控件与视图层次:探索界面的无限可能

[OC]UI学习笔记 文章目录 [OC]UI学习笔记视图和视图层次结构CGRectUILabelUIButtonUIView控件UIView的层级关系UIWindow定时器和视图移动UISwitch进度条和滑动条控件步进器和分栏控件警告对话框与等待指示器UITextField 视图和视图层次结构 Objective-C中的UI编程主要围绕视图…

IO系列(八) -浅析NIO工作原理

一、简介 现在使用 NIO 的场景越来越多&#xff0c;很多网上的技术框架或多或少的使用 NIO 技术&#xff0c;譬如 Tomcat、Jetty、Netty&#xff0c;学习和掌握 NIO 技术已经不是一个 Java 攻城狮的加分技能&#xff0c;而是一个必备技能。 那什么是 NIO 呢&#xff1f; NIO…

民国漫画杂志《时代漫画》第24期.PDF

时代漫画24.PDF: https://url03.ctfile.com/f/1779803-1248635000-177187?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了&#xff0c;截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!

怎么理解直接程序控制和中断方式?

直接程序控制 看完之后是不是依然一头雾水&#xff1f;来看下面两个例子 无条件传送 假设你正在使用键盘打字。当你敲击键盘上的一个键时&#xff0c;键盘会立即产生一个信号&#xff08;即输入数据&#xff09;&#xff0c;并且这个信号会立即被电脑接收。在这个过程中&…

属于程序员的浪漫,一颗会跳动的心!!!

绘制一颗会跳动的心❤ 嘿嘿 可以说是程序员的专属浪漫了吧&#xff0c;就像点燃一颗LED灯一样&#xff1f;&#xff08;我瞎说的啊&#xff0c;大家别当真&#xff0c;我很菜的&#xff01;&#xff01;&#xff01;&#xff01;&#xff09; 程序就在下面啦&#xff0c;然…

Markdown魔法手册:解锁高效写作的新技能

边使用边更新0.0... 文章目录 一、如何在Markdown中插入表情&#xff1f;二、文字样式设置1.文本颜色设置2.文本字号设置3.文本字体设置4. 实战演练5.黄色高亮 一、如何在Markdown中插入表情&#xff1f; 在Markdown中插入表情&#xff08;emoji&#xff09;的方法取决于你使用…

C#--SVG矢量图画法示例

1.代码示例 <Viewbox Grid.Column"1" Grid.ColumnSpan"1" Grid.RowSpan"1" ><Path Name"ValveShape" Stroke"Black" Data"M 50,0 L 150,200 L 50,200 L 150,0 Z" Width"200" Height"…

网络安全等级保护:正确配置 Linux

正确配置 Linux 对Linux安全性的深入审查确实是一项漫长的任务。原因之一是Linux设置的多样性。用户可以使用Debian、Red Hat、Ubuntu或其他Linux发行版。有些可能通过shell工作&#xff0c;而另一些则通过某些图形用户界面&#xff08;例如 KDE 或 GNOME&#xff09;工作&…

uni-app微信小程序动态切换tabBar,根据不同用户角色展示不同的tabBar

前言 在UniApp的开发小程序过程中&#xff0c;为了针对不同角色用户登录后的个性化需求。通过动态权限配置机制&#xff0c;能够根据用户的角色展示不同的TabBar。此项目是通过Uni-App命令行的方式搭建的Vue3ViteTsPiniaUni-ui的小程序项目 最终效果 1、司机角色&#xff1a; …

Kubernetes(K8S) 集群环境搭建指南

Kubernetes&#xff08;简称K8s&#xff09;是一个开源的容器编排平台&#xff0c;旨在自动化部署、扩展和管理容器化应用。K8S环境搭建过程比较复杂&#xff0c;涉及到非常多组件安装和系统配置&#xff0c;本文将会详细介绍如何在服务器上搭建好Kubernetes集群环境。 在学习…

MybatisPlus中自定义sql

背景 在开发过程中&#xff0c;可能会出现除了where条件&#xff0c;其它sql比较复杂&#xff0c;这时候就需要用到自定义sql了。 问题 如&#xff1a;用户状态为正常的数据年龄加一&#xff08;所有用户年龄加一&#xff09; 数据库中sql&#xff1a; UPDATE USER SET…

Go GORM介绍

GORM 是一个功能强大的 Go 语言 ORM&#xff08;对象关系映射&#xff09;库&#xff0c;它提供了一种方便的方式来与 SQL 数据库进行交互&#xff0c;而不需要编写大量的 SQL 代码。 GORM的关键特性 全功能的ORM&#xff1a;支持几乎所有的ORM功能&#xff0c;包括模型定义、基…

【漏洞复现】英飞达医学影像存档与通信系统 WebJobUpload 任意文件上传漏洞

0x01 产品简介 英飞达医学影像存档与通信系统 Picture Archiving and Communicaton System&#xff0c;它是应用在医院影像科室的系统&#xff0c;主要的任务就是把日常产生的各种医学影像(包括核磁&#xff0c;CT&#xff0c;超声&#xff0c;各种X光机&#xff0c;各种红外仪…

如何编辑 PDF 中的文本?4个有效的编辑PDF方法

PDF 文件可以轻松打开和查看&#xff0c;但修改要复杂得多 - 尤其是在 PDF 中的文本编辑方面。 知道如何离线编辑 PDF 中的文本对于任何需要快速更改而无需在线加载文档或担心安全问题的人来说都非常有益。它使用户能够更好地控制他们的文档&#xff0c;并有更广泛的字体和图形…

详解 Scala 的集合类型

一、集合简介 1. 类型 序列 Seq&#xff1a;类似于 Java 中的 List 接口集 Set&#xff1a;类似于 Java 中的 Set 接口映射 Map&#xff1a;类似于 Java 中的 Map 接口所有的集合都扩展自 Iterable 特质 2. 不可变集合 位于 scala.collection.immutable 包&#xff0c;指该集…

【Linux】fork和exec中的信号继承探索

fork和exec中的信号继承探索 一、结论二、代码验证2.1 代码编写2.2 代码执行 三、linux源码验证四、APUE中的验证五、其他 一、结论 fork时子进程会继承父进程的信号处理方式&#xff0c;包括父进程设置信号为SIG_DFL或SIG_IGN或捕获后设置自定义处理函数。exce时子进程会继承…