二叉搜索树的坑,你踩过几个?

一.什么是二叉搜索树

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
        1.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
        2.若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
        3.它的左右子树也分别为二叉搜索树

 

二.如何实现一个二叉搜索树

首先它是一个二叉树,那么该有的树的节点是必须有的。故先定义一个节点的结构体。

里面包含该节点的值和指向下一个节点的指针。注意:这里使用了模板,故这里的类型是

类名+模板参数

template <class k>
struct BStreenode
{
	struct BStreenode<k>* _left;
	struct BStreenode<k>* _right;
	k _val;

	//初始化
	BStreenode(const k& x)
		:_val(x)
	{
		_left = nullptr;
		_right = nullptr;
	}
};

然后在定义一个二叉搜索树的类,这里我们取名为BStree。里面的成员变量就是根节点root。对于一个类,少不了它的构造,拷贝,析构等函数。这里我们重点说说拷贝与析构的问题。首先这两个的实现用到递归与根节点,直接在拷贝和析构的函数上递归会发现参数不同,故我们这里要单独去调用一个函数来实现,并且对于使用者来说,他们只需要用到接口,对如何实现的并不关心,故我们把实现的函数放在private里面。(在递归版本上会经常用到)。再来说说拷贝里面的坑:必须深拷贝,不能浅拷贝,故我们要自己实现拷贝的函数,不然会在析构的时候导致二次析构。析构是按照后序来删除的,而拷贝是按照前序创建的。



template <class k>
class BStree
{
	typedef  BStreenode<k> Node;
public:
	//构造
	BStree()
		:root(nullptr)
	{

	}
    //拷贝
	BStree(const BStree<k>& t)
	{
		root = Copy(t.root);
	}
	//析构
	~BStree()
	{
		Destory(root);
	}
	
private:
	Node* Copy(Node* _root)
	{
		if (_root == nullptr)
			return nullptr;

		Node* copyroot = new Node(_root->_key);
		copyroot->_left = Copy(_root->_left);
		copyroot->_right = Copy(_root->_right);
		return copyroot;
	}

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

	
	Node* root;
};

一.插入功能

思想:如果根节点为空,则可以直接插入。当需要插入的值大于该节点所对应的值时,往右边走;当需要插入的值小于该节点所对应的值时,往左边走。当走到的节点为空时,就是符合条件的位置,可以插入。注意有坑:这里每走一步时要记录parent的位置,不然你创建的符合条件的位置并不能和这棵树链接起来。在链接的时候,还要注意当前位置的值时大于还是小于parent的值,以此来判断时连接到parent的左边还是右边。

//插入
	bool insert(const k& x)
	{
		if (root == nullptr)
		{
			root = new Node(x);
			return true;
		}
		Node* cur = root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_val > x)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_val < x)
			{
				parent = cur;
				cur = cur->_right;
			}
			//不能插入相同的
			else
			{
				return false;
			}
		}
		cur= new Node(x);
		if (parent->_val > x)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		return true;
	}

二.查找功能

思想:从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。相同就找到了,当走到空时就说明没有找到。查找比较好,没有坑。

bool find(const k& x)
	{
		Node* cur = root;
		while (cur)
		{
			if (cur->_val > x)
			{
				cur = cur->_left;
			}
			else if(cur->_val < x)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}
		return false;
	}

 三.删除功能(坑最多)

为什么说删除功能的坑很多呢?

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点

我们可以发现情况a其实可以归类与b或c里面。通过上面的思想我们可以把删除的情况总结下来:

对应情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点---直接删除
对应情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点---直接删除
对应情况d:在它的右子树中寻找中序下的第一个结点(右子树的最小值),或者找左子树的最大值。用它的值填补到被删除节点中,再来处理该结点的删除问题---替换法删除

 

 

bool erase(const k& x)
	{
		Node* parent = nullptr;
		Node* cur = root;
		while (cur)
		{
			if (cur->_val > x)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_val < x)
			{
				parent = cur;
				cur = cur->_right;
			}
			else//找到要删除的节点了
			{
				
				if (cur->_right == nullptr)//右为空
				{
					if (cur == root)//root节点就是要删除的节点
					{
						 root=cur->_left;
					}
					else
					{
						if (parent->_right == cur)//要注意需要删除的节点时parent的左还是右
						{
							parent->_right = cur->_left;
						}
						else
						{
							parent->_left = cur->_left;
						}
					}
				}
				else if (cur->_left == nullptr)//左为空
				{
					if (cur == root)//root节点就是要删除的节点
					{
						root = cur->_right;
					}
					else
					{
						if (parent->_right == cur)//要注意需要删除的节点时parent的左还是右
						{
							parent->_right = cur->_right;
						}
						else
						{
							parent->_left = cur->_right;
						}
					}
				}
				else //两个都不为空,方法:替换法:找该节点的左子树的最大值,或者右子数的最小值
				{
					//找替代节点
					Node* parent = cur;
					Node* leftmax = cur->_left;
					while (leftmax->_right)
					{
						parent = leftmax;
						leftmax = leftmax->_right;
					}
					swap(leftmax->_val, cur->_val);
					//可能leftmax是有左孩子的
					if (parent->_left == leftmax)
					{
						parent->_left = leftmax->_left;
					}
					else
					{
						parent->_right = leftmax->_left;
					}
					cur = leftmax;

				}
				delete cur;
				return true;
			}
			
		}
		return false;
	}

四.中序遍历

这是一颗二叉搜索树,我们经过中序(左根右)遍历后:1 3 4 6 7 8 10 13 14 是一个升序。

它与拷贝,析构一样,需要调用函数,真正执行的函数在private里面。

void _Inorder(Node* _root)
	{
		if (_root == NULL)
		{
			return;
		}
		_Inorder(_root->_left);
		cout << _root->_val << " ";
		_Inorder(_root->_right);
	}

 三.二叉搜索树的性能分析

1.对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多

2.如果二叉排序树是平衡的,则n个节点的二叉排序树的高度为,其查找效率为,近似于折半查找。如果二叉排序树完全不平衡,则其深度可达到n,查找效率为O(n),退化为顺序查找。一般的,二叉排序树的查找性能在到O(n)之间。因此,为了获得较好的查找性能,就要构造一棵平衡的二叉排序树。

 四.总结/感悟

我这里就不给完整代码了,将上诉代码copy在一起就ok啦。二叉树与递归往往两者是一起出现的。这种类型的题往往难度也很大(反正我是觉得遇到这样的题,脑子就不够用)。脑子不好使的话,最好的办法就是自己换递归展开图。


 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值