C++进阶——二叉搜索树BST

C++进阶——二叉搜索树BST

其实应该是二叉树内容的进阶版本:
二叉树在前面C数据结构阶段已经讲过,本节取名二叉树进阶是因为:

  1. map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
  2. 二叉搜索树的特性了解,有助于更好的理解map和set的特性
  3. 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘
  4. 有些OJ题使用C语言方式实现比较麻烦
    既然有了c++这么好的工具不如就再重新加强一下二叉搜索树(BST)。

二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
而且有一个特有意思的特点就是 中序输出是升序的。

举个例子:
在这里插入图片描述
int a [] = {5,3,4,1,7,8,2,6,0,9};

二叉搜索树的功能介绍

无非就是增、删、查、改,但是其中最难得的其实是删除。

1.查找

在这里插入图片描述
2.插入(增)
插入的具体过程如下:
一. 树为空,则直接插入

在这里插入图片描述
二.树不为空,按二叉搜索树性质查找插入位置,插入新节点
在这里插入图片描述

在这里插入图片描述
三.二叉树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,
再来处理该结点的删除问题

二叉树的实现

创建节点

	template<class T>
	struct BSTNode
	{
		BSTNode(const T& key = T()) //构造函数
			: _pLeft(nullptr), _pRight(nullptr), _key(key)
		{}

		BSTNode<T>* _pLeft;
		BSTNode<T>* _pRight;
		T _key;
	};

该节点需要有构造函数。

	template<class T>
	class BSTree
	{
	public:
		typedef BSTNode<T> node;
		typedef node* Pnode; 
		/*typedef BSTNode<T>* Pnode;*/
	private:
		Pnode _root = nullptr;
	};

每一个节点都需要一个root(node*类型)去调用。

中序打印

为了方便之后数据的检测,需要做一个输出的小工具,因为二叉树严格遵守中序输出是升序的规律,所以我们就设计一个中序打印。

	void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
		
	void _InOrder(Pnode root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_pLeft);
			cout << root->_key << ' ';
			_InOrder(root->_pRight);
		}

由于我们平时提供这个接口时不会往里面输入参数,所以我们需要在封装一个函数帮我们输入参数,因此我们的封装了两个接口函数。

增删查改的实现

public:
		bool find(const T& key)
		{
			if (_root == nullptr)
				return false;
			Pnode cur = _root;
			while (cur)
			{
				if (cur->_key > key)
				{
					cur = cur->_pRight;
				}
				else if (cur->_key < key)
				{
					cur=cur->_pLeft;
				}
				else
				{
					return true;
				}
			}
			return false;
		}
		

查并不难,因为就是挨个遍历(因为二叉树的性质,左边比根小,右边比根大,这样便利可以减少很多时间的)
如果有就返回true,没有就返回false。

bool insert(const T& key)
		{
			if (_root == nullptr)
			{
				_root = new node(key);
				return true;
			}

			Pnode parent = nullptr;
			Pnode cur = _root;
			
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_pLeft;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_pRight;
				}
				else
				{
					return false;
				}
			}
			cur=new node(key);//单独new结点却不进行连接是不行的 ,以下代码不能屏蔽
			if (parent->_key > cur->_key)
			{
				parent->_pLeft = cur;
			}
			else
			{
				parent->_pRight = cur;
			}
			return true;
		}

插入也很简单,也是一个一个查找合适的位置,当已经存在了就返回false,
当找到空的时候就可以插入了。

bool erase(const T& key)
		{
			if (_root == nullptr) return false;
			Pnode cur = _root;
			Pnode parent = cur;
			while (cur)
			{
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_pRight;
				}
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_pLeft;
				}
				else
				{
					if (cur->_pLeft == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_pRight;
						}
						else
						{
							if (parent->_pLeft == cur)
							{
								parent->_pLeft = cur->_pRight;
							}
							else if (parent->_pRight == cur)
							{
								parent->_pRight = cur->_pRight;
							}
						}
						delete cur;
					}
					else if (cur->_pRight == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_pLeft;
						}
						else
						{
							if (parent->_pLeft == cur)
							{
								parent->_pLeft = cur->_pLeft;
							}
							else if (parent->_pRight == cur)
							{
								parent->_pRight = cur->_pLeft;
							}
						}
						delete cur;
					}
					else//左右两边都不为空
					{
						Pnode PRightMin = cur;
						Pnode RightMin = cur->_pRight;
						while (RightMin->_pLeft)
						{
							PRightMin = RightMin;
							RightMin = RightMin->_pLeft;
						}
						cur->_key = RightMin->_key;
						if (PRightMin->_pLeft == RightMin)
						{
							PRightMin->_pLeft = RightMin->_pRight;
						}
						else
						{
							PRightMin->_pRight = RightMin->_pRight;
						}
						delete RightMin;
					}
					return true;
				}
			}
			return false;
		}

删除就会难一点,因为要考察是否要移动子树。
首先你删除的如果是叶子节点,那直接删除就可以了不用考虑别的。
比如:
在这里插入图片描述
但是我要删除一个节点他不是叶子节点怎么办?
就需要考虑到领养机制了。(linux中的进程领养是不是很相似?其实也可以想象成一颗进程树)

单子树

当删除的节点在右边,而且有一颗右子树时:
如下图直接把右子树给父节点的右就可以了。
在这里插入图片描述
同理“左左”也是这个原理
在这里插入图片描述
左右和右左也是差不多的
因为只要是在父节点的左边就是比父节点小,在父节点的右边就是比父节点大。
在这里插入图片描述

双子树

最难的是双子树了该怎么办呢?
我只介绍一种方法(另一种类似),就是找到要删除节点的右树的最小节点(右子树的最左一个节点),然后让他替换掉要删除的节点,最后删除这个右树最小的节点。,但是当这个最小节点有右树怎么办呢?
就是把他的右树,托管给最小节点的父亲。

无右树
在这里插入图片描述
在这里插入图片描述

有右树
在这里插入图片描述
在这里插入图片描述

二叉搜索树的应用

  1. **K模型:**K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
    比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
    以单词集合中的每个单词作为key,构建一棵二叉搜索树
    在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

  2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生
    活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
    文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定
    单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对
    比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:
    <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较Key
    查询英文单词时,只需给出英文单词,就可快速找到与其对应的key

template<class K, class V>
struct BSTNode
 {
 BSTNode(const K& key = K(), const V& value = V())
 : _pLeft(nullptr) , _pRight(nullptr), _key(key), _Value(value)
 {}
 BSTNode<T>* _pLeft;
 BSTNode<T>* _pRight;
 K _key;
 V _value
 };
template<class K, class V>
class BSTree
 {
 typedef BSTNode<K, V> Node;
 typedef Node* PNode;
public:
 BSTree(): _pRoot(nullptr)
 {}
 // 同学们自己实现,与二叉树的销毁类似
 ~BSTree();
 // 根据二叉搜索树的性质查找:找到值为data的节点在二叉搜索树中的位置
 PNode Find(const K& key);
 bool Insert(const K& key, const V& value)
 {
 // ...
 
 PNode pCur = _pRoot;
 PNode pParent = nullptr;
 while (pCur)
 {
 pParent = pCur;
 if (key < pCur->_key)
 pCur = pCur->_pLeft;
 else if (key > pCur->_key)
 pCur = pCur->_pRight; // 元素已经在树中存在
 else
 return false;
 }
 // ...
 return true;
 }
 bool Erase(const K& key)
 {
 // ...
 return true;
 }
private:
 PNode _pRoot;
 };

二叉搜索树的性能分析

1.插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
2.对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的
深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
在这里插入图片描述
但是一个搜索二叉树退化成了一个单树枝树,那他的搜索价值就没有了,就完全成了一个链表,该如何改进呢?
就需要接下来讲的平衡二叉树(avl)和红黑树了。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
二叉搜索树,也叫二叉查找树(BST,Binary Search Tree),是一种特殊的二叉树。相对于普通的二叉树,它有一个特点:所有左子树的节点都比根节点小,所有右子树的节点都比根节点大。因此,它可以快速地进行查找、插入、删除等操作。 具体来说,二叉搜索树的定义如下: - 节点的左子树中所有节点的值都小于该节点的值。 - 节点的右子树中所有节点的值都大于该节点的值。 - 左右子树也都是二叉搜索树。 如下图所示,就是一个二叉搜索树的例子: ``` 8 / \ 3 10 / \ \ 1 6 14 / \ / 4 7 13 ``` 在这个树中,每个节点都满足左子树的节点值小于该节点的值,右子树的节点值大于该节点的值。比如,节点 3 的左子树是 1 和 6,右子树是 4 和 7,都满足要求。 二叉搜索树的主要操作包括查找、插入和删除。 查找操作可以通过递归或者循环实现。递归实现如下: ``` def search(root, val): if not root or root.val == val: return root if root.val > val: return search(root.left, val) else: return search(root.right, val) ``` 插入操作需要先查找到插入的位置,然后创建一个新节点插入到该位置: ``` def insert(root, val): if not root: return TreeNode(val) if root.val > val: root.left = insert(root.left, val) else: root.right = insert(root.right, val) return root ``` 删除操作比较复杂,需要考虑多种情况。如果要删除的节点只有一个子节点,直接将其子节点替换上来即可。如果要删除的节点有两个子节点,可以找到其右子树中的最小节点(或者左子树中的最大节点)来替换该节点,然后再删除该最小节点(或者最大节点)。 ``` def delete(root, val): if not root: return None if root.val == val: if not root.left: return root.right elif not root.right: return root.left else: # 找到右子树中的最小节点 p = root.right while p.left: p = p.left root.val = p.val root.right = delete(root.right, p.val) elif root.val > val: root.left = delete(root.left, val) else: root.right = delete(root.right, val) return root ``` 需要注意的是,在删除节点时,要保证删除后的树仍然是二叉搜索树

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tom王要coding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值