<C++> 二叉搜索树

目录

二叉搜索树

[1. 概念](#1. 概念)

[2. 二叉搜索树操作](#2. 二叉搜索树操作)

[2.1 基础结构](#2.1 基础结构)

[2.2 非递归版](#2.2 非递归版)

[1. 查找](#1. 查找)

[2. 插入](#2. 插入)

[3. 删除](#3. 删除)

[2.3 递归版](#2.3 递归版)

[1. 查找](#1. 查找)

[2. 插入](#2. 插入)

[3. 删除](#3. 删除)

[2.4 拷贝构造函数](#2.4 拷贝构造函数)

[2.5 赋值运算符重载](#2.5 赋值运算符重载)

[2.6 析构函数](#2.6 析构函数)

[2.7 完整代码](#2.7 完整代码)

[3. 二叉搜索树的应用](#3. 二叉搜索树的应用)

[4. 二叉搜索树的性能](#4. 二叉搜索树的性能)


二叉搜索树

1. 概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

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

由于二叉搜索树的性质,该树的中序遍历就是递增序列

2. 二叉搜索树操作

2.1 基础结构

cpp 复制代码
	template<class K>
	struct BSTreeNode
	{
		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;

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

    template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		BSTree()
			:_root(nullptr)
		{}

	private:
		Node* _root;
	};

2.2 非递归版

1. 查找
  • key小于cur->_key,则往左子树找
  • key大于cur->_key,则往右子树找
cpp 复制代码
		bool Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else
				{
					return true;
				}
			}

			return false;
		}
2. 插入
  • 插入要先找到合适的空位
  • 如果已经之前已经存在,那么return false
  • 找到空位之后,new出一个新节点,在链接时我们要获取parent节点,这就需要我们提前记录parent节点
  • 链接时还要判断 key 与 parent->_key大小关系,因为我们找到了合适的位置,但是没有记录是在左还是在右
cpp 复制代码
bool Insert(const K& key)
		{
			//如果树为空,直接new一个根节点
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}

			//记录父节点,最后链到父节点
			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
				{
					return false;
				}
			}

			//由于不知道插入的节点与父亲的key大小关系,所以再判断一下是插到左边还是右边
			cur = new Node(key);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			return true;
		}
3. 删除
  • 先查找该节点是否存在,如果存在,再看以下几点
  • **删除节点情况分三种:**该节点左孩子为空、该节点右孩子为空、该节点左右孩子都有
  • **左孩子为空:**将右节点链接到parent上即可,此时还要判断cur在parent的哪一边,如果cur在parent的右边,那么cur的所有孩子必然比parent的key大,所以将cur的right链接到parent的right即可
  • **右孩子为空:**同理
  • **左右孩子都存在:**此时我们可以找该节点左子树的最右节点,或右子树的最左节点,这两个节点都是最接近根节点key值的节点(因为二叉搜索树的性质,当节点无穷时,这两个节点的key值将从左和从右边无限趋近于根节点的key值),找到节点后与根节点交换key值,此时如果是左子树的最右节点,那么该节点是绝不可能存在右子树,所以此时将该节点的左子树链接到parent节点即可(提前记录parent,在判断cur在parent的哪一边)
  • 最后不要忘了delete被删除的节点
cpp 复制代码
bool Erase(const K& key)
		{
			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  //再看看cur在父节点parent的哪一边,在哪一边就把孤儿链接到哪一边
						{
							if (parent->_right == cur)
							{
								parent->_right = cur->_right;
							}
							else
							{
								parent->_left = cur->_right;
							}
						}
					}
					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;
							}
						}
					}
					else  //该节点左右都不为空
					{
						Node* parent = cur;
						Node* leftMax = cur->_left;

						while (leftMax->_right)
						{
							parent = leftMax;
							leftMax = leftMax->_right;
						}

						swap(leftMax->_key, cur->_key);

						if (parent->_left == leftMax)
						{
							parent->_left = leftMax->_left;
						}
						else
						{
							parent->_right = leftMax->_left;
						}

						cur = leftMax;
					}
					delete cur;
					return true;
				}
			}

			return false;
		}

2.3 递归版

由于用户不传递root参数,所以FindR内层封装一个获取root参数的函数,其他函数同理

1. 查找
  • 如果找不到返回false,找到了返回true。比key小,往右子树递归找;比key大,往左子树递归找。
cpp 复制代码
		bool FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
			{
				return true;
			}
		}
2. 插入
  • insert函数神之一手的地方就是参数类型 Node*&,当它递归下去时 root 其实是它的父节点的 left 或 right 的引用!它可以不用提前保存parent的信息,直接链接新节点!
cpp 复制代码
		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}

		//Node*& root,这里的引用是神之一手,因为在连接的时候可以不用再去记录parent
		//在链接的时候,那个root是引用的父亲的root->_right或root->_left !!
		bool _InsertR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				//这个root是引用的父亲的root->_right或root->_left,这一步是直接链接了!
				root = new Node(key);
				return true;
			}

			if (root->_key < key)
			{
				return _InsertR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _InsertR(root->_left, key);
			}
			else
			{
				//已经有该节点了,返回false
				return false;
			}
		}
3. 删除
  • 先找到要删除的节点,然后再提前保存该节点,为了后续的delete
  • 同样的,erase的参数也是Node*&,它极大方便了节点的链接
  • 找到节点后,如果该节点的左节点为空,那么直接root = root->_right;如果右节点为空,那么 root = root->_left,这就是Node* &的强大之处。那么可能有疑问,非递归版本为什么不能用引用?循环版本不能使用引用,是因为引用不能改变指向!递归可以使用引用是因为每次都是一个新的栈帧
  • 如果左右节点都存在,找左子树最右节点,交换key值,再erase掉key的节点(注意,不能从root开始找,因为root此时已经被交换key值了,递归会往右边去找,这就会导致找错了
  • 最后Erase()这里不能传leftMax->_left,因为leftMax是局部变量,最后子节点会链接不上真正的root
cpp 复制代码
		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

        bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key < key)
			{
				return _EraseR(root->_left, key);
			}
			else  //找到了,开始删除
			{
				//提前记录
				Node* del = root;

				//左为空
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				//右为空
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				//左右都不为空
				else
				{
					Node* leftMax = root->_left;
					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}

					swap(leftMax->_key, root->_key);

					//这里不能传leftMax->_left,因为是别名,leftMax是局部变量,最后子节点会链接不上真正的root
					return _EraseR(root->_left, key);
				}

				delete del;
				return true;
			}
		}

2.4 拷贝构造函数

  • 根据前序序列递归拷贝
  • 该递归就是从最左边开始链接,再从最底层往上
cpp 复制代码
		BSTree(const BSTree<K>& t)
		{
			_root = Copy(t._root);
		}

		Node* Copy(Node* root)
		{
			//前序遍历拷贝
			if (root == nullptr)
				return nullptr;

			Node* copyroot = new Node(root->_key);
			copyroot->_left = Copy(root->_left);
			copyroot->_right = Copy(root->_right);

			return copyroot;
		}

2.5 赋值运算符重载

  • 现代写法
cpp 复制代码
		BSTree<K>& operator=(BSTree<K> t)
		{
			swap(_root, t._root);
			return *this;
		}

2.6 析构函数

  • 递归式析构
cpp 复制代码
		~BSTree()
		{
			Destory(_root);
		}

		void Destory(Node*& root)
		{
			if (root == nullptr)
				return;

			Destory(root->_left);
			Destory(root->_right);
			delete root;
			root = nullptr;
		}

2.7 完整代码

cpp 复制代码
namespace key
{
	template<class K>
	struct BSTreeNode
	{
		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;

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

	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		BSTree()
			:_root(nullptr)
		{}

		BSTree(const BSTree<K>& t)
		{
			_root = Copy(t._root);
		}

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

		~BSTree()
		{
			Destory(_root);
		}

		bool Insert(const K& key)
		{
			//如果树为空,直接new一个根节点
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}

			//记录父节点,最后链到父节点
			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
				{
					return false;
				}
			}

			//由于不知道插入的节点与父亲的key大小关系,所以再判断一下是插到左边还是右边
			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->_left;
				}
				else if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else
				{
					return true;
				}
			}

			return false;
		}

		bool Erase(const K& key)
		{
			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  //再看看cur在父节点parent的哪一边,在哪一边就把孤儿链接到哪一边
						{
							if (parent->_right == cur)
							{
								parent->_right = cur->_right;
							}
							else
							{
								parent->_left = cur->_right;
							}
						}
					}
					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;
							}
						}
					}
					else  //该节点左右都不为空
					{
						Node* parent = cur;
						Node* leftMax = cur->_left;

						while (leftMax->_right)
						{
							parent = leftMax;
							leftMax = leftMax->_right;
						}

						swap(leftMax->_key, cur->_key);

						if (parent->_left == leftMax)
						{
							parent->_left = leftMax->_left;
						}
						else
						{
							parent->_right = leftMax->_left;
						}

						cur = leftMax;
					}
					delete cur;
					return true;
				}
			}

			return false;
		}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

		bool FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

	private:
		Node* Copy(Node* root)
		{
			//前序遍历拷贝
			if (root == nullptr)
				return nullptr;

			Node* copyroot = new Node(root->_key);
			copyroot->_left = Copy(root->_left);
			copyroot->_right = Copy(root->_right);

			return copyroot;
		}

		void Destory(Node*& root)
		{
			if (root == nullptr)
				return;

			Destory(root->_left);
			Destory(root->_right);
			delete root;
			root = nullptr;
		}

		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key < key)
			{
				return _EraseR(root->_left, key);
			}
			else  //找到了,开始删除
			{
				//提前记录
				Node* del = root;

				//左为空
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				//右为空
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				//左右都不为空
				else
				{
					Node* leftMax = root->_left;
					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}

					swap(leftMax->_key, root->_key);

					//这里不能传leftMax->_left,因为是别名,leftMax是局部变量,最后子节点会链接不上真正的root
					return _EraseR(root->_left, key);
				}

				delete del;
				return true;
			}
		}

		//Node*& root,这里的引用是神之一手,因为在连接的时候可以不用再去记录parent
		//在链接的时候,那个root是引用的父亲的root->_right或root->_left !!
		bool _InsertR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				//这个root是引用的父亲的root->_right或root->_left,这一步是直接链接了!
				root = new Node(key);
				return true;
			}

			if (root->_key < key)
			{
				return _InsertR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _InsertR(root->_left, key);
			}
			else
			{
				//已经有该节点了,返回false
				return false;
			}
		}

		bool _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
			{
				return true;
			}
		}

		void _InOrder(Node* root)
		{
			if (root == NULL)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << " ";
			_InOrder(root->_right);
		}
	private:
		Node* _root;
	};

	void TestBSTree1()
	{
		int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
		BSTree<int> t;
		for (auto e : a)
		{
			t.Insert(e);
		}

		t.InOrder();

		t.EraseR(4);
		t.InOrder();

		t.EraseR(6);
		t.InOrder();

		t.EraseR(7);
		t.InOrder();

		t.EraseR(3);
		t.InOrder();

		for (auto e : a)
		{
			t.Erase(e);
		}
		t.InOrder();
	}

}

3. 二叉搜索树的应用

  1. K模型:K模型即只有key作为关键码,结构中只需要存储Key****即可,关键码即为需要搜索到的值

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

  • 以单词集合中的每个单词作为key,构建一棵二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
  1. KV模型:每一个关键码key**,都有与之对应的值Value,即****<Key, Value>的键值对。该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>**就构成一种键值对

比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:

  • <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较
  • Key查询英文单词时,只需给出英文单词,就可快速找到与其对应的key
cpp 复制代码
namespace key_value
{
	template<class K, class V>
	struct BSTreeNode
	{
		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key;
		V _value;

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

	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		BSTree()
			:_root(nullptr)
		{}

		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}

		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool InsertR(const K& key, const V& value)
		{
			return _InsertR(_root, key, value);
		}

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

	private:
		bool _EraseR(Node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (root->_key < key)
			{
				return _EraseR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _EraseR(root->_left, key);
			}
			else
			{
				Node* del = root;

				// 1、左为空
				// 2、右为空
				// 3、左右都不为空
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				else
				{
					Node* leftMax = root->_left;
					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}

					swap(root->_key, leftMax->_key);

					return _EraseR(root->_left, key);
				}

				delete del;
				return true;
			}
		}

		bool _InsertR(Node*& root, const K& key, const V& value)
		{
			if (root == nullptr)
			{
				root = new Node(key, value);
				return true;
			}

			if (root->_key < key)
			{
				return _InsertR(root->_right, key, value);
			}
			else if (root->_key > key)
			{
				return _InsertR(root->_left, key, value);
			}
			else
			{
				return false;
			}
		}

		Node* _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return nullptr;

			if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else
			{
				return root;
			}
		}

		void _InOrder(Node* root)
		{
			if (root == NULL)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}
	private:
		Node* _root;
	};


	void TestBSTree1()
	{
		//BSTree<string, Date> carTree;

		BSTree<string, string> dict;
		dict.InsertR("insert", "插入");
		dict.InsertR("sort", "排序");
		dict.InsertR("right", "右边");
		dict.InsertR("date", "日期");

		string str;
		while (cin >> str)
		{
			BSTreeNode<string, string>* ret = dict.FindR(str);
			if (ret)
			{
				cout << ret->_value << endl;
			}
			else
			{
				cout << "无此单词" << endl;
			}
		}
	}

	void TestBSTree2()
	{

		// 统计水果出现的次数
		string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
		BSTree<string, int> countTree;
		for (auto& str : arr)
		{
			auto ret = countTree.FindR(str);
			if (ret == nullptr)
			{
				countTree.InsertR(str, 1);
			}
			else
			{
				ret->_value++;
			}
		}

		countTree.InOrder();
	}
}

相比只有key版本,key_value只是在结构体内多加了value而已,部分函数原理也没有变

4. 二叉搜索树的性能

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

相关推荐
小陈phd几秒前
Vscode LinuxC++环境配置
linux·c++·vscode
火山口车神丶15 分钟前
某车企ASW面试笔试题
c++·matlab
jiao_mrswang26 分钟前
leetcode-18-四数之和
算法·leetcode·职场和发展
qystca34 分钟前
洛谷 B3637 最长上升子序列 C语言 记忆化搜索->‘正序‘dp
c语言·开发语言·算法
薯条不要番茄酱35 分钟前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea
今天吃饺子40 分钟前
2024年SCI一区最新改进优化算法——四参数自适应生长优化器,MATLAB代码免费获取...
开发语言·算法·matlab
是阿建吖!41 分钟前
【优选算法】二分查找
c++·算法
王燕龙(大卫)1 小时前
leetcode 数组中第k个最大元素
算法·leetcode
不去幼儿园2 小时前
【MARL】深入理解多智能体近端策略优化(MAPPO)算法与调参
人工智能·python·算法·机器学习·强化学习
Mr_Xuhhh2 小时前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法