C++【Binary Search Tree】

1. Binary search tree

(1) Concept

Binary search tree alias binary sorting tree is built into BST, it may be an empty tree, or it may be a binary tree with the following properties: if its left subtree is not empty, the values ​​of all nodes on the left subtree are all is 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, and its left and right subtrees are also binary search trees. As shown in the picture:
insert image description here

(2) Operation

There are three operations on a binary tree: search, insert, and delete.
Search : Start to compare and search from the root. If it is larger than the root, go to the right to search, and if it is smaller than the root, go to the left to search. At most, the search height times. If you go to the empty space, you have not found it yet, and this value does not exist.
Insert : If the tree is empty, add a node directly. If the tree is not empty, find the insertion position according to the nature of the binary search tree, and insert a new node. Delete
: first
check whether the element is in the binary search tree, and return if it does not exist, and vice versa There may be three cases for the node to be deleted
: the leaf node is classified into the first and second cases.
1. If the right side of the deleted node is empty, delete the node and make the parent node of the deleted node point to the left child node of the deleted node.
2. If the left side of the deleted node is empty, delete the node and make the parent node of the deleted node point to the right child node of the deleted node, which is also directly deleted like the first one.
3. Find the largest node (the rightmost node) of the left subtree, find the smallest node (the leftmost node) of the right subtree, use its value to fill in the deleted node, and then deal with the deletion of the node. It is equivalent to deletion by substitution method, indirect deletion.

(3) Application

There are two models:
K model: K model means that only the key is used as the key code, and only the Key needs to be stored in the structure, and the key code is the
value to be searched.
Example: Given a word hello, determine whether the word is spelled correctly. The specific method is as follows:
use each word in all the word sets in the thesaurus as a key, build a binary search tree, and retrieve the word in the binary search tree Whether it exists, the spelling is correct if it exists, and the spelling is wrong if it does not exist.
2. KV model: Each key code has a corresponding value Value, that is, a key-value pair of <Key, Value>.
For example: the English-Chinese dictionary is the corresponding relationship between English and Chinese. You can quickly find the corresponding Chinese through English. An English word and its corresponding Chinese <word, Chinese> constitute a key-value pair; another example is to count the number of words
. , given a word, you can quickly find the number of times it appears, and the word and its number of occurrences is <word, count> to form a key-value pair.

2. BST simulation implementation and function analysis

(1) Construct BST node structure

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

Nodes make up a tree, so now build a structure and add attributes in it. It needs to have a left pointer left and a right pointer right to point to the left child and right child respectively. It needs to store the value key of the node. Here they need to be initialized, so Also write a constructor inside.

(2) BST default construction and copy construction

        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;
			}

Here we use default to force the generation of the default structure, and the copy structure involves deep copying. We then encapsulate a copy function. If it is empty, return nullptr directly, otherwise new creates a node, and then recurses the left subtree and the right subtree respectively. Finally return to the root node.

(3) BST assignment overload and destructor

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;
			}

For assignment overloading, it also involves deep copying, and only needs to exchange the pointers or addresses of the root nodes of the two trees. The destructor needs to be released one by one and empty at the end.

(4) Non-recursive implementation of BST three operations

(1) Find

       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;
		}

To write the search in a non-recursive way, you only need to use the while loop and the nature of BST. In the loop: starting from the root node, if I am older than you, go to the right and update the current position at the same time. If I am younger, go to the left and update the current position. Otherwise, return true if found, go to the empty end, and return false if it has not been found at the end of the traversal.

(2) delete

        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;
		}

The idea of ​​deletion is to first use the BST property to find the node to be deleted, go to the right if it is older than you, and go to the left if it is younger than you, here is also to record the parent node for subsequent deletion, and finally use the three mentioned above The situation is deleted.
1. If the left side of the deleted node is empty, first judge whether the node is the root node, if it is, give the root node the right subtree of the node, let it act as the root node, and then release the node; if it is not the root node node, I will make two judgments in it, if the right child of the parent node is the current node, link the right child of the current node with the right pointer of the father, otherwise, link the right child of the current node with the Father's left pointer link. Finally release the node. As in the previous figure: delete 1 and 6.
2. If the right side of the deleted node is empty, it is also necessary to first judge whether the node is the root node. If so, give the root node the left subtree of the node, let it act as the root node, and then release the node; if If it is not the root node, two judgments need to be made inside. If the right child of the parent node is the current node, link the left child of the current node with the right pointer of the father, otherwise, the left child of the current node and parent's left pointer link. As in the previous picture: delete 14.
3. If the left and right sides of the deleted node are empty, then you need to use the substitution method to find a suitable node to act as the existing node. The appropriate node value is to find the largest node of the left subtree, which is the rightmost node, or the smallest node of the right subtree, which is the leftmost node. Here is the rightmost node to find. When traversing the search, you need to record the parent node pmiR and the smallest node miR of the smallest node. After finding it, overwrite it, and give the value of the smallest node to the value of the node to be deleted, and finally link. Two judgments need to be made here. If the left child of the parent node is the smallest node, the right child of the smallest node needs to be linked with the left pointer of the father, otherwise, the right child of the smallest node should be linked with the right pointer of the father.

(3) insert

        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;
		}

The idea of ​​insertion is to first find the empty space to insert, use the BST property, traverse the left and right subtrees, and record the parent node at the same time, and then insert the new new node. Two judgments are required. If it is larger than it, link the right pointer of the parent node, otherwise link Parent node left pointer.

(5) Recursively implement three operations of BST

(1) Find

        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);
		}

The recursive search method still uses the BST property. If it is larger than it, it will recurse the right subtree, if it is smaller than it, it will recurse the left subtree. If it is found, it will return true, and if it is empty, it will return false.

(2) insert

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);
		}

Recursive insertion also uses the nature of BST. First recurse the left and right subtrees, and insert when it is empty, but the insertion must be linked with the father. How to link? We can pass references without adding parameters, which is the optimal solution, because it worked for the last time. In fact, the root at this time is an alias for the left pointer or right pointer of the previous root, and a new one is created here. The node is given to root, which is indirectly connected.

(3) delete

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;
			}

		}

Recursive deletion, using the BST property to find the node to be deleted, recursive left and right subtrees, there is no need to record the parent node here, and there are also three cases.
1. The left side of the deleted node is empty, define a del pointer variable, give it the currently deleted node, and then link, use the reference, no need to care which pointer the parent node is linked to, directly link, and release del.
2. Same as 1.
3. The left and right sides of the deleted node are not empty, and a suitable node is found to replace it. Here, the largest node of the left subtree is found, that is, the rightmost node. After finding it, the deleted node and the largest node are Value exchange, do recursion again to delete the largest node of the left subtree, at this time the key stored by mal will not enter the third condition, because at this time mal's child either has one or does not, so it will Is it the first case or the second case, no matter which case we can use the reference, find the father and then link.
Note here that the second recursive pointer here cannot be directly passed to mal, because if it is passed, our reference will be useless and the link will not work, because it will enter the first or second condition, what we want is the previous position References are not references at this location.

3. BST implementation source code

(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();


}

4. BST running results

insert image description here

Five, the performance of the 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.
The binary search tree is a complete binary tree (or close to a complete binary tree), and the number of times is: O(logN).
insert image description here
The binary search tree is a single-branch tree, and the number of times is: O(N^2).
insert image description here
If it degenerates into a single-branch tree, the performance of the binary search tree is lost. It can be improved, no matter in what order the key codes are inserted, the performance of the binary search tree can reach the best, and the AVL tree and the red-black tree can solve this problem.

Guess you like

Origin blog.csdn.net/m0_59292239/article/details/130300614