[数据结构] 二叉搜索树的详解实现

概念

二叉搜索树(Binary Search Tree,BST),也称为二叉查找树或二叉排序树,是一种特殊的二叉树,其中节点的值按照一定规律排列。具体来说 ,对于任意一个节点,其左子树的节点键值均小于根节点的键值;右子树的节点键值均大于根节点的键值。

即满足以下性质:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

在这里插入图片描述




实现

架构

下面的代码是二叉搜索树的代码实现框架,省去了具体功能的实现。

#pragma once

namespace aiyimu
{
	template<class K>
	// 二叉树节点
	struct BSTreeNode
	{};

	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		// 尾插
		bool insert(const K& key)
		{}

		// 中序遍历
		void inOrder()
		{}

		// 查找 值为key的节点
		bool Find(const K& key)
		{}

		// 删除 值为key的节点
		bool Erase(const K& key)
		{}

		// ---------------------------------------- 递归写法 ---------------------------------------
		// 查找
		bool FindR(const K& key)
		{}

		bool InsertR(const K& key)
		{}

		bool EraseR(const K& key)
		{}


		// 默认成员函数
		// 构造
		BSTree() = default;//C++: 强制编译器生成默认构造

		// 拷贝构造
		BSTree(const BSTree<K>& bst)
		{}

		// 析构
		~BSTree()
		{}

		// 赋值重载
		BSTree<K>& operator=(BSTree<K> bst)
		{}

		// 不能访问_root
	private:
		// 拷贝
		Node* _Copy(Node* root)
		{}

		// 销毁BST
		void _Destory(Node*& root)
		{}

		bool _FindR(Node* root, const K& key)
		{}

		bool _InsertR(Node* root, const K& key)
		{}

		bool _EraseR(Node* root, const K& key)
		{}

		void _inOrder(Node* root)
		{}
	private:
		Node* _root = nullptr;
	};
}



BSTreeNodea(节点)

template<class K>
// 二叉树节点
struct BSTreeNode
{
	// 成员变量
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
	K _key;


	// 构造函数
	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};
  • _left 表示指向当前节点的左子节点的指针
  • _right 表示指向当前节点的右子节点的指针
  • _key 表示当前节点所存储的键值(key),即二叉搜索树中的排序关键字。

_left 和 _right 成员变量的类型均为 BSTreeNode*,即指向某个类型为 K 的节点的指针。这是因为二叉树节点本身也是一种自定义数据类型,其成员变量也可以是指向其他节点的指针。

构造函数

  1. 首先将 _left 和 _right 成员变量初始化为 nullptr,表示当前节点暂时没有左子节点和右子节点。
  2. 然后将 _key 成员变量初始化为传入的参数 key,表示当前节点存储的键值就是 key。


BSTree

框架

增删查 – 循环写法

insert(尾插)

在这里插入图片描述

  1. 插入时如果此时无节点,直接创建新节点作为_root,返回true
  2. 首先查找key值需要插入的位置,如果key值大于节点值,向右继续查找;如果key值小于节点值,向左继续查找
  3. 如果插入的key值已存在,则直接返回false(二叉搜索树没有重复的元素,这一步去重),最后的cur即为待插入的位置
  4. 当找到要插入的cur时,最后将key作为cur的子节点插入
  5. 具体代码都有相应的注释做解释
// 尾插
bool insert(const K& key)
{
	// 如果此时无节点,直接创建新节点作为_root,返回true
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;

	// 查找要插入的位置
	while (cur)
	{
		// 如果key大,则向右插入
		// 反之key小向左插入
		if (key > cur->_key)
		{
			//parent存cur:代表改变后的cur的父节点
			parent = cur;
			cur = cur->_right;
		}
		else if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		// 元素重复则返回false
		else
		{
			return false;
		}
	}
	
	// 此时找到了待插入的位置cur
	// 将key插入作为cur的左/右子节点
	
	//创建节点
	cur = new Node(key);
	if (key > parent->_key)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	return true;	//插入成功返回true
}

inOrder(遍历)

在上文框架重可以知道,inOrder()是public访问,而_inOrder()是private访问

好处:

  1. 封装性: 将具体实现细节封装在私有函数中,可以保证外部用户无法直接访问和修改数据结构,提高程序的封装性和安全性。
  2. 灵活性: 将遍历操作和遍历入口分离开来,可以在不暴露内部实现的前提下对外提供更多的功能接口,比如后序遍历、层序遍历等。
  3. 可读性: 通过将不同的功能或逻辑分别实现在不同的函数中,可以大大提高代码的可读性和可维护性。
  • 在inOrder()中,直接调用_inOrder()并传入_root成员函数
public:
void inOrder()
{
	_inOrder(_root);
	cout << endl;
}
  • 中序遍历符合 左 中 右 的顺序,当root为空时返回,按照顺序递归即可
private:
void _inOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}

	_inOrder(root->_left);
	cout << root->_key << " ";
	_inOrder(root->_right);
}

Find(查找)

  1. 查找所执行的操作和插入时一致,遍历二叉搜索树,如果key大向右找,key小想左找即可。
bool Find(const K& key)
{
	Node* cur = _root;

	// 遍历二叉搜索树查找key
	while (cur)
	{
		// 如果key大,则向右找
		if (key > cur->_key)
		{
			cur = cur->_right;
		}
		// key小,向左找
		else if (key < cur->_key)
		{
			cur = cur->_left;
		}
		// 找到了
		else
		{
			return true;
		}
	}
	// 退出循环,找不到
	return false;
}

Erase(删除)

首先查找待删除的节点,如果找不到,直接返回false
在这里插入图片描述

找到节点后,执行删除操作,一共有三种情况:

  1. 待删除节点左树为空
  2. 待删除节点右树为空
  3. 待删除节点左右都不为空

下图做解释:

当左树为空待删除的节点是根节点

在这里插入图片描述

当左树为空待删除的节点不是根节点

在这里插入图片描述

当右树为空

当右树为空的操作思路和左树为空时一致,按照左树的思路写即可。

当左右树都不为空

在这里插入图片描述

另外:在这段代码中,由于是在查找 cur 节点并删除其子树的过程中,无论 cur 是否是根节点都不会影响查找和删除的过程。

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

	while (cur)
	{
		// 找到要删除的节点
		if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		// 执行删除操作
		else
		{
			// 三种情况
			// 1、左为空
			// 2、右为空
			// 3、左右都不为空
			
			// 1. 左为空时
			if (cur->_left == nullptr)
			{
				// 当要删除的为根时,parent会出问题,单独写这种情况
				if (cur == _root)
				{
					_root = cur->_right; // 直接将根改为右节点(左为空)
				}
				// 判断 cur 在 parent 的左侧还是右侧,然后将 parent 对应的子节点指向 cur 的右子节点。
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}

				// 清理掉 cur 的内存空间
				delete cur;
				cur = nullptr;
			}
			// 2. 右为空时
			else if (cur->_right == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}

				delete cur;
				cur = nullptr;
			}
			// 3. 左右都不为空
			else
			{
				// 找到右子树的最小节点替换
				Node* minParent = cur;
				Node* min = cur->_right;

				while (min->_left)
				{
					minParent = min;
					min = min->_left;
				}

				swap(cur->_key, min->_key);
				if (minParent->_left == min)
					minParent->_left = min->_right;
				else
					minParent->_right = min->_right;

				delete min;
			}
			return true;
		}
	}

	return false;
}


默认成员函数

构造

这里使用了 C++11 中的新特性:= default 它表示对于该函数,我们采用编译器默认生成的实现方式,而不需要手写构造函数的实现代码。

直接用默认构造进行成员变量的初始化

BSTree() = default;//C++: 强制编译器生成默认构造

拷贝构造

拷贝构造依然调用_Copy()私有函数,传入bst._root

BSTree(const BSTree<K>& bst)
{
	_root = _Copy(bst._root);
}

_Copy()

  1. 如果root为空,直接返回空
  2. 先创建新节点拷贝root的key值
  3. 然后拷贝左右指针,最后返回新节点
// 拷贝
Node* _Copy(Node* root)
{
	// root为空返回空
	if (root == nullptr)
	{
		return nullptr;
	}

	// 拷贝节点和指向关系
	Node* copyRoot = new Node(root->_key);
	copyRoot->_left = _Copy(root->_left);
	copyRoot->_right = _Copy(root->_right);

	return copyRoot;
}

析构函数
  • 同理拷贝构造,共有函数调用_Destory(),传入_root
public:
~BSTree()
{
	_Destory(_root);
}
  • _Destory()利用递归删除左右子树,最后销毁根节点
private:
void _Destory(Node*& root)
{
	if (root == nullptr)
		return;

	// 递归销毁根节点所有左右节点
	_Destory(root->_left);
	_Destory(root->_right);
	// 销毁根节点
	delete root;
	root = nullptr;
}

赋值运算符重载

函数体中调用了 swap 函数,将当前对象的 根节点 _root 和新建的对象 bst 的根节点 bst._root 进行交换 ,从而实现了二叉搜索树的值拷贝和赋值操作

BSTree<K>& operator=(BSTree<K> bst)
{
	swap(_root, bst._root); // 交换根节点
	return *this;
}

增删查 – 递归写法

_InsertR(递归尾插)

  • 首先判断 root 是否为空,如果为空,则直接创建一个新的节点并将 key 值赋值给新节点的键值 _key,然后将新节点作为根节点返回,表示插入操作成功。
  • 若root不为空则查找要插入的位置,若key值大则向右递归key值小则向左递归
  • 如果key值等于某一个节点的值,不再插入,返回false
bool _InsertR(Node* root, const K& key)
{
	// 为空,直接创建新节点
	if (root == nullptr)
	{
		root = new Node(key);
		return true;
	}

	// 查找位置并插入节点
	if (key > root->_key)
	{
		return _InsertR(root->_right, key);
	}
	else if (key < root->_key)
	{
		return _InsertR(root->_left, key);
	}
	else 
		return false;
}

_FindR(查)

相同的思路,递归查找

bool _FindR(Node* root, const K& key)
{
	// 遇空返回
	if (root == nullptr)
	{
		return;
	}

	// 如果key大向右找
	if (key > root->_key)
		return _FindR(root->_right, key);
	else if (key < root->_key)
		return _FindR(root->_left, key);
	else
		return true;
}

_EraseR(删除)

bool _EraseR(Node* root, const K& key)
{
	// 为空返回false
	if (root == nullptr)
	{
		return false;
	}

	// 寻找要删除的节点
	if (key > root->_key)
	{
		return _EraseR(root->_right, key);
	}
	else if (key < root->_left)
	{
		return _EraseR(root->_left, key);
	}
	else
	{
		// 执行删除操作
		Node* del = root;
		if (root->_left == nullptr)
		{
			root = root->_right;
		}
		else if (root->_right == nullptr)
		{
			root = root->_right;
		}
		else
		{
			// 找到右树的最 小/左 节点并替换
			Node* min = root->_right;
			while (min->_left)
			{
				min = min->_left;
			}
			swap(root->_key, min->_key);
			return _EraseR(root->_right, key);
		}
		// 删除节点
		delete del;
		return true;
	}
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值