二叉搜索树,就这,就这啊。

什么是二叉搜索树?

二叉搜索树,可知道他是一个二叉树,并且肯定是为了有和搜索相关的地方,那他到底有什么不一样的呢?

github源码地址 : https://github.com/duchenlong/Cpp/tree/master/SearchTree/SearchTree

概念

所谓二叉搜索树,也叫二叉排序树。他或者是一颗空树,或者他一定具有以下的几个性质

  1. 如果他的左子树不为空,那么左子树的所有节点的值都小于根节点的值
  2. 如果他的右子树不为空,那么右子树的所有节点的值都大于根节点的值
  3. 他的左右子树也分别为二叉搜索树

在这里插入图片描述
类似于这样的一个结构,可以看出,每一个根节点都比他的左子树的值大,比他右子树的值小。

对于二叉搜索树而言,一开始的时候,他的根节点是一个nullptr,所以当我们把一个数组中的元素按照不同的顺序进行插入的时候,所得到的二叉搜索树的结构可能会不同。

因为我们插入的第一个节点的值就是树的根节点的值,以后所有的数据都是围绕这个根节点进行插入的。所以二叉搜索树不唯一
在这里插入图片描述
这也是一颗二叉搜索树

所以说,对于二叉搜索树的描述,我们可以用这样的一个结构体

//存储树中节点的结构体
template<class T>
struct TreeNode
{
	T _key;
	TreeNode<T>* _left;
	TreeNode<T>* _right;

	TreeNode(const T key = 0)
		:_key(key), _left(nullptr), _right(nullptr)
	{}
};

另外,我们可以很直观的看到,对于这棵二叉搜索树,将他按照中序的方式进行遍历,那么他的遍历的结果就是一个有序的数组。

	//中序遍历接口
	void _Inorder(TreeNode* root)
	{
		if (!root) return;

		_Inorder(root->_left);
		cout << root->_key << ' ';
		_Inorder(root->_right);
	}

接下来就是一些增删查的接口了

插入节点

在插入元素之前,我们需要明确,首先二叉搜索树是不能存在相同元素的,其次,我么每次所插入的位置一定是一个nullptr节点的位置。

所以在插入之前,我们需要先找到这个插入的位置,这时就可以利用二叉搜索树的特性,快速的查找到这个位置,当我们遍历到一个节点

  1. 如果当前节点的值 比 key 大,说明该数据要插在自己的 左子树
  2. 如果当前节点的值 比 key 小,说明该数据要插在自己的 右子树

然后直到找到nullptr节点的位置再进行插入操作,或者找到了这个节点(就不需要插入了)。

递归插入

递归插入的时候,我们需要注意,我们所插入的位置一定是一个nullptr节点,空指针是没有地址的,所以我们递归插入的时候,有两种传参方式

  1. 参数加上父亲节点的指针,然后将申请的节点和父亲节点连接起来
  2. 使用C++的引用,这样我们直接给nullptr节点申请空间的时候,就可以直接和他的父亲节点连接
	//递归插入
	bool InsertR(const T key)
	{
		return _InsertR(root, key);
	}
	
	//递归插入接口
	bool _InsertR(TreeNode*& root, const T key)
	{
		//根节点为 nullptr ,所以说待插入的位置就是根节点的位置
		if (!root)
		{
			root = new TreeNode(key);
			return true;
		}
		//如果根节点的值和key相同,就不需要插入
		if (root->_key == key) return true;

		//当前位置不是插入位置,判断需要递归插入左右子树哪里
		bool ret = root->_key > key ? _InsertR(root->_left, key) : _InsertR(root->_right, key);
		return ret;
	}

在这里插入图片描述

非递归

	//迭代插入
	bool Insert(const T key)
	{
		TreeNode* cur = root;
		TreeNode* parent = nullptr;

		//找到需要插入的位置,如果这个位置是真,说明还不是待插入的地方
		while (cur)
		{
			//先排除等于的情况
			if (cur->_key == key) return true;
			parent = cur;

			//如果当前节点的值 比 key 大,说明该数据要插在自己的 左子树
			//如果当前节点的值 比 key 小,说明该数据要插在自己的 右子树
			cur = cur->_key > key ? cur->_left : cur->_right;
		}

		cur = new TreeNode(key);
		//注意当前cur节点为nullptr,他是没有地址的,我们需要把新申请的节点和他的父亲节点手动连接
		parent == nullptr ? root = cur : (parent->_key > key ? parent->_left = cur : parent->_right = cur);
		return true;
	}

查找节点

对于查找,其实也是充分利用了二叉搜索树的特点,左子树都是比自己小的,右子树都是比自己大的

递归查找

	//递归查找
	TreeNode* FindR(const T key)
	{
		return _FindR(root, key);
	}
	
	//查找调用的接口
	TreeNode* _FindR(TreeNode*& root, const T key)
	{
		//根节点为空,或者根节点为Key,就直接返回root了
		if (!root || root->_key == key) return root;

		//没找到,就到子树的逻辑中找
		TreeNode* ret = root->_key > key ? _FindR(root->_left, key) : _FindR(root->_right, key);

		return ret;
	}

非递归

	//迭代查找
	TreeNode* Find(const T key)
	{
		TreeNode* cur = root;

		while (cur)
		{
			if (cur->_key == key) return cur;

			cur = cur->_key > key ? cur->_left : cur->_right;
		}
		//这里的时候,cur为nullptr ,说明没有找到,返回nullptr
		return nullptr;
	}

删除节点

删除节点就比较麻烦了,这时候就需要分情况去删除了,有着这样的三种情况:

  1. 需要删除的节点的左子树是nullptr,这个时候我们只需要连接他的右子树和父亲节点就可以了
    在这里插入图片描述
  2. 需要删除的节点的右子树是nullptr,这个时候我们只需要连接他的左子树和父亲节点
    在这里插入图片描述
  3. 需要删除的节点,他的左右子树都不为nullptr

对于第三种情况,他有着两种删除方式:

  1. 把删除节点的左子树移动到他右子树的最小的左节点的左子树位置

在这里插入图片描述
这样删除,需要面临的一个问题就是,他会使得这棵树的高度变得更高,所以说是不可取的。因为高度变高之后,可能就会失去二叉搜索树原本搜索变快的目的

  1. 把删除节点右子树的最小的左节点,和删除节点的值进行交换,然后删除这个最小的左节点
    在这里插入图片描述

递归删除

//递归删除接口
	bool _EarseR(TreeNode*& root,const T key)
	{
		//根节点为空,说明没有key的节点
		if (!root) return false;

		if (root->_key != key)
			return root->_key > key ? _EarseR(root->_left, key) : _EarseR(root->_right, key);

		//这里就是root->_key == key 的逻辑
		//此时就需要删除 root 节点
		TreeNode* del = root;

		if (!root->_right)	//需要删除的位置的右节点为空,根节点 = 他的左孩子
			root = root->_left;
		else if (!root->_left)	//需要删除位置的左节点为空,根节点 = 他的右孩子
			root = root->_right;
		else  //需要删除节点左右孩子均不为空
		{
			TreeNode* cur = root->_right;
			//找到最后一个左孩子
			while (cur->_left)
			{
				cur = cur->_left;
			}

			//此时需要把cur放到根节点的位置,然后删除cur节点
			root->_key = cur->_key;

			//这里可以变成在这个右子树中,删除cur节点
			return _EarseR(root->_right, cur->_key);

			/*
			//这样会让整个树变高的可能
			//将该位置的左孩子,插入到第一个右孩子的最后一个左孩子的位置
			TreeNode* left = root->_left;
			TreeNode* right = root->_right;

			//找到第一个右孩子的左孩子中,找到最后的空节点
			TreeNode* cur = right;
			while (cur)
				cur = cur->_left;

			//连接左孩子
			cur = left;
			root = right;
			*/
		}

		delete del;
		return true;
	}

非递归删除

对于非递归的情况,这个时候我们不能使用引用类型,因为引用会使得原本根节点的位置发生变化。

所以我们需要设置一个变量,这个变量用来记录父亲节点的位置,当这个变量为nullptr时,表示删除的节点是根节点

	//迭代删除
	bool Earse(const T key)
	{
		//先查找该节点,同时保留父亲节点的位置
		TreeNode* parent = nullptr;
		TreeNode* cur = root;

		while (cur)
		{
			if (cur->_key == key) break;

			parent = cur;
			cur = cur->_key > key ? cur->_left : cur->_right;
		}

		//如果没有这个节点,就不需要删除
		if (!cur) return false;

		TreeNode* del = cur;

		//此时需要删除cur节点,注意要删除的位置是根节点的情况
		if (!cur->_left)
		{
			if (!parent)
				root = cur->_right;
			else if (cur == parent->_left) //要删除的节点是左子树
				parent->_left = cur->_right;
			else if (cur == parent->_right)	//要删除的节点是右子树
				parent->_right = cur->_right;
		}
		else if (!cur->_right)
		{
			if (!parent)
				root = cur->_left;
			else if (cur == parent->_left)//要删除的节点是左子树
				parent->_left = cur->_left;
			else if (cur == parent->_right)//要删除的节点是右子树
				parent->_right = cur->_left;
		}
		else
		{
			TreeNode* minParent = cur;
			TreeNode* minLeft = cur->_right;
			
			//找到最后一个左孩子
			while (minLeft->_left)
			{
				minParent = minLeft;
				minLeft = minLeft->_left;
			}

			//此时需要把minLeft放到cur节点的位置,然后删除minLeft节点
			cur->_key = minLeft->_key;

			//删除minLeft节点,但是防止minLeft的右孩子存在
			if (minParent->_right == minLeft) //说明第一个右孩子没有左孩子
				minParent->_right = minLeft->_right;
			else	//删除这个左孩子,他的位置就让 左孩子的右孩子顶替
				minParent->_left = minLeft->_right;

			del = minLeft;//此时的minLeft变成了要删除的节点
		}

		delete del;
		return true;
	}

最后,如果我们一开始建立二叉搜索树的时候,给的数字都是有序的数字,那么就会出现一棵树,他只有左子树,或者只有右子树的情况,这个时候就需要我们去平衡二叉树了。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值