C++ Advanced - [Advanced Binary Tree]

Table of contents

1. Binary search tree

 1.1 Concept of Binary Search Tree

 1.2 Binary search tree operation

2. Implementation of binary search tree

 2.1 Structure of binary search tree

2.2 Insertion of binary search tree

 2.3 Find Find

2.4 Inorder traversal in InOrder

 2.5 Erase delete

2.6 Construction and copy construction

 2.7 Assignment overloading and destruction

 2.8 Insertion (recursive)

 2.9 Delete (recursive)

3. Application of binary search tree 

 4. Performance Analysis of Binary Search Tree


 

1. Binary search tree

 1.1 Binary search tree concept

A binary search tree, also known as a binary sorting tree, is either an empty tree or a binary tree with the following properties :

  • If its left subtree is not empty, the values ​​of all nodes on the left subtree are less than the value of the root node
  • If its right subtree is not empty, the values ​​of all nodes on the right subtree are greater than the value of the root node
  • Its left and right subtrees are also binary search trees

 1.2 Binary search tree operation

  1.  Searching for a binary search tree                                                                                                      a . Compare and search from the root. If it is larger than the root, go to the right to continue searching, and if it is smaller than the root, go to the left to continue searching.  b . The height is searched for at most times, and the value does not exist if it reaches the empty space and has not been found yet.
  2. Insertion of binary search tree The specific process of                                                                                                         insertion is as follows:                                                                                                 a. If the tree is empty, add a node directly and assign it to the root pointer                                                                b. If the tree is not empty, find the corresponding insertion position according to the nature of the binary search tree and insert a new node                                  
  3. 3. Deletion of the binary search tree                                    ​​​​​​​​First find out whether the element is in the binary search tree, if not, return it, otherwise the node to be deleted may be divided into the following four situations:                                                                                                              a. To delete The node has no child nodes                                                                                              b. The node to be deleted is only the left child node                                                                                    c. The node to be deleted is only the right child node                                                                                      d. The node to be deleted has left and right child nodes                                                                                    ​​​​It seems that there are 4 situations for the node to be deleted. The actual situation a can be combined with the situation b or c , so the real deletion process is as follows:                                                                                                         situation b : delete the node and let the parent node of the deleted node point to the deleted node Left child node - delete   directly Case c : delete the node and let the parent node of the deleted node point to the right child node of the deleted node - delete directly   Case d : find the smallest node in the right subtree , Use its value to fill in the deleted node , and then deal with the deletion of the node - replacement deletion

2.  Implementation of binary search tree

 2.1 Structure of binary search tree

template<class K>
	struct BSTreeNode
	{
		BSTreeNode(const K& key)
			:_key(key)
			, _left(nullptr)
			, _right(nullptr)
		{}

		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;
	};
template<class K>
	class BSTree
	{
	public:
		typedef BSTreeNode<K> Node;

    private:
		Node* _root = nullptr;
	};

2.2 Insertion of binary search tree

        What needs to be noted here is the problem of the link between the inserted node and the parent node, because the child node cannot find the parent node upwards, so it is necessary to define a pointer to the parent node in advance, and then go down step by step. Insert a new node after finding the position, and then link with the parent node. We need to pay attention when linking, because we don't know whether the inserted node is the left or right of the parent, so we need an if to help us judge whether it is left or right. Note that the same value cannot be inserted in the binary search tree! Moreover, the different order of insertion may also lead to different positions of nodes in the tree, and may also cause the tree to shift to one side, as shown in the following figure:

bool Insert(const K& key)
		{
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;

				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					return false;
				}
			}
			cur = new Node(key);
			if (parent->_key > key)
				parent->_left = cur;
			else
				parent->_right = cur;
			return true;
		}

 2.3 Find Find

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.4 Inorder traversal in InOrder

        Because in-order traversal requires a root node, we encapsulate one and call our own root node directly in the class.

void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
//下面的可以放到private里面
void _InOrder(Node* root)
		{
			if (root == nullptr)
				return;
			_InOrder(root->_left);
			cout << root->_key << ' ';
			_InOrder(root->_right);
		}

 2.5 Erase delete

        There are more considerations for deleting here, so it will be a little more complicated. First of all, find the node to be deleted first, and then there are three situations to consider:

1. The left subtree of the cur node is empty, so it is still the same as inserting, the parent node of the node to be deleted is also adjusted in place, the left subtree of the cur node is empty, and the right subtree of the cur node is entrusted to its parent node ( It is equivalent to letting the parent node skip his right subtree pointing to him), and then delete the node.

2. The right subtree of the cur node is empty, almost the same as the left subtree, just change the direction.

3. Neither the left subtree nor the right subtree of the cur node is empty. Here, it is mainly deleted by replacement. There are two values ​​to choose from, one is the maximum value of the left subtree, and the other is the minimum value of the right subtree, which one to choose It's okay, it depends on how you implement it. I chose the minimum value of the right subtree. The value selection here is not as simple as the figure below. It is necessary to define a pointer to find it. After finding it, you can directly assign the value of minRight (minimum value) to cur (it can also be exchanged), and then let the parent node point to the right node of minRight. Why is it right? Because minRight is the minimum value in the right subtree of cur, it is impossible to have a left node. Of course, the parent node still needs to be judged here. After the link is completed, minRight is deleted and it is over.

Of course, there is another kind, that is, the node corresponding to the value to be deleted cannot be found.

bool Erase(const K& key)
		{
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					//1.左为空
					if (cur->_left == nullptr)
					{
						if (cur == _root)
							_root = cur->_right;
						else
						{
							if (parent->_left == cur)
								parent->_left = cur->_right;
							else
								parent->_right = cur->_right;
						}
						delete cur;
					}
					//2.右为空
					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;
					}
					//3.都不为空
					else
					{
						Node* parent = cur;
						Node* minRight = cur->_right;
						while (minRight->_left)
						{
							parent = minRight;
							minRight = minRight->_left;
						}
						cur->_key = minRight->_key;
						if (minRight == parent->_left)
							parent->_left = minRight->_right;
						else
							parent->_right = minRight->_right;
						delete minRight;
					}
					return true;
				}
			}
			return false;
		}

2.6 Construction and copy construction

        The structure is relatively simple. The copy structure is to go through the method of preorder traversal and insert nodes one by one.

BSTree()
			:_root(nullptr)
		{}
	
		
		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;
		}

 2.7 Assignment overloading and destruction

        The modern way of writing for assignment overloading, if you don’t know it, you can review the blog of the previous linked list. The destructor is to use the post-order traversal method to delete, first delete the left node, then delete the right node, and then delete the root node.

        BSTree<K>& operator=(BSTree<K> t)
		{
            //现代写法
			swap(_root, t._root);
			return *this;
		}
		
        ~BSTree()
		{
			Destory(_root);
			_root = nullptr;
		}
		void Destory(Node* root)
		{
			//二叉树后续遍历删除
			if (root == nullptr)
				return;
			Destory(root->_left);
			Destory(root->_right);

			delete root;

		}

 2.8 Insertion (recursive)

        The parameter uses a very clever reference here, which makes the link very simple.

        For example, if you want to insert a value into the left subtree, the root is empty at this time, and a new node inserts a value. The root node is the reference of the left pointer of the parent node. If this reference is not added, then modifying the pointer in the function will not affect the function. For the pointer outside, even if the node is new, the pointer of the parent node outside still points to null and will not be affected. With reference added, root is the alias of the parent node pointing to the left subtree pointer. Modifying root at this time is equivalent to modifying the left pointer of the parent node.


		bool InsertR(const K& key)
		{
			return _InsertR(_root, key);
		}
		bool _InsertR(Node*& root,const K& key)
		{
			if (root == nullptr)
			{
				root = new Node(key);
				return true;
			}
			if (root->_key > key)
				return _InsertR(root->_left, key);
			else if (root->_key < key)
				return _InsertR(root->_right, key);
			else
				return false;
		}

 2.9 Delete (recursive)

        Find the node first, and delete the node in three cases: 1. The left node of the root is empty, save the root with a pointer in advance, and then let the right node of the root be given to yourself, because you are the pointer of the parent node, so it is quite Just let the parent node point to the right node pointing to the root (Tuogu), and finally release the root node saved in advance, and that’s it; 2. The right node of root is empty, which is similar to the first one; It is not empty, and the exchange value is used here, and then go to the right subtree of the root to delete the original key (delete 3 at the position circled in the picture below), so as to simplify the problem to deal with.

		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->_left, key);
			else if (root->_key < key)
				return _EraseR(root->_right, key);
			else
			{
				Node* del = root;
				//1.左为空
				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				//2.右为空
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				//3.都不为空
				else
				{
					Node* minRight = root->_right;
					while (minRight->_left)
					{
						minRight = minRight->_left;
					}
					//root->_key = minRight->_key;
					//return _EraseR(root->_right, minRight->_key);
					//转换到子树去删除
					swap(root->_key, minRight->_key);
					return _EraseR(root->_right, key);
				}
				delete del;
				return true;
			}
		}


3. Application of binary search tree 

1. K model: The K model only has the key as the value, and only the key needs to be stored in the structure , and the value is the value to be searched .
 For example: given a word word , judge whether the word is spelled correctly , the specific method is as follows:
Construct a binary search tree         with each word in all word collections in the thesaurus as a key
        Retrieve whether the word exists in the binary search tree, if it exists, the spelling is correct, if it does not exist, the spelling is wrong.
2. KV model: Each value key has a corresponding value Value , that is, a key-value pair of <Key, Value> . This approach is very common in real life:
        For example, the English-Chinese dictionary is the corresponding relationship between English and Chinese . Through English, you can quickly find the corresponding Chinese, English
A Chinese word and its corresponding Chinese <word, chinese> constitute a key-value pair;
        Another example is to count the number of words . After the statistics are successful, the number of occurrences of a given word can be quickly found.
The number of occurrences is <word, count> which constitutes a key-value pair .
namespace KV
{
	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)
			:_key(key)
			, _value(value)
			, _left(nullptr)
			, _right(nullptr)
		{}
	};
	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
	public:
		bool Insert(const K& key, const V& value)
		{
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				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;
				}
			}
			cur = new Node(key, value);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			else
			{
				parent->_left = cur;
			}
			return true;
		}
		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;
		}
		void Inorder()
		{
			_Inorder(_root);
		}

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

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

	void TestBSTree2()
	{

		// Key/Value的搜索模型,通过Key查找或修改Value
		KV::BSTree<string, string> dict;
		dict.Insert("sort", "排序");
		dict.Insert("string", "字符串");
		dict.Insert("left", "左边");
		dict.Insert("right", "右边");

		string str;
		while (cin >> str)
		{
			KV::BSTreeNode<string, string>* ret = dict.Find(str);
			if (ret)
			{
				cout << ret->_value << endl;
			}
			else
			{
				cout << "无此单词" << endl;
			}
		}
	}
	void TestBSTree3()
	{
		// 统计水果出现的次数
		string arr[] = { "苹果", "西瓜", "香蕉", "草莓","苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

		KV::BSTree<string, int> countTree;
		for (auto e : arr)
		{
			auto* ret = countTree.Find(e);
			if (ret == nullptr)
			{
				countTree.Insert(e, 1);
			}
			else
			{
				ret->_value++;
			}
		}

		countTree.Inorder();
	}
}


 4.  Performance Analysis of Binary Search Tree

        Both insertion and deletion operations must be searched first, and the search efficiency represents the performance of each operation in the binary search tree.
For a binary search tree with n nodes, if the probability of finding each element is equal, then the average search length of the binary search tree is
A function of the depth of the fork search tree, that is, the deeper the node, the more comparisons. But for the same set of values, if the order of insertion of each value is different, a binary search tree with a different structure may be obtained:

 In the optimal case, the binary search tree is a complete binary tree ( or close to a complete binary tree ) , and its average number of comparisons is: O(logN);

In the worst case, the binary search tree degenerates into a single branch tree ( or similar to a single branch ) , and its average number of comparisons is: O(N);

This article is over, if you have any questions or don't understand, you can comment or private message. Thank you all for watching!

Guess you like

Origin blog.csdn.net/weixin_68993573/article/details/128892720