二叉树进阶(搜索二叉树)

news2024/11/24 17:39:51

目录

引言 

1.二叉搜索树的模拟实现

1.1  链式二叉树的定义

1.2 二叉搜索树的模拟实现 

1.2.1 二叉搜索树的结点类

1.2.2 二叉搜索树类的构造与中序遍历实现

1.2.3 增

1.非递归实现

2.非递归实现

1.2.4 查

1.非递归实现

2.递归实现 

1.2.5 删

1.非递归实现

(1)情况分析

(2)解决方案 

(3)领养代码实现 

(4)替代法代码实现 

2.递归实现

1.2.6  整体代码展示

2.二叉搜索树的应用

2.1 key模型

2.2 key-value(kv)模型

3.二叉搜索树的相关oj题

1. 二叉树创建字符串

2. 二叉树的分层遍历1

3. 二叉树的分层遍历2

4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先

5. 二叉树搜索树转换成排序双向链表

6. 根据一棵树的前序遍历与中序遍历构造二叉树

7. 根据一棵树的中序遍历与后序遍历构造二叉树


引言 

在之前文章中,我们用C语言实现了链式二叉树,在该节,我们将会通过C++的方式,对二叉树进

一步研究,尝试手动模拟实现二叉搜索树,为接下来学习map,set打下相应的基础

1.二叉搜索树的模拟实现

1.1  链式二叉树的定义

在链式二叉树的一节中,我们曾经提到过

由于二叉树的形状是不确定的,甚至可以全部倾向一边,构成我们熟悉的单链表.

因此,从某种意义上来说,二叉树的增删查改并没有意义,除非有着特殊结构的限定

所以,这节主要研究的就是二叉树中一种特殊的结构——搜索二叉树(如图所示)

搜索二叉树的特点:

1. 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

2. 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

3. 它的左、右子树也分别为二叉搜索树

1.2 二叉搜索树的模拟实现 

1.2.1 二叉搜索树的结点类

struct BSTreeNode
{
	BSTreeNode <K>* _left;
	BSTreeNode <K>* _right;
	K _key;

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

构造函数,直接将结点左右指针都赋为空,同时_key置为用户给的key值 

1.2.2 二叉搜索树类的构造与中序遍历实现

二叉搜索树类的成员就一个——根

为了接下来代码书写简便,和前面模拟实现一样,对二叉搜索树类型重命名

typedef BSTreeNode<K> Node;

二叉搜索树其实还有一个非常有趣的特征,中序遍历二叉搜索树,得到的会是一个有序的序列

因此,我们可以通过在类里面实现中序遍历,来迅速简单验证我们构造的二叉搜索树是否成功

调用中序遍历Inorder,需要我们传根,但是根是我们类里面的私有成员

在类外部,是无法直接访问的,除非我们在类里面加一个Getroot的成员函数

不过这样就会显得代码有些冗余,所以我们会换一种做法,通常是将中序遍历进行嵌套实现

这样用户调用的时候,不需要传根,同时在类里面,我们也可以直接访问根这个私有成员

//中序遍历
  	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}

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

 PS:

这里不能采用给缺省值来解决问题

void _Inorder(Node* root = _root) ❌

1.缺省值必须是全局变量或者常量,而这里的_root显然不是

2.函数成员变量的访问,需要this指针,它是一个临时形参,只能在函数内部访问,现在连函数内部都还没进去,怎么可以通过this指针来访问函数成员呢?

1.2.3 增

1.非递归实现

我们第一个实现的是插入函数,这样我们就可以先构造出我们的二叉搜索树,并简单用中序遍历,

验证实现是否成功,这样再进行后续的模拟实现

二叉搜索树的插入(增)

a. 树为空,则直接新增节点,赋值给root指针

b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

树为空,这没有什么好解释的,new一个结点,对应结点指针指向_root即可

但假如树不为空,就必须满足二叉搜索树的性质,先找到结点的正确位置,再进行插入

找到结点位置没有什么好解释的,就按照新节点的val值和现在当前节点的val作比较,如果比它

大,则向右移动插入,否则,就向左移动插入

同时为了方便插入,我们还需要随时记录插入结点的父节点位置指针

比如下面这张图,要插入16,我们移动cur的同时,还需要记录它的parent,这样当cur为空,即找

到对应的正确位置,就及时将parent指向它

PS:cur可能是parent的左孩子也可能是右孩子,所以在调整指向的时候,还需要进一步判断

	bool insert(const K& key)
	{
		//如果是第一个元素,直接就将它作为二叉树的根
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		//如果不是就考虑插入元素
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			//如果当前结点的值小于Key,往右移动
			if (cur->_key < key)
			{   
				parent = cur;
				cur = cur-> _right;
			}
			//如果当前结点的值大于Key,往左移动
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			//出现相同元素,直接报错
			else
			{
				return false;
			}
		}
		//找到对应正确位置,new一个节点,并且将parent指向它
		cur = new Node(key);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		return true;
	}

2.非递归实现

前面我们已经讲过,如果要在类外部访问私有成员变量_root,那就需要增添Getroot成员函数

所以一般在类内部实现递归函数,我们都用嵌套实现

这里有一个技巧非常绝妙,我们注意到_InsertR函数参数中,root用的是引用传参

所以找到正确的位置,传给root,直接就是parent->left或者parent->right的别名

充分利用了函数栈帧和引用相关知识,设计得很有意思

//二叉树插入(递归版本)
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

		if (root->_key < key)
		{
			return _InsertR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _InsertR(root->_left, key);
		}
		else
		{
			return false;
		}
	}

1.2.4 查

 二叉搜索树的查找

a. 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找

b. 最多查找高度次,走到到空,还没找到,这个值不存在。

总体思路和插入非常类似,相比而言,会比插入还要简单,相当于插入的节选代码,稍作修改,两

者甚至可以实现赋用

1.非递归实现

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

2.递归实现 

//二叉树查找(递归版本)
	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}
bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;
		if (root->_key == key)
			return true;

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

1.2.5 删

1.非递归实现

(1)情况分析

删除可以说是二叉搜索树模拟实现的核心,而且难度也最高

我们需要一步步进行分析

首先对于被删除的节点,我们需要考虑它的child,节点被删除,那child指向肯定就要作出调整

比如说删除16,那就可以直接删除即可

但是删除15,或者说删除18,就会是另外不同的情况,不可能删除15,然后把17和16两个结点都

弄丢的,所以我们需要先把不同的情况列出来,然后根据每种情况,提出不同的解决方案

总共有四种情况

列写思路也很简单,这个节点有没有孩子,无孩子,就像图中的16;有孩子的话有几个孩子?有一

个孩子,和有两个孩子是不同的情况

无孩子

a. 要删除的结点无孩子结点

有孩子(有几个孩子?)

b. 要删除的结点只有左孩子结点

c. 要删除的结点只有右孩子结点

d. 要删除的结点有左、右孩子结点 

但事实上,我们可以把a情况,放到b情况或者c情况中,用统一方法解决,所以总共可以算三种情

(2)解决方案 

对于第一种情况,直接删除即可,没有孩子需要处理,比如说16这个节点

对于第二,第三种情况,我们采取托孤的方式,让爷爷来带孙子,毕竟父亲被删除了,那爷爷自然

就可以空出一只手,我们让被删结点的爸爸,也就是爷爷,来照顾孙子,就可以解决只有左孩子和

右孩子的情况

比如15这个节点,我们把它删除,那它的父节点18自然会空出一只手,来领养17这个儿子

PS:

第一种情况,可以归到第二,三种情况处理,删除16,实际上就是让17领养孙子,孙子为空指针,没有差别,可以直接让17领养空结点

对于第三种情况,我们采取替代的方法,寻找被删除节点左树的最大节点或者右树的最小节点

为什么要用左树的最大节点或者右树的最小节点来替代呢?

1.因为我们要保证替代后,它仍然是一棵搜索二叉树

    左树的最大节点,或者右树的最小节点,都可以满足替代后,根大于左子树的所有结点,同时小于右子树的所有结点

2.替代后,删除原结点,可以转换为我们熟悉的前两种情况

比如说删除12,我们用它的左子树的最大结点来替代(其实就是把12这个值改成9)

然后把原来9这个结点直接释放,就实现了删除12结点的操作

9由于是从左子树里面挑出来的,所以比12右子树所有结点都要小

但又是左子树里面最大的,所以完全可以实现替代12之后,保持是一棵搜索二叉树

(类似我们也可以选择15——右树的最小结点来替代)

(3)领养代码实现 

现在讨论完不同情况后,我们就可以开始着手实现代码,在这期间,同样有很多需要注意的细节

首先,我们还是要找到这个要删除的结点,同样的,我们也要记录它的父亲

我们把第一种情况也放在第二种一同解决

但这里还需要注意两个点

第一,假如被删结点,没有父节点呢?也就是没有爷爷,此时就无法实现托孤了,对空指针解引用,会造成程序的崩溃

  

第二,被删结点的父节点不是随便领养的,还需要判断被删结点原来是父节点的右孩子还是左孩子,只有空出来的手才可以领养孩子

//如果该结点没有右孩子
if (cur->_right == nullptr)
{   
	//判断cur是不是根,如果是根,则没有parent
	if (cur == _root)
	{
		_root = cur->_left;
	}
	else
	{
		//并且该结点是父节点的右孩子,则让parent的右指针领养它的左孩子
		if (parent->_right == cur)
		{
			parent->_right = cur->_left;
		}
		else
		{
			parent->_left = cur->_left;
		}
	}
	delete cur;
}

//如果该结点没有左孩子
else if (cur->_left == nullptr)
{
	//并且该结点是父节点的右孩子,则让parent的右指针领养它的右孩子
	//父亲有可能为空,因为根没有parent
	if (cur == _root)
	{
		_root = cur->_right;
	}
	else
	{
		if (parent->_right == cur)
		{
			parent->_right = cur->_right;
		}
		else
		{
			parent->_left = cur->_right;
		}
	}
	delete cur;
}
(4)替代法代码实现 

接下来,我们需要处理最复杂的第三种情况,也就是用替代法,来删除有两个孩子的结点

这里采用右树最小节点的方式来替代,把右树最小节点,记为minRight

它的父节点指针,记作pminRight

只要左子树不为空,我们就一直往左移动,毕竟一棵搜索二叉树的最小节点,一定在最左边

找到后,将它的值和被删结点互换即可

但是还是有两点需要注意

第一,右树的最小节点一定没有左孩子,否则就不是最小节点

这不意味着右树最小节点一定没有右孩子,所以删除的时候,同样需要考虑领养问题

比如说删除12,则15就是右子树的最小节点,删除15后,还需要考虑18和17的领养问题

第二,pminRight不能初始值赋为空,假如没有进循环找minRight,则pminRight就始终为空,则后面领养的时候,空指针解引用,会导致程序崩溃

//该结点有两个孩子,找左子树的最大值,或者右子树的最小值
else
{   
	//pMinRight不能赋为空,否则没有进循环,空指针解引用报错
	Node* pMinRight = cur;
	Node* MinRight = cur->_right;
	//只要左子树不为空,就一直往左移动,找右树最小节点
	while (MinRight->_left)
	{  
		pMinRight = MinRight;
		MinRight = MinRight->_left;
	}
	
	//直接赋值
	cur->_key = MinRight->_key;
	//删除相应结点,需要考虑领养问题
	//右子树的最小节点,一定没有左孩子,但无法确定是否有右孩子
	if (pMinRight->_right == MinRight)
	{
		pMinRight->_right = MinRight->_right;
	}
	else
	{
		pMinRight->_left = MinRight->_right;
	}
	delete MinRight;
}

 将上述全部代码结合起来,就是整个删除的非递归代码

//二叉树删除
bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		//查找到要删除的结点,以及它的父节点
		else
		{
			//如果该结点没有右孩子
			if (cur->_right == nullptr)
			{   
				//判断cur是不是根,如果是根,则没有parent
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{
					//并且该结点是父节点的右孩子,则让parent的右指针领养它的左孩子
					if (parent->_right == cur)
					{
						parent->_right = cur->_left;
					}
					else
					{
						parent->_left = cur->_left;
					}
				}
				delete cur;
			}
			//如果该结点没有左孩子
			else if (cur->_left == nullptr)
			{
				//并且该结点是父节点的右孩子,则让parent的右指针领养它的右孩子
				//父亲有可能为空,因为根没有parent
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					if (parent->_right == cur)
					{
						parent->_right = cur->_right;
					}
					else
					{
						parent->_left = cur->_right;
					}
				}
				delete cur;
			}
			//该结点有两个孩子,找左子树的最大值,或者右子树的最小值
			else
			{   
				//pMinRight不能赋为空,否则没有进循环,空指针解引用报错
				Node* pMinRight = cur;
				Node* MinRight = cur->_right;
				//只要左子树不为空,就一直往左移动
				while (MinRight->_left)
				{  
					pMinRight = MinRight;
					MinRight = MinRight->_left;
				}
				
				//直接赋值
				cur->_key = MinRight->_key;
				//删除相应结点,需要考虑领养问题
				//右子树的最小节点,一定没有左孩子,但无法确定是否有右孩子
				if (pMinRight->_right == MinRight)
				{
					pMinRight->_right = MinRight->_right;
				}
				else
				{
					pMinRight->_left = MinRight->_right;
				}
				delete MinRight;
			}
			return true;
		}
	}
	return false;
}

2.递归实现

递归的总体思路,和非递归其实是一致的,没有什么区别

下面代码采取的是找出左树最大节点的方式,来实现替代法

不过递归如果要用引用传值来实现,也不是一件简单的事情

在替代法实现删除的时候,假如只有一个孩子,用引用传值来实现递归,和上面其实是一样的

但是如果是替代法,引用传值起到的作用其实不大

还有很关键的一点,由于是层层函数栈帧递归,我们要采用swap,而不是直接赋值

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

bool _EraseR(Node*& root, const K& key)
{   
    //假如递归到空节点,说明删除节点不存在,此时return false
	if (root == nullptr)
		return false;
    //递归删除节点
	if (root->_key < key)
	{
		return _EraseR(root->_right, key);
	}
	else if (root->_key > key)
	{
		return _EraseR(root->_left, key);
	}
	else
	{
		//找到相应的结点,并保存下来
		Node* del = root;
		//如果只有左孩子
		if (root->_right == nullptr)
		{
			root = root->_left;
		}
		//如果只有右孩子
		else if (root->_left == nullptr)
		{
			root = root->_right;
		}
		//如果有左右孩子
		else
		{
			Node* maxleft = root->_left;
			while (maxleft->_right)
			{
				maxleft = maxleft->_right;
			}

			//交换两个结点的值
			swap(root->_key, maxleft->_key);
			//再递归删除子结点
			return _EraseR(root->_left, key);
		}
		delete del;

		return true;
	}
}

1.2.6  整体代码展示

#pragma once
template <class K>
struct BSTreeNode
{
	BSTreeNode <K>* _left;
	BSTreeNode <K>* _right;
	K _key;

	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};
template <class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	BSTree() = default;  // 强制生成默认构造
	//拷贝构造
	BSTree(const BSTree<K>& t)
	{
		_root = Copy(t._root);
	}
	//赋值运算符重载
	BSTree<K>& operator=(BSTree<K> t)
	{
		swap(_root, t.root);
		return *this;
	}
	//析构函数
	~BSTree()
	{
		Destroy(_root);
	}
	//二叉树插入
	bool insert(const K& key)
	{
		//如果是第一个元素,直接就将它作为二叉树的根
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		//如果不是就考虑插入元素
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			//如果当前结点的值小于Key,往右移动
			if (cur->_key < key)
			{   
				parent = cur;
				cur = cur-> _right;
			}
			//如果当前结点的值大于Key,往左移动
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			//出现相同元素,直接报错
			else
			{
				return false;
			}
		}
		
		cur = new Node(key);
		if (parent->_key < key)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		return true;
	}
    //二叉树查找
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				cur = cur->_left;
			}
			else
				return true;
		}
		return false;
	}
	//二叉树删除
	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_key < key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_key > key)
			{
				parent = cur;
				cur = cur->_left;
			}
			//查找到要删除的结点,以及它的父节点
			else
			{
				//如果该结点没有右孩子
				if (cur->_right == nullptr)
				{   
					//判断cur是不是根,如果是根,则没有parent
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						//并且该结点是父节点的右孩子,则让parent的右指针领养它的左孩子
						if (parent->_right == cur)
						{
							parent->_right = cur->_left;
						}
						else
						{
							parent->_left = cur->_left;
						}
					}
					delete cur;
				}
				//如果该结点没有左孩子
				else if (cur->_left == nullptr)
				{
					//并且该结点是父节点的右孩子,则让parent的右指针领养它的右孩子
					//父亲有可能为空,因为根没有parent
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (parent->_right == cur)
						{
							parent->_right = cur->_right;
						}
						else
						{
							parent->_left = cur->_right;
						}
					}
					delete cur;
				}
				//该结点有两个孩子,找左子树的最大值,或者右子树的最小值
				else
				{   
					//pMinRight不能赋为空,否则没有进循环,空指针解引用报错
					Node* pMinRight = cur;
					Node* MinRight = cur->_right;
					//只要左子树不为空,就一直往左移动
					while (MinRight->_left)
					{  
						pMinRight = MinRight;
						MinRight = MinRight->_left;
					}
					
					//直接赋值
					cur->_key = MinRight->_key;
					//删除相应结点,需要考虑领养问题
					//右子树的最小节点,一定没有左孩子,但无法确定是否有右孩子
					if (pMinRight->_right == MinRight)
					{
						pMinRight->_right = MinRight->_right;
					}
					else
					{
						pMinRight->_left = MinRight->_right;
					}
					delete MinRight;
				}
				return true;
			}
		}
		return false;
	}
	//二叉树查找(递归版本)
	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}
	//二叉树插入(递归版本)
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
	//二叉树删除(递归版本)
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}
	//中序遍历
	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}
protected:
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
	    
		Node* newRoot = new Node(root->_key);
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);
		return newRoot;
	}
	void Destroy(Node*& root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;

		root = nullptr;
	}
	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;
		if (root->_key == key)
			return true;

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

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

		if (root->_key < key)
		{
			return _InsertR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _InsertR(root->_left, key);
		}
		else
		{
			return false;
		}
	}

		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;
			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _EraseR(root->_left, key);
			}
			else
			{
				//找到相应的结点,并保存下来
				Node* del = root;
				//如果只有左孩子
				if (root->_right == nullptr)
				{
					root = root->_left;
				}
				//如果只有右孩子
				else if (root->_left == nullptr)
				{
					root = root->_right;
				}
				//如果有左右孩子
				else
				{
					Node* maxleft = root->_left;
					while (maxleft->_right)
					{
						maxleft = maxleft->_right;
					}
		
					//交换两个结点的值
					swap(root->_key, maxleft->_key);
					//再递归删除子结点
					return _EraseR(root->_left, key);
				}
				delete del;
		
				return true;
			}
		}

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

2.二叉搜索树的应用

2.1 key模型

K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值

K模型在生活中其实很常见,比如门禁系统,车库系统识别车牌,检查一篇文章中单词拼写是否正

确都可能采用K模型

拿检查一篇文章中单词拼写是否正确这个例子来说,我们将词库中所有单词插入到一棵二叉搜索树

中,然后将文章中每一个单词,和这颗二叉搜索树进行比对,由于二叉搜索树的特性,能够可以迅

速确定该单词是否在这棵树里面,如果没有找到,就意味着单词拼写错误

2.2 key-value(kv)模型

每一个关键码key,都有与之对应的值Value,即键值对。该种方式在现实生活中非常常见

常见的应用,比如说中英文互译字典,或者通过电话号码+验证码来查询考试成绩

都是键-值互换的典型应用

想要实现key_value的二叉搜索树也不是难事,只要在节点中多加入一个参数value即可

其它都不需要改变,建树,找树的整个过程,依旧只和key相关

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)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}
	};

	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* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;
				}
			}

			cur = new Node(key, value);
			// 链接
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}

			return true;
		}

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

			return nullptr;
		}

		bool Erase(const K& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;

			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					// 删除
					// 1、左为空
					if (cur->_left == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
						}

						delete cur;

					} // 2、右为空
					else if (cur->_right == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}

						delete cur;
					}
					else
					{
						// 找右树最小节点替代,也可以是左树最大节点替代
						Node* pminRight = cur;
						Node* minRight = cur->_right;
						while (minRight->_left)
						{
							pminRight = minRight;
							minRight = minRight->_left;
						}

						cur->_key = minRight->_key;

						if (pminRight->_left == minRight)
						{
							pminRight->_left = minRight->_right;
						}
						else
						{
							pminRight->_right = minRight->_right;
						}

						delete minRight;
					}

					return true;
				}
			}

			return false;
		}


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

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

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}
	private:
		Node* _root = nullptr;
	};
}
void TestBSTree5()
{
	string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果",\
   "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉", "梨"};

	key_value::BSTree<string, int> countTree;
	for (auto str : arr)
	{
		//key_value::BSTreeNode<string, int>* ret = countTree.Find(str);
		auto ret = countTree.Find(str);
		if (ret == nullptr)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}

	countTree.InOrder();
}

3.二叉搜索树的相关oj题

1. 二叉树创建字符串

606. 根据二叉树创建字符串 - 力扣(LeetCode)

空括号有可能省略,也有可能不省略,需要根据图片分类讨论

总共有三种情况

1.左子树为空,右子树不为空,该括号不能省略

2.左子树,右子树都为空,该括号省略

3.左子树为空,右子树不为空,该括号省略

class Solution {
public:
    string tree2str(TreeNode* root) {
         if (root == nullptr)
           return "";
         
         //左子树为空,右子树不为空,该括号不能省略
         //左子树,右子树都为空,该括号省略
         //左子树为空,右子树不为空,该括号省略
         string str = to_string(root->val);
         //左子树不为空,或者左子树为空,右子树不为空,加括号
         if (root->left || root->right)
         {
             str+="(";
             str+=tree2str(root->left);
             str+=")";
         }
         //如果右子树不为空
         if(root->right)
         {
             str+="(";
             str+=tree2str(root->right);
             str+=")";
         }
         return str;
    }
};

2. 二叉树的分层遍历1

102. 二叉树的层序遍历 - 力扣(LeetCode)

由于C++有vector这个容器,因此二维数组实现起来非常方便

只需要创建vector<vector<int>> 类型的对象即可

同时创建一个levelSize的变量,也就是一层中有多少个节点

每次循环,除了将该层节点存入一个vector中,还将下一层的非空节点,存入栈中

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        //定义一个levelsize来确定现在出的是第几行,保证一行一行出
        queue<TreeNode*> q1;
        vector<vector<int>> vv;
        int Levelsize = 0;
        if (root)
        {
            q1.push(root);
            Levelsize = 1;
        }

        while (!q1.empty())
        {
            vector<int> v;
            while (Levelsize--)
            {
                //栈顶元素出队列
                TreeNode* front = q1.front();
                q1.pop();
                v.push_back(front->val);
                //如果该节点的左子树不为空,则把相应节点入队列
                if (front->left)
                {
                    q1.push(front->left);
                }
                //如果该节点的右子树不为空,则把相应节点入队列
                if (front->right)
                {
                    q1.push(front->right);
                }
            }
            //把对应的v压入vv中
            vv.push_back(v);
            Levelsize = q1.size();
        }
        return vv;
    }
};

3. 二叉树的分层遍历2

107. 二叉树的层序遍历 II - 力扣(LeetCode)

先实现层序遍历,再reverse一下即可

4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先

236. 二叉树的最近公共祖先 - 力扣(LeetCode)

本道题有两种思路来解决

第一种,两个节点假如分别在左子树和右子树,则它们的根节点必定是公共节点

我们先实现一个判断节点是否在一棵树里面的函数InTree

然后直接递归即可,如果两个节点都在右子树,则公共祖先不可能在左子树;同理,假如两个节点

都在左子树,则公共祖先不可能在右子树;途中假如其中一个节点是根节点,则它就是两者的公共

祖先

class Solution {
    bool InTree(TreeNode* root, TreeNode* x)
    {
       if (root == nullptr)
         return false;
       if (root == x)
         return true;
       return InTree(root->left,x) || InTree(root->right,x);
    }
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
       //方法一,两个节点假如分别在左子树和右子树,则它们的根节点必定是公共节点
       if (root == nullptr)
         return nullptr;
       //假如其中一个节点是根节点,则它就是两者的公共祖先
       if (root == p || root == q)
         return root;
       //p在左子树或者在右子树
       bool pInleft = InTree(root->left,p);
       bool pInright = !pInleft;

       //q在左子树或者在右子树
       bool qInleft = InTree(root->left,q);
       bool qInright = !qInleft;
       
       //如果两者都在左子树,则公共节点一定在左子树
       if (pInleft && qInleft)
          return lowestCommonAncestor(root->left,p,q);
       //如果两者都在右子树,则公共节点一定在右子树
       else if(pInright && qInright)
          return lowestCommonAncestor(root->right,p,q);
       else 
          return root;
    }
};

第一种思路虽然可以解决问题,但是时间效率上却不高

第二种思路在时间效率上就会高于第一种思路,但是实现起来却不太容易

总体思路为求出两个节点的路径,然后求两个路径的交点,该交点即为公共节点

如何求出从根节点到目标节点的路径呢?

我们采取先序遍历的方式,不管三七二十一都先把节点压入栈中,假如在某一个节点,在它的左子

树中,我们没有找到目标节点,在右子树中,也还是没找到目标节点,那该节点一定不在路径当

中,就可以把该节点直接pop掉

class Solution {
public:
    bool GetPath(TreeNode* root, stack<TreeNode*>& Path,TreeNode* x)
    {
        //只要节点不为空,就入栈
        if (root == nullptr)
            return false;
        Path.push(root);
        //假如遇到了目标节点,则剩下的就不需要再递归
        if (root == x)
          return true;
        //先序遍历
        if (GetPath(root->left,Path,x))
          return true;
        if (GetPath(root->right,Path,x))
          return true;
        //假如左子树,右子树都不存在该节点,则该节点必定不是该路径上的一点,需要pop掉
        Path.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        //法二:求出两个节点的路径,然后求两个路径的交点,该交点即为公共节点
        stack<TreeNode*> pPath;
        stack<TreeNode*> qPath;
        GetPath(root,pPath,p);
        GetPath(root,qPath,q);
        //假如两者长度不一致,则先调整为一致长度
        while (pPath.size() != qPath.size())
        {
            if (pPath.size() > qPath.size())
                pPath.pop();
            else
                qPath.pop();
        }
        //现在两者同样长度,则同时出栈,直到遇到公共祖先节点
        while (pPath.top() != qPath.top())
        {
            pPath.pop();
            qPath.pop();
        }
        return pPath.top();
    }
};

5. 二叉树搜索树转换成排序双向链表

二叉搜索树与双向链表_牛客题霸_牛客网 (nowcoder.com)

这道题的难度在于它只允许直接在原树上操作,需要充分理解递归,建立函数栈帧的过程,才能

对这道题的破解有一定思路

class Solution {
public:
    void _Convert(TreeNode* cur,TreeNode*& prev)
	{
        if(cur == nullptr)
		  return;
		_Convert(cur->left,prev);
		//核心代码
		cur->left = prev;
		//只有前驱节点指针不为空,才指向cur
		if(prev){
			prev->right = cur;
		}
		prev = cur;
		_Convert(cur->right,prev);
	}
    TreeNode* Convert(TreeNode* pRootOfTree) {
        //观察双向链表,可知要采取中序遍历的方式
		//为了调节节点的指向,我们定义一个前驱指针和后驱指针
		TreeNode* prev = nullptr;
		_Convert(pRootOfTree,prev);

		//已知根节点,找双向循环链表的头节点
		TreeNode* head = pRootOfTree;
		while (head && head->left)
		{
			head = head->left;
		}
		return head;
    }
};

6. 根据一棵树的前序遍历与中序遍历构造二叉树

105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode) 

利用类似快排的思想,先从前序遍历中确定根节点,再从中序遍历中找它的左子树和右子树

自下而上把它们链接起来

PS:中序遍历是左,根,右的结构,我们遍历先序遍历,是采用cur引用从前往后遍历,因此链接

的时候,也是先是左孩子链接,然后是右孩子链接 

class Solution {
public:
   TreeNode* _buildTree(vector<int>& preorder, \
       vector<int>& inorder,int& cur,int begini,int endi)
   {    
        if (begini > endi)
          return nullptr;
        TreeNode* root = new TreeNode(preorder[cur]);
        //分割出左右区间,从中序遍历序列中,找出前序遍历的根节点
        int rooti = begini;
        while (rooti <= endi)
        {
            if(preorder[cur] == inorder[rooti])
              break;
            else
              rooti++;
        }

        cur++;
        //[begini  rooti-1] rooti [rooti+1  endi]
        root->left = _buildTree(preorder,inorder,cur,begini,rooti - 1);
        root->right = _buildTree(preorder,inorder,cur,rooti+1,endi);
        return root;
   }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
       //类似快排的思想,划分子区间,再递归建树
       int cur = 0;
       return _buildTree(preorder,inorder,cur,0,inorder.size()-1);
    }
};

7. 根据一棵树的中序遍历与后序遍历构造二叉树

106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode) 

思路和上题一样,不过区别就在于后序遍历是左,右,根的结构,因此需要从后往前遍历,而且链

接的时候,要先右子树链接,再链接左子树

class Solution {
   TreeNode* _buildTree(vector<int>& inorder,\
      vector<int>& postorder,int& cur,int begini,int endi)
    {
         //如何区间长度小于1,则返回空指针
         if (begini > endi)
           return nullptr;
         //根据后序遍历序列,建节点
         TreeNode* root = new TreeNode(postorder[cur]);
         //从中序遍历序列中找到和根节点的值相同的节点
         //注意不是从头(下标0)开始找,而是从当前的区间的开始begini开始找
         int rooti = begini;
         while (rooti <= endi)
         {
             if (inorder[rooti] == postorder[cur])
               break;
             rooti++;
         }
         //更新根节点
         cur--;
         //找到后,就可以根据中序序列,将区间划分为三个部分
         //[begini,rooti - 1]rooti[rooti+1,endi]
         //要从右子树开始建树
         //因为后序遍历是左子树,右子树,根的结构,cur--得到的是右子树的根
         root->right = _buildTree(inorder, postorder,cur,rooti+1,endi);
         root->left = _buildTree(inorder, postorder,cur,begini,rooti - 1);

         return root;
    }
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0)  return nullptr;
        //后序遍历,从后往前才是根
        int cur = postorder.size() - 1;
        return _buildTree(inorder,postorder,cur,0,inorder.size() - 1);
    }
};

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

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

相关文章

【无标题】visual studio2019+Qt5.15.2+PCL1.12.1+vtk9.1.0+cmake3.22.2

安装VS------安装Qt-------安装PCL------安装cmake-----安装VTK-----环境配置 安装用了两天时间 1环境搭建 1.1 Visual Studio 2019 (PCL所需版本) https://my.visualstudio.com/Downloads?qvisual%20studio%202019&wt.mc_idomsftvscom~older-downloads 1.2 Qt安装 …

MySQL为什么要使用B+树做索引?MySQL索引存储模型推演,B+树在MySQL的落地形式

文章目录 一、什么是索引1、索引初体验2、索引图解3、索引类型 二、索引存储模型推演1、二分查找2、二叉查找树&#xff08;BST Binary Search Tree&#xff09;3、平衡二叉树&#xff08;AVL Tree&#xff09;&#xff08;左旋、右旋&#xff09;&#xff08;1&#xff09;平衡…

openGauss学习笔记-08 openGauss 数据类型

文章目录 openGauss学习笔记-08 openGauss 数据类型8.1 数值类型8.2 布尔类型8.3 字符类型8.4 二进制类型8.5 日期/时间类型8.6 几何类型8.7 网络地址类型8.8 位串类型8.9 文本搜索类型8.10 UUID数据类型8.11 JSON/JSONB类型8.12 HLL数据类型8.13 范围类型8.14 索引8.15 对象标…

C++笔记之虚析构函数的作用

C笔记之虚析构函数的作用 C中的虚析构函数用于确保通过基类指针删除派生类对象时&#xff0c;能正确调用派生类的析构函数&#xff0c;从而释放派生类对象所占用的资源。 在C中&#xff0c;析构函数用于释放对象所占用的资源&#xff0c;并进行清理操作。它与类的构造函数相对…

飞书ChatGPT机器人 – 打造智能问答助手

文章目录 前言环境列表1.飞书设置2.克隆feishu-chatgpt项目3.配置config.yaml文件4.运行feishu-chatgpt项目5.安装cpolar内网穿透6.固定公网地址7.机器人权限配置8.创建版本9.创建测试企业10. 机器人测试 前言 在飞书中创建chatGPT机器人并且对话&#xff0c;在下面操作步骤中…

spring cloud 之 Hystrix

Hystrix概述 Hystrix是一个供分布式系统使用&#xff0c;提供延迟和容错功能&#xff0c;保证复杂的分布系统在面临不可避免的失败是时&#xff0c;仍具有弹性。 当服务器A调用服务器B时&#xff0c;如果服务器B宕机&#xff0c;则服务器A不去调用。当服务器B在时间范围内未响…

Unity物理相关组件——2D关节

一&#xff1a;前言 关节组件用来模拟物体与物体之间的连接关系&#xff0c;必须依赖于刚体组件 二&#xff1a;Hinge Joint 2D 用于实现链条物体、钟摆等物体的模拟 ——Enable Collision&#xff1a;是否开启碰撞 ——Connected Rigid Body&#xff1a;关联的刚体&#xff0…

rocketmq使用mqtt协议

文章目录 前言一、安装rocketmq二、打包rocketmq-mqtt三、配置rocketmq-mqtt四、初始化操作五、启动六、测试 前言 rocketmq从4.9.3开始&#xff0c;可以兼容mqtt协议&#xff0c;需要安装编译一个rocketmq-mqtt工程&#xff0c;参考&#xff1a;https://rocketmq.apache.org/…

Java设计模式之结构型-享元模式(UML类图+案例分析)

目录 一、基本概念 二、UML类图 三、角色设计 四、案例分析 1、基本实现 2、游戏角色 五、总结 一、基本概念 享元模式是一种结构型设计模式&#xff0c;主要用于减少创建大量相似对象所占用的内存&#xff0c;它通过共享技术来有效支持大量细粒度的对象。 二、UML类图…

【动态规划算法】-简单多状态题型(11-18题)

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你…

微信小程序(二)

目录 1、input标签 一、表单绑定 1、数据绑定 2、输入获取 二、网络请求 1、介绍 2、注意 3、使用 4、基于Promise封装 三、自定义组件 1、创建 2、父向子组件通信 3、子向父组件通信 4、生命周期 四、vant weapp组件库 1、配置 2、使用 进入本章前的拓展&#…

宇宙的尽头是银行?聊聊在银行做软件测试的那些事

从一家工作了5年的软件公司的测试管理者跳槽到**银行做软件测试&#xff0c;短短2个月&#xff0c;对银行测试有了初步认识&#xff0c;总结和记录下来&#xff0c;加深个人的理解&#xff0c;同时也共享给各位。 银行作为大家的理财顾问&#xff0c;对金钱非常敏感&#xff0…

118、仿真-基于51单片机的直流电压电流控制系统设计(Proteus仿真+程序等)

方案选择 单片机的选择 方案一&#xff1a;AT89C52是美国ATMEL公司生产的低电压&#xff0c;高性能CMOS型8位单片机&#xff0c;器件采用ATMEL公司的高密度、非易失性存储技术生产&#xff0c;兼容标准MCS-51指令系统&#xff0c;片内置通用8位中央处理器(CPU)和Flash存储单元…

想冲嵌入式,学这套教程

今天&#xff0c;给大家带来一套非常前沿的视频教程&#xff01;有多前沿&#xff1f;诸君&#xff0c;请听我一一道来&#xff1a; 众所周知&#xff0c;现在嵌入式空前火热&#xff0c;随着AI驾驶、智能机器人、智能制造、智慧家居等智能软硬件的飞速发展&#xff0c;嵌入式已…

PLC工作者的工作待遇现状如何?

PLC工作者的工作待遇通常会受到多个因素的影响&#xff0c;包括地理位置、经验水平、行业需求以及个人技能等。 我这里刚好有嵌入式、单片机、plc的资料需要可以私我或在评论区扣个6 在一般情况下&#xff0c;PLC工作者通常拥有较高的技术水平和专业知识&#xff0c;因此他们…

关于自学\跳槽\转行做网络安全行业的一些建议

很好&#xff0c;如果你是被题目吸引过来的&#xff0c;那请看完再走&#xff0c;还是有的~ 为什么写这篇文章 如何自学入行&#xff1f;如何小白跳槽&#xff0c;年纪大了如何转行等类似问题 &#xff0c;发现很多人都有这样的困惑。下面的文字其实是我以前的一个回答&#…

pnpm改造替换npm

Q: 为什么要迁移pnpm&#xff1f; 相比于npm&#xff0c;pnpm有一些优势&#xff1a; 更快的安装速度: 在安装包时&#xff0c;pnpm使用了硬链接的方式&#xff0c;将已安装的包链接到新的目录下&#xff0c;而不是复制或下载包。这样&#xff0c;当你安装一个包的不同版本或者…

LLM - DataCollatorForLanguageModeling 样本生成 by transformers

目录 一.引言 二.生成样本 By API 1.样本处理样式 2.DataCollatorForLanguageModeling 2.1 样本准备 2.2 API 生成 三.生成样本 By DIY 1.样本准备 2.data_colloator 实现 3.使用自定义 data_colloator 四.总结 一.引言 前面我们讲了 Baichuan7B 的 lora 微调步骤&a…

2023年7月广州/惠州/深圳软考信息系统项目管理师报名

信息系统项目管理师是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目之一&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资…

矩阵的范数和特征值之间的关系

参考&#xff1a; linear algebra - Why is the norm of a matrix larger than its eigenvalue? - Mathematics Stack Exchange