【C++】二叉搜索树

一、概念和应用

1.1 概念

二叉搜索树也叫二叉排序树,它可以是一个空树。主要有以下特点:

  • 如果左子树不为空,则所有左子树的节点的值小于根节点的值
  • 如果右子树不为空,则所有右子树的节点的值小于根节点的值
  • 它的左右子树也都是二叉搜索树

下图就是一个二叉搜索树:
在这里插入图片描述
二叉搜索树有一特点,当它中序遍历时就会形成升序

以上图为例:
1 3 4 6 7 8 10 13 14

1.2 应用

二叉搜索树有两种模型:

  • K模型:只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。比如判断单词是否拼写正确
  • KV模型:每一个关键码key,都有与之对应的值Value,通过一个快速的找到另一个。比如汉英词典的对应关系

二、实现

2.1 创建节点和私有成员变量

创建一个节点的结构体,每个节点包含左指针、右指针和该节点的数据。然后对它进行初始化,每个节点的指针域指向空,数据域是传过来的参数。

template<class K>
struct BSTreeNode
{
   
	typedef BSTreeNode<K> Node;
	Node* _left;
	Node* _right;
	K _key;

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

私有成员变量是为_root,给它缺省值,方便直接给初始化列表构造

Node* _root = nullptr;

2.2 K模型二叉搜索树

2.2.1 中序遍历

先写中序遍历方便验证后续的代码是否正确,如果后面的代码没问题,那么中序遍历后是一个升序。这里要注意的一点是,因为是在类里写,每个成员函数都有隐藏的this指针,而递归写法是要带参数的,所以这里写中序遍历时,要有一个子函数来递归,后面的递归写法也要这样。

//中序遍历
void _InOrder(Node* root)//子函数要递归,需要有参数
{
   
	if (root == nullptr)
	{
   
		return;
	}
	_InOrder(root->_left);
	cout << root->_key << " ";
	_InOrder(root->_right);
}
void InOrder()
{
   
	_InOrder(_root);//给子函数传参
	cout << endl;
}

2.2.2 查找 - 循环

从根节点开始查找,找到了返回true,找不到返回false。定义一个临时变量cur,要找的值比当前节点的值大,进入右子树;反之,进入左子树,直到找到为止。最多查找树的高度次。

//查找
bool Find(const K& key)
{
   
	Node* cur = _root;//刚开始为根节点
	while (cur)
	{
   
		if (cur->_key < key)//key的值比当前节点的值大
		{
   
			cur = cur->_right;//往右边走
		}
		else if (cur->_key > key)//key的值比当前节点的值小
		{
   
			cur = cur->_left;//往左边走
		}
		else//key的值与当前节点的值相等
		{
   
			return true;//找到了
		}
	}
	return false;//没找到
}

2.2.3 插入- 循环

插入数据分为两种情况,一种是空树,另一种是非空树。是空树,就将当前位置(根节点)构成一个节点,然后返回true。不是空树,定义一个临时变量cur去找与插入的数据有没有相同的节点的值,如果相同,则返回false,因为不能有重复的节点;不相同,此时cur的位置指向空,则当前位置构成一个节点,然后与树连接起来。这里还要多定义一个临时变量parent,作用是记录cur上一次的位置,到最后cur形成节点的时候,与parent连接就是与树连接。

//插入
bool Insert(const K& key)
{
   
	Node* cur = _root;//刚开始为根节点
	//如果根节点为空的情况--空树
	if (cur == nullptr)
	{
   
		_root = new Node(key);//在根节点插入数据
		return true;
	}
	//非空树
	Node* parent = nullptr;//cur的上一次位置,便于连接
	while (cur)
	{
   
		if (cur->_key < key)//key的值比当前节点的值大
		{
   
			parent = cur;//记录上一次cur的位置
			cur = cur->_right;//往右边走
		}
		else if (cur->_key > key)//key的值比当前节点的值小
		{
   
			parent = cur;//记录上一次cur的位置
			cur = cur->_left;//往左边走
		}
		else //要插入的值不能与树中的某个节点的值相等
		{
   
			return false;
		}
	}
	//cur走到空,要插入数据
	cur = new Node(key);//先转换为一个节点
	if (parent->_key < key)//如果要插入的值大于上一个节点值
	{
   
		parent->_right = cur;//插入的节点在上一个节点的右边
	}
	else//同理,小于的情况
	{
   
		parent->_left = cur;//插入的节点在上一个节点左边
	}
	return true;
}

2.2.4 删除- 循环

删除某个节点与前面的步骤一样,定义两个临时变量cur和parent,找要删除节点的位置。找不到,则要删除的节点不存在,返回false;找到了,则要删除该节点。删除节点分为三种情况:左为空,右为空,两边都不为空。

左为空:
在这里面也有两种情况,当前位置cur是根节点,不是根节点。如果是根节点,则把根节点的位置移到右子树去;不是根节点,就将当前位置的右子树与上一个位置的节点parent连接。但是这里要区分一下cur是parent的左边还是右边,然后再连接。

右为空:
与左为空类似,连接的是Cur的左子树。

两边都不为空:
采用的是替换法删除,即将要删除的节点的值与它的左子树或者右子树中的某个节点的值进行替换,然后再把替换后的节点删除,与其他节点连接。

那么有个问题,怎么知道跟哪个节点交换呢?这里要考虑的是,怎么做到交换后的值还能保持它的左子树的所有值比它小,右子树比它大。根据这个规律,可以发现能够进行替换的节点是当前节点右子树的最小节点,或者是左子树的最大节点。右子树中的最小节点即最左节点,左子树中的最大节点即最右节点,这里选择右子树的最左节点来进行替换。
在这里插入图片描述

//删除
bool Erase(const K& key)
{
   
	Node* cur = _root;//刚开始为根节点
	Node* parent = nullptr;//cur的上一次位置,便于连接
	while (cur)
	{
   
		if (cur->_key < key)//key的值比当前节点的值大
		{
   
			parent = cur;//记录上一次cur的位置
			cur = cur->_right;//往右边走
		}
		else if (cur->_key > key)//key的值比当前节点的值小
		{
   
			parent = cur;//记录上一次cur的位置
			cur = cur->_left;//往左边走
		}
		else//找到要删除的节点位置cur - 3种情况
		{
   
			if (cur->_left == nullptr)//左为空,右有节点
			{
   
				if (cur == _root)//cur为根节点
				{
   
					_root = cur->_right;//根到右边
				}
				else//cur非根节点
				{
   
					//判断cur是上一次位置的哪边
					if (parent->_left == cur)//左边连接cur
					{
   
						parent->_left = cur->_right;
					}
					else//右边连接cur
					{
   
						parent->_right = cur->_right;
					}
				}
				delete cur;//清理cur
				return true;
			}
			else if (cur->_right == nullptr)//右为空,左有节点
			{
   
				if (cur == _root)//cur为根节点
				{
   
					_root = cur->_left;//根到左边
				}
				else//cur非根节点
				{
   
					//判断cur是上一次位置的哪边
					if (parent->_left == cur)//左边连接cur
					{
   
						parent->_left = cur->_left;
					}
					else//右边连接cur
					{
   
						parent->_right = cur->_left;
					}
				}
				delete cur;//清理cur
				return true;
			}
			else//两边都有节点--替换法删除
			{
   
				Node* rightMin = cur->_right;//开始找右边最小
				Node* rightMinParent = cur;//rightMin的上一次位置
				while (rightMin->_left)//右子树最小即最左
				{
   
					rightMinParent = rightMin;//记录rightMin的上一次位置
					rightMin = rightMin->_left;//找到最左为止
				}
				//出循环,此时rightMin为最左
				cur->_key = rightMin->_key;//替换要删除节点的值
				//判断rightMin在上一个节点的哪边
				if (rightMinParent->_left == rightMin)
				{
   
					rightMinParent->_left = rightMin->_right;
				}
				else
				{
   
					rightMinParent->_right = rightMin->_right;
				}
				//然后把rightMin位置的节点删除
				delete rightMin;
				return true;
			}
		}
	}
	return false;//没找到
}

2.2.5 查找 - 递归

如果root为空,没找到,返回false;不为空,找到了,返回true

//查找
bool _FindR(const K& key, Node* root)
{
   
	if (root == nullptr)
	{
   
		return false;//找不到
	}
	if (root->_key < key)
	{
   
		return _FindR(key, root->_right);//进入右子树
	}
	else if (root->_key > key)
	{
   
		return _FindR(key, root->_left);//进入左子树
	}
	else
	{
   
		return true;//找到了
	}
}
bool FindR(const K& key)
{
   
	return _FindR(key, _root);
}

2.2.6 插入- 递归

插入的数据不能与树中的数据相同,当root为空时,说明没有相同的数据,则插入新数据,返回true。有相同的数据返回false。注意:还没有完全结束,要实现每个节点之间的连接,必须在参数列表里加上引用。 因为插入函数的返回类型是bool,返回给上一层时只是正确与不正确的问题,并没有实现节点的连接,加上引用root每次递归都是它自己,所以能够实现连接。不加引用只能把返回类型改成Node * 节点指针类型。还有一点,递归的话不需要循环那样得有一个parent来记录上一次的位置,因为从哪里递归下来、返回回去,都是确定好的,即从当前位置往左子树的方向下去,返回就从左子树的位置回去,右边一样的。重要的是这个引用,因为它关系到的是能不能实现连接的问题。

//插入
bool _InsertR(const K& key, Node*& root)//别忘加引用
{
   
	if (root == nullptr)
	{
   
		root = new Node(key);//插入新节点
		return true;
	}
	if (root->_key < key)
	{
   
		return _InsertR(key, root->_right);//进入右子树
	}
	else if (root->_key > key)
	{
   
		return _InsertR(key, root->_left);//进入左子树
	}
	else
	{
   
		return false;//有重复数据
	}
}
bool InsertR(const K& key)
{
   
	return _InsertR(key, _root);
}

2.2.7 删除- 递归

大体思路与前面的循环类似,先找到要删除的节点,找不到返回false,找到了,有三种情况,左为空、右为空、两边都有节点。与前面的相比递归的代码简化了不少。但是要注意下面几个地方,当找到要删除的节点时,定义一个临时变量记录当前root的位置,清理的时候清理的就是这个临时变量;然后是交换数据,不能是循环那里的覆盖,必须是真正的交换,因为后面还有递归还要找到这个key值,然后进行同样的步骤,直到删除完返回即可。

//删除
bool _EraseR(const K& key, Node*& root)//别忘加引用
{
   
	if (root == nullptr)
	{
   
		return false;//没找到要删除的节点
	}
	if (root->_key < key)
	{
   
		return _EraseR(key, root->_right);//进入右子树
	}
	else if (root->_key > key)
	{
   
		return _EraseR(key, root->_left);//进入左子树
	}
	else
	{
   
		Node* del = root;//记录当前位置~~
		if (root->_left == nullptr)//左为空
		{
   
			root = root->_right;//它的右子树与当前节点连接
		}
		else if (root->_right == nullptr)//右为空
		{
   
			root = root->_left;//它的左子树与当前节点连接
		}
		else//两边都有节点
		{
   
			Node* rightMin = root->_right;
			while (rightMin->_left)
			{
   
				rightMin = rightMin->_left;
			}
			swap(root->_key, rightMin->_key);//交换数据
			return _EraseR(key, root->_right);//进入右子树~~
		}
		delete del;//清理
		return true;
	}
}
bool EraseR(const K& key)
{
   
	return _EraseR(key, _root);
}

2.2.8 构造 拷贝构造 析构 赋值重载

//强制生成默认构造
BSTree() = default;

//拷贝构造
BSTree(const BSTree<K>& t)
{
   
	_root =  Copy(t._root);
}
Node* Copy(Node* root)
{
   
	if (root == nullptr)
	{
   
		return nullptr;
	}
	Node* newNode = new Node(root->_key);
	newNode->_left = Copy(root->_left);
	newNode->_right = Copy(root->_right);
	return newNode;
}

//赋值重载
BSTree<K>& operator=(BSTree<K> t)
{
   
	swap(_root, t._root);
	return *this;
}

//析构
~BSTree()
{
   
	Destroy(_root);
}
void Destroy(Node* root)
{
   
	if (root == nullptr)
	{
   
		return;
	}
	Destroy(root->_left);
	Destroy(root->_right);
	delete root;
}

2.3 KV模型二叉搜索树

以下几点与K模型有所不同,其他一样。

2.3.1 创建节点结构体

在前面的基础上多了个模板参数V:

template<class K, class V>
struct BSTreeNode
{
   
	typedef BSTreeNode<K, V> Node;
	Node* _left;
	Node* _right;
	K _key;
	V _value;

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

2.3.2 查找

返回类型变为节点指针类型,才能判断是哪个数据

//查找
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;
}

2.3.3 插入

形参多个参数 value

//插入
bool Insert(const K& key, const V& value)
{
   
	Node* cur = _root;
	if (cur == nullptr)
	{
   
		_root = new Node(key, value);
		return true;
	}
	Node* parent = nullptr;
	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;
}

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-02-19 21:44:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-19 21:44:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-19 21:44:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-19 21:44:01       20 阅读

热门阅读

  1. CSS transition(过渡效果)详解

    2024-02-19 21:44:01       25 阅读
  2. 【STM32】重定向printf函数

    2024-02-19 21:44:01       30 阅读
  3. podspec中引用父级目录的功能代码

    2024-02-19 21:44:01       28 阅读
  4. python 将普通文件转换为ts文件,用udp-ts 发送

    2024-02-19 21:44:01       31 阅读
  5. TP-LINK路由器的IPTV功能测试验证

    2024-02-19 21:44:01       139 阅读
  6. Linux 目录磁盘满了,怎么查找大文件

    2024-02-19 21:44:01       22 阅读
  7. python函数的定义和调用

    2024-02-19 21:44:01       28 阅读
  8. LeetCode 2824.统计和小于目标的下标对数目

    2024-02-19 21:44:01       29 阅读
  9. 力扣:139. 单词拆分

    2024-02-19 21:44:01       27 阅读