Algorithm analysis and example demonstration of searching binary trees

insert image description here

1. Characteristics and implementation of searching binary trees

1. Features

Binary search tree is a special binary tree with more strict data structure characteristics:
(1) All key values ​​of the non-empty left subtree are less than the key value of its root node.
(2) All key values ​​of the non-empty right subtree are greater than the key values ​​of its root node.
(3) The left and right subtrees are both binary search trees.
As shown in the figure below, it is a typical search binary tree:
insert image description here
the result of in-order traversal of this structure is in ascending order. The above characteristics can help us solve many problems. For example, find

2. Realize

The search function logic of searching a binary tree is relatively simple. The value to be searched is compared with the current node key value in sequence. If it is less than the value, continue searching in the left tree, otherwise search in the right tree .

	bool find(const T& 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;
	}

The insertion function first needs to find where the value to be inserted should be placed, and then determines whether it is the left tree or right tree of the parent node and then inserts it. When the same key value as the value to be inserted is found, the insertion fails because Searching a binary tree does not allow the same value to exist. It should be noted that when the tree is an empty tree, the value can be inserted directly:

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

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

The important thing to understand is the next delete function: to delete a key value, you first need to find the node. The general logic is similar to the find function, but the difference is how to delete the node when found. At this time, there are three situations: the left subtree of the node is empty/the right subtree of the node is empty/neither the left nor right subtree is empty. When the left or right subtree is empty, the logic is simpler and you can use the Tool method . First determine whether it is the root node. If so, directly assign the root node to the right or left node of the root node and then determine whether it is the left/right of the parent node. node, and then let the left/right of the parent node point directly to my left or right node. When both the left and right subtrees are not empty , the replacement method needs to be used to replace the node to be deleted with the key value of the largest node of the left subtree or the key value of the smallest node of the right subtree ( because it is necessary to ensure the characteristics of searching the binary tree ) and then delete it.

bool Erase(const T& key)
	{
    
    
		Node* cur = _root;
		Node* parents = nullptr;

		if (cur == nullptr)
			return false;
		while (cur)
		{
    
    
			if (cur->_key > key)
			{
    
    
				parents = cur;
				cur = cur->left;
			}
			else if (cur->_key < key)
			{
    
    
				parents = cur;
				cur = cur->right;
			}
			else
			{
    
    
				if (cur->left == nullptr)
				{
    
    	if (cur == _root)
					{
    
    
						_root = cur->right;
					}
					else
					{
    
    
						if (parents->left == cur)
						{
    
    
							parents->left = cur->right;
						}
						else
						{
    
    
							parents->right = cur->right;
						}
					}
				}
				else if (cur->right == nullptr)
				{
    
    
					if (cur == _root)
					{
    
    
						_root = cur->left;
					}
					else
					{
    
    
						if (parents->left == cur)
						{
    
    
							parents->left = cur->left;
						}
						else
						{
    
    
							parents->right = cur->left;
						}
					}
				}
				else//左子树和右子树都不为空
				{
    
    
					Node* parents = cur;
					Node* leftmax = cur->left;
					while (leftmax->right)
					{
    
    
						parents = leftmax;
						leftmax = leftmax->right;
					}
					swap(cur->_key, leftmax->_key);

					if (parents->left == leftmax)
					{
    
    
						parents->left = leftmax->left;
					}
					else
					{
    
    
						parents->right = leftmax->left;
					}
					cur = leftmax;

				}
				delete cur;
				return true;
			}
		}
		return false;
	}

The above explanation is the implementation of the non-recursive version. The recursive version of the search, insertion and deletion code is more concise, but more difficult to understand.
Because calling member functions outside the class cannot pass parameter member variables to the function, some encapsulation is done in the class :

public:

	bool findR(const T& key)
	{
    
    
		return _findR(_root, key);
	}
	
private:

	bool _findR(Node* root, const T& key)
	{
    
    
		if (root == nullptr)
		{
    
    
			return false;
		}
		if (root->_key > key)
		{
    
    
			return _findR(root->left, key);
		}
		else if (root->_key < key)
		{
    
    
			return _findR(root->right.key);
		}
		else
		{
    
    
			return true;
		}
	}

The search and delete functions also use encapsulation:

public:

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

	bool EraseR(const T& key)
	{
    
    
		return _EraseR(_root, key);
	}
	
private:
	
	bool _EraseR(Node*& root, const T& 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;
			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 T& 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. Performance of searching binary trees

The performance of the search is logN when the search binary tree is a complete binary tree or a full binary tree or close to it.
insert image description here
In extreme cases, the average time complexity is N/2.

Guess you like

Origin blog.csdn.net/qq_43289447/article/details/132476682