[C++] Ordinary binary search tree implementation

Table of contents

1 Basic concept of binary search tree

2 Construction of binary search tree

2.1 Nodes of binary search tree

2.2 The structure of the search tree class

3 member functions

3.1 Insert

3.2 Search

3.3 Delete (emphasis)

3.4 Auxiliary functions for default member functions

4 Efficiency of ordinary binary search tree


1 Basic concept of binary search tree

        A binary search tree, also known as a binary sort tree, is either an empty tree, and it has the following characteristics:

1. If its left subtree is not empty, then all node values ​​of the left subtree are less than the value of the root node.

2. If its right subtree is not empty, then all node values ​​of the right subtree are greater than the value of the root node.

3. Its left and right subtrees also conform to the characteristics of a binary search tree.

        The following figure is a visual display of a binary search tree:

         As shown in the figure, it can be intuitively felt that when a tree is a binary search tree, its structural characteristics satisfy the concepts I described before, and, through the access method of my arrow, it can also be seen that in the end its The access result must be an ascending sequence, which is why the binary search tree has practical significance compared with the ordinary binary tree.

        So on this basis, we can perform meaningful comparisons, insertions, deletions, and other operations on the nodes of this tree.

2 Construction of binary search tree

2.1 Nodes of binary search tree

template<class K>
struct BinaryNode
{
    K _key;
    BinaryNode<K>* _left;
    BinaryNode<K>* _right;

    BinaryNode<K>(const K& key)
        :_key(key),_left(nullptr),_right(nullptr){}
};

        As a binary tree, it must have its own nodes. Here, the blogger uses the simplest binary chain method to show you, that is, ordinary data, left nodes, right nodes, plus a reference structure.

        For bloggers, I don't quite support the way of adding a no-argument structure to the search tree, because as a binary search tree, each node has its own independent meaning. If you add a no-argument structure, then What practical significance does the generated node have for us? It is better not to open this function directly to the outside world, so that users can feel the actual problems when they are writing.

        Then because we want to perform paradigm programming, it is inevitable that we need to use template-related knowledge, and for ordinary binary tree structures, what is needed is nothing more than stored data, or its comparison method needs to add templates. For example, part of the data is a structure, such as a pair, but the blogger here defaults it to be an ordinary single variable. After all, it is just the beginning, and the blogger does not intend to increase your burden.

2.2 The structure of the search tree class

template<class K>
class BSTree
{
    typedef BinaryNode<K> Node;
public:
    BSTree()
    {

        _root = nullptr;

    }

     BSTree(const BSTree<K>& copy)
    {
        _root = _copy(copy);
    }

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

    ~BSTree()
    {
        destroy(_root);
    }

private:
    Node* _root;
};

        The blogger does not intend to explain the structure of the search tree. It is composed of its default function and a root node pointer. There is no need to explain it. There are some actual functions in the default member function. The blogger intends to explain it below. A section for everyone to explain.

3 member functions

3.1 Insert

//插入
	bool insert(const K& key)
	{
		//第一次插入
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		//之后的插入,要保证之后的连接
		Node* cur = _root;

		while (cur)
		{
			//大于往右走
			if (key > cur->_key)
			{
				if (cur->_right == nullptr)
				{
					cur->_right = new Node(key);
					return true;
				}
				cur = cur->_right;
			}
			//小于往左走
			else if (key < cur->_key)
			{
				if (cur->_left == nullptr)
				{
					cur->_left = new Node(key);
					return true;
				}
				cur = cur->_left;
			}
			//已经有了该数据,插入失败
			else
			{
				return false;
			}
		}

		//不可能走到的位置
		return false;
	}

        The insertion of ordinary binary search tree is very simple, because its characteristics guarantee the efficiency of our insertion, as follows:

         If our binary tree is a chain structure, it is bound to make each of our insertions O(N) time complexity, but since it is a search tree, according to the characteristics of the search tree, ideally, we One comparison can eliminate half of the options, which means that the insertion efficiency becomes O(log2_N). Of course, there is generally no such a good structure, and we will explain it later.

        Looking back at the code, first of all, we have to ensure that the time is the first insertion, that is, at the beginning, the root of our search tree object is initially empty, we need to modify it to ensure its validity, otherwise we will access the root node later When pointing, there will be a problem that dereferencing the root node will cause the program to crash.

        After that, it is a question of finding the insertion position. By comparing the relationship between the current node and the key value of the inserted node through a loop, and then considering whether to move to the left or to the right, it will be indicated when the next node to be moved is detected to be empty. Find the position that needs to be inserted, and connect it at this position through the new node.

        Some friends may have asked before, what should we do if we already have the inserted value? Obviously, there is no good way to solve it in our tree structure, but it is not impossible to solve it. The learning library container multimap can also solve it, that is, specify its left node at the position of this node, or The position of the right node is inserted in the same way as a linked list, and the rest of the structure remains unchanged. The blogger doesn't want to solve it this way, so fortunately, it just returns an insertion failure, which is simple and rude.

3.2 Search

//查询
	bool find(const K& key) const
	{
		//树内还没有数据
		if (_root == nullptr)
		{
			return false;
		}

		//查找
		Node* cur = _root;
		while (cur)
		{
			if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else
			{
				return true;
			}
		}
		return false;
	}

        It is even easier to search, and the blogger does not want to explain it, which is basically the same as the insertion method.

3.3 Delete (emphasis)

question:

        After reading the above two pieces of code, I believe that everyone will think in their hearts, "This is the search tree? Doesn't it feel difficult? I can do it myself." If you really think so, then you can only underestimate it. , please think about the following, if we want to delete a node, how should we delete it?

        Let me give you a little hint, delete the leaf node, delete the node with only one child, delete the node with two children, delete the root node, in general, these are the situations, see if you can think of a way to solve this What's the problem?

         Seeing the search tree above, for example, I want to delete node 42, node 45, node 15, and node 30.

explain:

        Let's start with a simple question. What should it be like to delete a leaf node?

        First of all, a leaf node is a node without children, and only the parent node is connected to it upwards, so who will only be affected by its deletion? That is its parent's child node, is it related to the rest of the nodes? No, then after deletion, it will become the following picture:

         It's easy and there are no problems, so let's start the next question, what should I do to delete 45?

        Deleting 45 is to delete a node with one child. Deleting itself is not important. The important thing is, how do we keep its child still in our structure? At this time, we need to find someone to help manage it. Who is qualified? Of course, it is the father node. If you are on the left of the father, then you and your child nodes will be smaller than the parent node, and vice versa.

        Also, when connecting, it is necessary to determine which node needs to be managed, and it is the parent node that manages it. Then the following diagram will be presented to us in the end: it has no effect on the structure.

         Finally, what should I do if I delete the node at position 15 or 30? Does our parent node have the ability to help us manage two children. So at this time, we need to hire a nanny, so what kind of person can be our nanny? I don’t buy it anymore, it’s very simple, the nanny can only be the node with the largest left subtree, or the node with the smallest right subtree, once this condition is met, then the corresponding nanny can only be There is only one child or there is no child, do we have a way to delete nodes with no child and with one child? have! Just now.

        So why use the left largest or right smallest node to be a nanny? Please observe the picture below:

         As shown in the figure above, we overwrite the data at the original deletion location with the data of these two nodes. Does it mean that we have deleted this node? After all, the node itself is meaningless, what is meaningful is its data, and after we replace it, is it still a binary search tree? Yes, then do we have the ability to delete the new deletion location directly? able. So why is it possible to do this? Because the root of the largest node of the left subtree must be smaller than the right subtree and larger than the left subtree, the same is true for the smallest node of the right subtree.

        So in this way, we can realize the deletion of a node with two children, and because the logic of the two children needs to repeatedly include deleting a node and deleting two nodes, then the writing order allows it to be written At the front, if the function is split into a function, you don't need to think about it, just call it directly.

code:

//删除
bool erase(const K& key)
{
	//为空不能删除
	if (_root == nullptr)
	{
		return false;
	}

	//找到需要删除的那个结点位置
	//保留父节点
	Node* prev = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (key > cur->_key)
		{
			prev = cur;
			cur = cur->_right;
		}
		else if (key < cur->_key)
		{
			prev = cur;
			cur = cur->_left;
		}
		else
		{
			break;
		}
	}
	//没有找到
	if (cur == nullptr) return false;

	//找到了需要分多种情况,没有孩子,有一个孩子,有两个孩子
	if (cur->_left != nullptr && cur->_right != nullptr)
	{
		//一定有左结点
		prev = cur;
		Node* sub = cur->_left;

		//找左子树的最大结点,在左子树的最右位置
		while (sub->_right != nullptr)
		{
			//找到的数据一定只有一个孩子或者是没有孩子
			prev = sub;
			sub = sub->_right;
		}
		//用值去覆盖,然后更换新的删除目标
		cur->_key = sub->_key;
		cur = sub;
	}

	//没有孩子
	if (cur->_left == nullptr && cur->_right == nullptr)
	{
		//需要去掉父节点的指向
		if (prev != nullptr)
		{
			if (prev->_left == cur) prev->_left = nullptr;
			else prev->_right = nullptr;
		}
		delete cur;
		return true;
	}
	//有一个孩子,需要用父节点来帮忙管理
	else if ((!cur->_left && cur->_right) || (cur->_left && !cur->_right))
	{
		//父节点为空,表示需要删除的位置是根节点,那么此时直接把子节点放上来就行
		if (prev == nullptr)
		{
			//更新根节点为它的不为空的那一个孩子
			_root = cur->_left == nullptr ? cur->_right : cur->_left;
		}
		//左节点不为空
		else if (cur->_left != nullptr)
		{
			//判断需要父节点的哪一个孩子结点去接收
			if (prev->_left = cur)
				prev->_left = cur->_left;
			else
				prev->_right = cur->_left;
		}
		//右节点不为空
		else
		{
			//判断该节点连接到父节点的哪一个位置
			if (prev->_left = cur)
				prev->_left = cur->_right;
			else
				prev->_right = cur->_right;
		}
	}
	delete cur;
	return true;
}

3.4 Auxiliary functions for default member functions

 Code: (the logic itself is relatively simple, the blogger does not explain it)

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

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

	destroy(root->_left);
	destroy(root->_right);

	delete root;
	root = nullptr;
}

4 Efficiency of ordinary binary search tree

        I believe everyone has also seen that the efficiency of searching a binary tree is better than our linked list, but it is unstable. Why? Because he may become the following situation:

        Is this a binary search tree? Yes, but what is its search efficiency? O(N), isn’t this nonsense? I spent a long time writing this damn thing. The ordinary binary search tree is not reliable, so it proves that the underlying structure used by the container map and set is not it , but its upgraded version, some are AVL trees, some are red-black trees, but the mainstream way of writing is red-black trees. In this article, the blogger does not intend to explain these two data structures, and will share them with you later.


        The above is the blogger's full understanding of the ordinary search binary tree, and I hope it can help everyone.

Guess you like

Origin blog.csdn.net/weixin_52345097/article/details/130620052