二叉搜索树原理以及实现

🍑个人主页:Jupiter.
🚀 所属专栏:C++学习笔记
欢迎大家点赞收藏评论😊

在这里插入图片描述

在这里插入图片描述


最近学习了有关搜索二叉树的相关知识,在此特意将该知识进行总结分享,希望对大家有所帮助。

二叉搜索树

二叉搜索树的概念

二叉搜索树又叫二叉排序树,二叉查找树,可以为空,也可以不为空,具体有以下的特性:

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

在这里插入图片描述

二叉搜索树的操作(含思路分析+代码实现)

一个基本的二叉搜索树需要有哪些函数(接口)呢?

二叉搜索树的查找(递归实现看最后总代码)
  • 基于二叉搜索树的结构的特点:左节点小于根,右节点大于根

  • 查找的思路分析:

    • 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
    • 最多查找高度次,走到到空,还没找到,这个值不存在。

图解:
在这里插入图片描述

代码实现:

	bool Find(const K& key)       //key为寻找关键字
	{
		if (_root == nullptr)    //如果为空,直接返回
			return false;
		else {
			Node* cur = _root;         //用cur迭代寻找
			while (cur){
				if (key > cur->_key)      //如果key比根节点的值大,则去右子树寻找
				{
					cur = cur->_right;
				}
				else if(key < cur->_key)   //如果key比根节点的值小,则去左子树寻找
				{
					cur = cur->_left;
				}
				else{
					return true;       //相等,返回true
				}
			} 
			return false;          //出循环,还没找到,返回false
		}
	}
二叉搜索树的插入(递归实现看最后总代码)
  • 插入思路分析:

    • 树为空,则直接新增节点,赋值给root指针
    • 树不空,按二叉搜索树性质查找插入位置,插入新节点

图解:
在这里插入图片描述

代码实现:

	bool insert(const K& key){
		if (_root == nullptr)    //如果为空树,直接插入
		{
			Node* newNode = new Node(key);     //new一个节点
			_root = newNode;                   //连接

			return true;      //插入成功,返回true
		}
		else{
			Node* parent = nullptr;     //因为要链接,所以需要记录父亲
			Node* cur = _root;   
			while (cur)                   //while循环和find函数类似,目的找到合适位置插入
			{
				if (key > cur->_key){
					parent = cur;    //记录父节点
					cur = cur->_right;
				}
				else if(key < cur->_key){
					parent = cur;
					cur = cur->_left;
				}
				else{
					return false;    //搜索二叉树里面的值不能重复,如果已经存在,则返回false
				}
			}
			//cur空,结束循环,代表找到位置了
			Node* newNode = new Node(key);      //new节点
			if (key < parent->_key)             //判断是链接到父亲的左还是右
				parent->_left = newNode;
			if (key > parent->_key)         
				parent->_right = newNode;

			return true;
		}
	}
二叉搜索树的删除(递归实现看最后总代码)
  • 首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

    • 要删除的结点无孩子结点 //直接删除
    • 要删除的结点只有左孩子结点 //托孤
    • 要删除的结点只有右孩子结点 //托孤
    • 要删除的结点有左、右孩子结点 //替换法删除

看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:

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

托孤法删除的原理:

  • 假设我们要删除【图一】的22,它只有一个孩子,就可以使用托孤的方法,直接让父节点(15)指向它的孩子(17);
    该方法也可以在删除没有孩子的节点上使用(将它看作有一个为NULL的孩子即可),例如下【图二】,假设删除17;
    在这里插入图片描述

在这里插入图片描述

替换法删除的原理:
方法作用于有两个孩子的节点:

a,找到该节点左子树的最右节点(即左子树最大的节点)或则右子树的最左节点(即右子树最小值);
b,然后将待删除节点的值与找到的最大(或最小)节点的值进行交换,转化为删除找到的这个节点;
c,最后用上面的托孤的方法删除该节点

在这里插入图片描述

代码实现:

bool erase(const T& key)
{
	if (_root == nullptr) return false;   //空树,直接返回
		
	Node* parent = nullptr;  //记录被删除节点的父节点
	Node* cur = _root;       //记录被删除节点
	
	while (cur)   //while是找删除节点
	{
		if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else break;     //找到了,跳出循环
	}
	if (cur == nullptr) return false;    //树中没有找到被删除节点

	//此时的cur指向的是被删除节点  parent指向的是被删除节点的父节点
	//1. 被删除节点有两个孩子  替换法删除
	if (cur->_left && cur->_right)
	{
		//1. 找到cur节点的右子树中最左的节点(也就是右子树中最小的节点)
		Node* minparent = cur;           //记录最小节点的父节点
		Node* rightmin = cur->_right;    //记录右树的最小节点
		while (rightmin->_left)
		{
			minparent = rightmin;
			rightmin = rightmin->_left;
		}
		std::swap(cur->_key, rightmin->_key);
		//这里不需要判断rightmin节点与minparent的关系,一定是parent的左孩子  ----  错误想法❌
		
		//注意:上面的想法是错误的。
		//这里需要判断是父节点的左孩子还是右孩子,因为可能 minparent == cur ,rightmin == cur->_right
		//这样rightmin就是parent的右孩子
		
		if (minparent->_left == rightmin)
		{
			minparent->_left = rightmin->_right;
		}
		else
		{
			minparent->_right = rightmin->_right;

		}
		delete rightmin;
		rightmin = nullptr;
	}
	//2. 被删除节点的孩子<=1   托孤法删除
	else{
		if (cur == _root)   _root == nullptr;   //被删除的节点是根节点
		
		else if (parent->_left == cur)   //1. 判断cur是parent的左孩子还是右孩子
		{
			if (cur->_left)
				parent->_left = cur->_left;
			else
				parent->_left = cur->_right;
		}
		else
		{
			if (cur->_right)
				parent->_right = cur->_right;
			else
				parent->_right = cur->_left;
		}
	}
	return true;
}
总代码(含递归实现):
#pragma once
#include<iostream>

using namespace std;

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

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

};

template<class K>
class B_S_Tree
{
public:
	typedef Binary_Serach_TreeNode<K> Node;
	//B_S_Tree() = default;    //强制生成默认构造
	B_S_Tree()
		:_root(nullptr)
	{}

	B_S_Tree(B_S_Tree<K>& bst)
	{
		_root = copy(bst._root);
	}
	Node* copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* newroot = new Node(root->_key);
		newroot->_left = copy(root->_left);
		newroot->_right = copy(root->_right);

		return newroot;
	}
	B_S_Tree<K>& operator=(B_S_Tree<K> bst)
	{
		std::swap(bst._root, _root);
		return *this;
	}
	bool insert(const K& key)
	{
		if (_root == nullptr)    //如果为空树,直接插入
		{
			Node* newNode = new Node(key);    //new一个节点
			_root = newNode;                   //链接

			return true;      //插入成功,返回true
		}
		else
		{
			Node* parent = nullptr;     //因为要链接,所以需要记录父亲
			Node* cur = _root;   
			while (cur)                   //while循环和find函数类似,目的找到合适位置插入
			{
				if (key > cur->_key)
				{
					parent = cur;    //记录父节点
					cur = cur->_right;
				}
				else if(key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					return false;    //搜索二叉树里面的值不能重复,如果已经存在,则返回false
				}
			}
			//cur空,结束循环,代表找到位置了
			Node* newNode = new Node(key);      //new节点
			if (key < parent->_key)            
				parent->_left = newNode;
			if (key > parent->_key)          //判断是链接到父亲的左还是右
				parent->_right = newNode;

			return true;
		}
	}

	bool Erase(const K& key)
	{
		if (_root == nullptr)     //如果树为空,直接返回
			return false;
		else                     //树不为空
		{
			Node* parent = _root;
			Node* cur = _root;       
			while (cur)                 //while循环,先找到待删除节点以及记录它父节点
			{                           //该循环体与find和insert类似
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (key < cur->_key) 
				{
					parent = cur;
					cur = cur->_left;
				}
				else                             //找到该节点了,开始删除
				{
				    //1.只有一个孩子或则没有孩子
					Node* del = cur;                    //保存要删除的这个节点,防止等下托孤后该节点找不到
					if (!(cur->_left && cur->_right))    //两个孩子都存在的逻反就是有一个孩子或则没有孩子
					{
						if (cur == _root)       //这里,判断如果该节点是头节点,直接将头节点置空,返回
						{
							_root = nullptr;         
						}
						if (parent->_left == cur)     //该删除节点为父节点的左子树,则将它的孩子连接到它父节点的左子树上
						{
							if (cur->_left)           //判断孩子是待删除节点的哪个孩子
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_left = cur->_right;
							}
							return true;
						}
						else                        //和上面一样
						{
							if (cur->_left)
							{
								parent->_right = cur->_left;
							}
							else
							{
								parent->_right = cur->_right;
							}
							return true;
						}
					}
					//2.待删除节点有两个孩子
					else                            //这里实现的是替换法中找右子树中最小的节点
					{
						Node* rightMinparent = cur;        //记录最小节点的父亲,因为等下需要托孤
						Node* rightMin = cur->_right;      //记录最小节点
						while (rightMin->_left)
						{
							rightMinparent = rightMin;
							rightMin = rightMin->_left;
						}
						//到这里说明中找到了右树的最小节点
						swap(rightMin->_key, cur->_key);    //交换最小节点与待删除节点的值,转换为删除这个最小节点
						if (rightMinparent->_left==rightMin)     //下面的逻辑与上面的托孤一样
						{
							rightMinparent->_left = rightMin->_right;
						}
						else
						{
							rightMinparent->_right = rightMin->_right;
						}
						delete rightMin;                      //最后删除
					}
					return true;
				}
			}
			return false;
		}
	}

	bool Find(const K& key)       //key为寻找关键字
	{
		if (_root == nullptr)    //如果为空,直接返回
		{
			return false;
		}
		else                      
		{
			Node* cur = _root;         //用cur迭代寻找
			while (cur)
			{
				if (key > cur->_key)      //如果key比根节点的值大,则去右子树寻找
				{
					cur = cur->_right;
				}
				else if(key < cur->_key)   //如果key比根节点的值小,则去左子树寻找
				{
					cur = cur->_left;
				}
				else
				{
					return true;       //相等,返回true
				}
			} 
			return false;          //出循环,还没找到,返回false
		}
	}
	bool insertR(const K& key)
	{
		return _inserR(_root, key);
	}
	bool  FindR(const K& key)
	{
		return _FindR(_root,key);
	}
	bool  EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}

	void OrdPrint()
	{
		_OrdPrint(_root);
		cout << endl;
	}

	~B_S_Tree()
	{
		//_Destory(_root);
	}
private:
	void _OrdPrint(Node* root)
	{
		if (root == nullptr)
			return;

		_OrdPrint(root->_left);
		cout << root->_key <<' ';
		_OrdPrint(root->_right);
	}
	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;
		if (key < root->_key)
			return _FindR(root->_left, key);
		else if (key < root->_key)
			return _FindR(root->_right, key);
		else
			return true;
	}
	bool _inserR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		if (key < root->_key)
			return _inserR(root->_left, key);
		else if (key > root->_key)
			return _inserR(root->_right, key);
		else
			return false;
	}
	bool _EraseR(Node*& root,const K&  key)
	{
		if (root == nullptr)
			return false;
		if (key < root->_key)
			_FindR(root->_left, key);
		else if (key < root->_key)
			_FindR(root->_right, key);
		else
		{
			Node* del = root;
			if (root->_left==nullptr)
				root = root->_right;
			else if(root->_right==nullptr)
				root = root->_left;
			else
			{
				Node* rightMinparent = root;
				Node* rightMin = root->_right;
				while (rightMin->_left)
				{
					rightMinparent = rightMin;
					rightMin = rightMin->_left;
				}
				swap(root->_key, rightMin->_key);
				return _EraseR(root->_right, key);
			}
			delete del;
			return true;

		}
	}
	void _destory(Node* root)
	{
		if (root == nullptr)
			return;
		else
		{
			_destory(root->_left);
			_destory(root->_right);
			delete root;
		}
	}

private:
	Node* _root;
};

二叉搜索树的应用

应用一

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

应用二

  • KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:比如英汉 词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

二叉搜索树的性能分析

  • 插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

  • 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树)【如图左】,其平均比较次数为:log N
    最差情况下,二叉搜索树退化为单支树(或者类似单支)【如图右】,其平均比较次数为:N

在这里插入图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值