您现在的位置是:首页 >技术教程 >C++【二叉搜索树】网站首页技术教程

C++【二叉搜索树】

7昂7. 2023-06-18 04:00:02
简介C++【二叉搜索树】

一、二叉搜索树

(1)概念

二叉搜索树别名二叉排序树建成BST,它可能是一棵空树,也可能过是具有以下性质的二叉树:如果它的左子树不为空,则左子树上所有节点的值都小于根节点的值,如果它的右子树不为空,则右子树上所有节点的值都大于根节点的值,而且它的左右子树也分别为二叉搜索树。如图:
在这里插入图片描述

(2)操作

二叉树有三种操作:查找,插入,删除。
查找:从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找,最多查找高度次,走到到空,还没找到,这个值不存在。
插入:树为空,则直接新增节点,树不空,按二叉搜索树性质查找插入位置,插入新节点
删除
先查找元素是否在二叉搜索树中,如果不存在,则返回, 反之要删除的结点可能
有三种情况:把叶子结点归到了第一和第二种情况。
1.如果删除结点右为空,删除该结点且使被删除节点的父结点指向被删除节点的左孩子结点。
2.如果删除结点左为空,删除该结点且使被删除节点的父结点指向被删除结点的右孩子结点,也和第一种一样都是直接删除。
3.找左子树的最大结点(最右结点)找右子树的最小结点(最左节点),用它的值填补到被删除节点中,再来处理该结点的删除问题,相当于替换法删除,间接删。

(3)应用

有两种模型:
K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到
的值。
举例:给一个单词hello,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树,在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
举例:英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

二、BST模拟实现及函数解析

(1)构建BST结点结构体

template<class K>
	struct BSTNode
	{
		BSTNode<K>* left;
		BSTNode<K>* right;
		K _key;
		BSTNode(const K& t)
			:_key(t)
			,left(nullptr)
			,right(nullptr)
		{}
		
	};

结点组成树,所以现构建结构体,在里面添加属性,他需要有左指针left和右指针right分别指向左孩子和右孩子,需要存储结点的值key,这里需要对它们进行初始化,所以还在里面写一个构造函数。

(2)BST默认构造及拷贝构造

        BST() = default;
        BST(const BST<K>& t)
		{
			_root = copy(t._root);
		}
		Node* copy(Node* root)
			{
				if(!root)
				{
					return nullptr;
				}
				Node* newroot = new Node(root->_key);
				newroot->left = copy(root->left);
				newroot->right = copy(root->right);
				return newroot;
			}

我们在这用default强制生成默认构造,而拷贝构造涉及到深拷贝,我们再封装一个copy函数,如果为空直接返回nullptr,反之new出一个结点,再分别递归左子树和右子树,最后返回根结点。

(3)BST赋值重载和析构函数

BST<K>& operator=(const BST<K>& t)
		{
			swap(_root, t._root);
			return *this;
		}
		~BST()
		{
			destroy(_root);
		}
			void destroy(Node*& root)
			{
				if (!root)
					return;
				destroy(root->left);
				destroy(root->right);
				delete root;
				root = nullptr;
			}

对于赋值重载,同样也涉及深拷贝,只需要交换两个树的根节点的指针即地址即可。析构函数,需要逐一释放并最后置为空。

(4)非递归实现BST三种操作

(1)查找

       bool 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 true;
				}
			}
			return false;
		}

非递归法写查找,只需要用while循环和BST的性质,在循环里面:从根节点开始我比你大就往右走,同时更新当前的位置,我比小就往左走,同时更新当前的位置,否则找到返回真,走到空结束,遍历结束还没有找到返回假。

(2)删除

        bool erase(const K& key)
		{
			//叶子结点,左为空或右为空,可以把前面归到后面,实际上是两类。托孤,要记录父亲(删1和6)
			//第三种情况左右都不为空,请保姆,找左子树的最大结点(最右结点)找右子树的最小结点(最左节点),间接删,替代法。
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->left;
				}
				else
				{
					if (cur->left == nullptr)//左为空
					{
						if (cur == _root)
						{
							_root = cur->right;
						}
						else
						{
							if (parent->right == cur)
							{
								parent->right = cur->right;
							}
							else
							{
								parent->left = cur->right;
							}
						}
						delete cur;

					}

					else if (cur->right == nullptr)//右为空
					{
						if (cur == _root)
						{
							_root = cur->left;
						}
						else
						{
							if (parent->right == cur)
							{
								parent->right = cur->left;
							}
							else
							{
								parent->left =cur->left;
							}
						}
						delete cur;
					}
					else//左右不为空,这里去找右子树的最左节点
					{
						Node* pmiR = cur;
						Node* miR = cur->right;
						while (miR->left)
						{
							pmiR = miR;
							miR = miR->left;
						}

						cur->_key = miR->_key;
						if (pmiR->right == miR)
						{
							pmiR->right = miR->right;
						}
						else
						{
							pmiR->left = miR->right;
						}
						delete miR;
					}
					return true;
				}
			}
			return false;
		}

删除思路就是先用BST性质进行找要删除的结点,比你大往右走,比你小往左走,这里还有记录一下父结点,以便后续删除,最后再用前面说的三种情况进行删除。
1、如果删除结点左为空,先去判断这个结点是不是根结点,如果是就结点的右子树给给根节点,让它充当根节点,再释放结点;如果不是根结点,我就在里面还要进行两次判断,如果是父亲节点的右孩子是当前结点,就把当前节点的右孩子和父亲的右指针链接,反之就把当前结点的右孩子和父亲的左指针链接。最后释放结点。如前面的图:删除1和6。
2、如果删除结点右为空,同样也是先去判断这个结点是不是根结点,如果是就结点的左子树给给根节点,让它充当根节点,再释放结点;如果不是根结点,需要在里面还要进行两次判断,如果是父亲节点的右孩子是当前结点,就把当前节点的左孩子和父亲的右指针链接,反之就把当前结点的左孩子和父亲的左指针链接。如前面的图:删除14。
3、如果删除结点左右都为空,这时候就需要用替代法,找一个合适的结点来充当现有结点。合适的结点值就要找一个左子树的最大结点即最右结点或右子树的最小结点即最左节点。这里是找的最右结点,遍历找的时候需要记录最小节点的父节点pmiR和最小结点miR,找到之后进行覆盖,把最小结点的值给要删除结点的值,最后进行链接,这里需要做出两种判断,如果父结点的左孩子是最小结点,需要把最小结点的右孩子和父亲的左指针链接,反之把最小结点的右孩子和父亲的右指针链接。

(3)插入

        bool insert(const K& key)
		{
			if (_root ==nullptr)
			{
				_root = new Node(key);
				return true;
			}
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->right;
				}
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->left;
				}
				else
				{
					return false;
				}
			}

			cur = new Node(key);
			if (parent->_key < key)
			{
				parent->right = cur;
			}
			else
			{
				parent->left = cur;
			}
			return true;
		}

插入思路是先找到空进行插入,用BST性质,遍历左右子树,同时记录父亲结点,接着new新节点进行插入,需要两个判断如果比它大,就链接父结点右指针,反之链接父节点左指针。

(5)递归实现BST三种操作

(1)查找

        bool _Rfind(Node* root, const K& key)
		{
			if (!root)
				return false;
			if (root->_key = key)
				return true;
			if (root->_key < key)
				return _Rfind(root->right);
			else 
				return _Rfind(root->left);
		}

递归法查找思路还是利用BST性质,如果比它大就递归右子树,如果比它小就递归左子树,如果找找到返回真,为空返回假。

(2)插入

bool _Rinsert(Node*& root, const K& key)
		{
			if (!root)
			{
				root = new Node(key);
			}
			if (root->_key < key)
				return _Rinsert(root->right, key);
			if (root->_key > key)
				return _Rinsert(root->left, key);
		}

递归法插入,也是用BST性质,先递归左右子树,等到为空的时候进行插入,但插入要和父亲链接起来,怎么链接?在不增加参数的情况下我们可以传引用,是最优方案,因为就最后一次起了作用,实际上就是此时的root是上一层root左指针或右指针的别名,在这里new了一个结点给给root,间接地链接上了。

(3)删除

bool _Rerase(Node*& root, const K& key)
		{
			if (!root)
				return false;
			if (root->_key < key)
				return _Rerase(root->right, key);
			else if (root->_key > key)
				return _Rerase(root->left, key);
			else
			{
				Node* del = root;
				if (root->right == nullptr)
				{
					root = root->left;
				}
				else if (root->left == nullptr)
				{
					root = root->right;
				}
				else
				{
					Node* mal = root;
					mal = mal->left;
					
					while (mal->right)
					{
						mal = mal->right;
					}
					swap(root->_key, mal->_key);
					return _Rerase(root->left, mal->_key);

				}
				delete del;
				return true;
			}

		}

递归法删除,用BST性质进行找要删除的结点,递归左右子树,这里不需要记录父结点,同样也是三种情况。
1、删除结点左为空,定义一个del指针变量,把当前删除的结点给它,再进行链接,用了引用,不需要管父节点和哪个指针链接,直接链接,最释放del。
2、同理1。
3、删除结点左右都不为空,也是找一个合适的结点替代它,在这里找的是左子树的最大结点即最右结点,找到之后把删除结点和最大结点的值交换,重新再做一次递归转化为删掉左子树的最大结点,此时的mal存的key不会进入第三个条件,因为此时mal的孩子要么有一个要么没有,这样它会是第一种情况或是第二种情况,不管哪种情况我们能用到引用,找到父亲进而链接。
这里注意的这里第二次递归指针不能直接传mal,因为如果传了,我们的引用就没有用了就链接不了,因为它会进入第一个或第二条件,我们要的是上个位置的引用不是这个位置的引用。

三、BST实现源代码

(1)BST.h

#pragma once


namespace nza
{
	template<class K>
	struct BSTNode
	{
		BSTNode<K>* left;
		BSTNode<K>* right;
		K _key;
		BSTNode(const K& t)
			:_key(t)
			,left(nullptr)
			,right(nullptr)
		{}
		
	};
	template<class K>
	class BST
	{
		typedef BSTNode<K> Node;
	public:
		BST() = default;
		BST(const BST<K>& t)
		{
			_root = copy(t._root);

		}
		BST<K>& operator=(const BST<K>& t)
		{
			swap(_root, t._root);
			return *this;
		}
		~BST()
		{
			destroy(_root);
		}

		//非递归如下:
		bool insert(const K& key)
		{
			if (_root ==nullptr)
			{
				_root = new Node(key);
				return true;
			}
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->right;
				}
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->left;
				}
				else
				{
					return false;
				}
			}

			cur = new Node(key);
			if (parent->_key < key)
			{
				parent->right = cur;
			}
			else
			{
				parent->left = cur;
			}
			return true;
		}

		bool 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 true;
				}
			}
			return false;
		}

		bool erase(const K& key)
		{
			//叶子结点,左为空或右为空,可以把前面归到后面,实际上是两类。托孤,要记录父亲(删1和6)
			//第三种情况左右都不为空,请保姆,找左子树的最大结点(最右结点)找右子树的最小结点(最左节点),间接删,替代法。
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->left;
				}
				else
				{
					if (cur->left == nullptr)//左为空
					{
						if (cur == _root)
						{
							_root = cur->right;
						}
						else
						{
							if (parent->right == cur)
							{
								parent->right = cur->right;
							}
							else
							{
								parent->left = cur->right;
							}
						}
						delete cur;

					}

					else if (cur->right == nullptr)//右为空
					{
						if (cur == _root)
						{
							_root = cur->left;
						}
						else
						{
							if (parent->right == cur)
							{
								parent->right = cur->left;
							}
							else
							{
								parent->left =cur->left;
							}
						}
						delete cur;
					}
					else//左右不为空,这里去找右子树的最左节点
					{
						Node* pmiR = cur;
						Node* miR = cur->right;
						while (miR->left)
						{
							pmiR = miR;
							miR = miR->left;
						}

						cur->_key = miR->_key;
						if (pmiR->right == miR)
						{
							pmiR->right = miR->right;
						}
						else
						{
							pmiR->left = miR->right;
						}
						delete miR;
					}
					return true;
				}
			}
			return false;
		}


		void inorder()
		{
			_inoder(_root);
			cout << endl;
		}

		//递归如下:
		bool Rfind(const K& key)
		{
			return _Rfind(_root, key);
		}
		bool Rinsert(const K& key)
		{
			return _Rinsert(_root, key);
		}
		bool Rerase(const K& key)
		{
			return _Rerase(_root, key);
		}

	protected:
		bool _Rfind(Node* root, const K& key)
		{
			if (!root)
				return false;
			if (root->_key = key)
				return true;
			if (root->_key < key)
				return _Rfind(root->right);
			else 
				return _Rfind(root->left);
		}
		bool _Rinsert(Node*& root, const K& key)
		{
			if (!root)//插入要和父亲链接起来,在不增加参数的情况下我们可以传引用,是最优方案,因为就最后一次起了作用,实际上就是
				//此时的root是上一层root指向左指针或右指针的别名,在这里new了一个结点给给root,间接地链接上了。
			{
				root = new Node(key);
			}
			if (root->_key < key)
				return _Rinsert(root->right, key);
			if (root->_key > key)
				return _Rinsert(root->left, key);
		}
		bool _Rerase(Node*& root, const K& key)
		{
			if (!root)
				return false;
			if (root->_key < key)
				return _Rerase(root->right, key);
			else if (root->_key > key)
				return _Rerase(root->left, key);
			else
			{
				Node* del = root;
				if (root->right == nullptr)
				{
					root = root->left;
				}
				else if (root->left == nullptr)
				{
					root = root->right;
				}
				else
				{
					Node* mal = root;
					mal = mal->left;
					
					while (mal->right)
					{
						mal = mal->right;
					}
					swap(root->_key, mal->_key);
					return _Rerase(root->left, mal->_key);//引用不能改指向,所以把它们的值交换,重新再做一次递归转化为删掉左子树的
					//最大结点,此时的mal存的key不会进入第三个条件,因为此时mal的孩子要么有一个要么没有。这里注意的这里第二次递归指针
					// 不能直接传mal,因为如果传了,我们的引用就没有用了就链接不了,因为它会进入第一个或第二条件,我们要的是上个位置的引用
					//不是这个位置的引用

				}
				delete del;
				return true;
			}

		}
			Node* copy(Node* root)
			{
				if(!root)
				{
					return nullptr;
				}
				Node* newroot = new Node(root->_key);
				newroot->left = copy(root->left);
				newroot->right = copy(root->right);
				return newroot;
			}
			void destroy(Node*& root)
			{
				if (!root)
					return;
				destroy(root->left);
				destroy(root->right);
				delete root;
				root = nullptr;
			}
			void _inoder(Node* root)
			{
				if (!root)
					return;
				_inoder(root->left);
				cout << root->_key << " ";
				_inoder(root->right);

			}

	private:
		Node* _root = nullptr;
		
	};
}

(2)Test.cpp

#include<iostream>
using namespace std;
#include"BST.h"
int main()
{
	nza::BST<int> t;
	int a[] = { 5,2,9,6,1,0,3 };
	for (auto n : a)
	{
		t.insert(n);
	}
	t.inorder();

	t.erase(9);
	t.inorder();

	t.erase(5);
	t.inorder();

	t.erase(1);
	t.inorder();


	t.Rerase(2);
	t.inorder();
	 
	t.Rerase(0);
	t.inorder();

	t.Rinsert(100);
	t.inorder();


}

四、BST运行结果

在这里插入图片描述

五、二叉搜索树的性能

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
二叉搜索树为完全二叉树(或者接近完全二叉树),次数为:O(logN)。
在这里插入图片描述
二叉搜索树为单支树,次数为:O(N^2)
在这里插入图片描述
如果退化成单支树,二叉搜索树的性能就失去了。是可以进行改进的,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优,而AVL树和红黑树就可以解决这个问题。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。