Data structure and algorithm [tree]

Binary tree properties

full binary tree

insert image description here
The depth is k, there are 2 k − 1 2^{k}-12kA binary tree with 1 node is a full binary tree.

complete binary tree

The definition of a complete binary tree is as follows: In a complete binary tree, except for the bottom node that may not be filled, the number of nodes in each layer reaches the maximum value, and the nodes in the bottom layer are all concentrated in the leftmost positions of the layer. If the bottom layer is the hth layer, then this layer contains 1~ 2^(h-1) nodes.

Binary tree storage method

Including chained storage and sequential storage
Since the binary tree of chained storage is more conducive to our understanding, we generally use chained storage of binary trees.
So everyone should understand that a binary tree can still be represented by an array.

insert image description here

Binary tree chain storage code

struct TreeNode{
    
    
	int val;
	TreeNode *left;
	TreeNode *right;
	TreeNode(int x):val(x),left(NULL),right(NULL){
    
    }
};

How to traverse a binary tree

There are two types of traversal methods, four

Regarding the traversal method of the binary tree, it is first distinguished from the depth and breadth.

  1. Depth-first traversal: Go deep first, and then go back when you encounter a leaf node.
  2. Breadth-first traversal: traverse layer by layer.
    These two traversals are the most basic two traversal methods in graph theory, which will be introduced later when graph theory is introduced.

Then we further expand the depth-first traversal and breadth-first traversal to have a more detailed distinction of traversal methods:

  • Depth-first traversal
    • Preorder traversal (recursive method, iterative method)
    • Inorder traversal (recursive method, iterative method)
    • Post-order traversal (recursive method, iterative method)
  • breadth first traversal
    • Hierarchical traversal (iteration method)
      in depth-first traversal: there are three orders, front, middle and back order traversal, some students can't distinguish these three orders, and often get confused, I will teach you a technique here.
      The front, middle, and back here actually refer to the traversal order of the intermediate nodes . As long as you remember, the front, middle, and rear order refers to the position of the intermediate nodes. Looking at the traversal order of the following nodes, you can find that the order of the intermediate nodes is the origin of the so-called traversal method name:
  • Pre-order traversal: middle, left, right
  • Inorder traversal: left center right
  • Post-order traversal: left, right, middle

Implementation of traversal

Finally, let’s talk about the implementation of depth-first traversal and breadth-first traversal in binary trees. When we do topics related to binary trees, we often use recursive methods to achieve depth-first traversal.
When I talked about the stack before, I said that the stack is actually a recursive implementation structure, first in last out. That is to say, the logic of front-middle-back order traversal can actually be realized in a non-recursive way with the help of the stack. (Avoid recursive operations through the structure of the stack)

The implementation of breadth-first traversal is generally realized by means of queues, which is also determined by the characteristics of first-in-first-out queues, because a first-in, first-out structure is required to traverse the binary tree layer by layer.

In fact, here we have learned about an application scenario of stacks and queues.
We will talk about the specific implementation later, here we must first understand these theoretical foundations.

Binary tree and recursion (recursive traversal of binary tree)

When it comes to binary trees, we have to talk about recursion. Many students are both familiar and unfamiliar with recursion. The recursive code is generally very short, but every time it is easy to read, it is useless when you write it.

The root cause of bad recursive writing is that it is not systematic and there is no recursive methodology. Through the recursive writing method of the front, middle and back order of the binary tree, we determine the recursive methodology, and then deal with complex recursive topics.

First of all, every time you write a recursive algorithm, first determine the three elements:

  1. Determine the parameters and return values ​​of the recursive function: determine which parameters need to be processed during the recursion process, then add this parameter to the recursive function, and also clarify what the return value of each recursion is to determine the return type of the recursive function .
  2. Determine the termination condition: After writing the recursive algorithm, when it is running, it often encounters a stack overflow error, that is, the termination condition is not written or the termination condition is written incorrectly. The operating system also uses a stack structure to save the information of each layer of recursion , if the recursion is not terminated, the operating system's memory stack will inevitably overflow.
  3. **Determine the logic of single-layer recursion: **Determine the information that needs to be processed for each layer of recursion. Here, the process of calling itself repeatedly to achieve recursion will also be repeated.

How to cut in and find the three elements?
Let's take preorder traversal as an example to find the feeling!
1. Determine the parameters and return value of the recursive function: because we want to print the value of the pre-order traversal node, we need to pass in a vector to put the value of the node in the parameter. Apart from this, we don’t need to process any data, and we don’t need There is a return value, so the return type of the recursive function is void. The code is as follows
: Except for the vector, there is no need to process any data, and there is no need to have a return value, but the purpose of passing in TreeNode *cur is not mentioned, but my guess is that to process the tree itself, it must be passed in to the currently processed tree The pointer of the node, as to why it is necessary to use vector to store the value of the node, I can’t figure it out, I hope I will figure it out later)

void traversal(TreeNode* cur, vector<int>& vec)

2. Determine the termination condition: in the recursion process, how to calculate the end of the recursion? For preorder traversal, if the currently traversed node is empty, it means that the recursion is over, so if the current traversed node is empty, return directly, the code is as follows:

if (cur == NULL) return;

3. Determine the logic of single-level recursion: the pre-order traversal is in the order of middle, left, and right, so the logic of single-level recursion is to first take the value of the midpoint node, (the concept of single-level recursive logic does not make sense!! First of all, what Is it single-layer recursion? What if the process of repeatedly calling single-layer recursion to achieve recursion? My understanding is that every time a new data is read, how should the data be processed? This is the data processing logic of single-layer recursion!!! After the data processing is completed , it is the logic of recursion, the single-level recursion is essentially over after the data is processed!!! followed by the logic of recursion, for example, the next data processed by the recursion here is the node of the left subtree, so for the left subtree Perform recursion, and then process the right subtree, and continue to recurse on the right subtree here!! Therefore, the single-level recursion mentioned here is essentially a data processing process + the logic of the data that needs to be processed later, that is, the logic that needs to be processed later The recursive arrangement of which data is actually just calling this recursive data processing method here, but the order of recursion needs to be based on the needs of the problem, such as the pre-order traversal here, so the read-in root needs to be printed and stored in the result first, Then print the data of the left and right subtrees. !!!)

Therefore, the code is as follows

vec.push_back(cur->val);    // 中
traversal(cur->left, vec);  // 左
traversal(cur->right, vec); // 右

Up to this point, I still can’t understand it. The reason is that I don’t know the meaning of TreeNode* cur and vector &vec. ! !

However, when readers continue to read and read the overall code, they will suddenly realize:

class Solution {
    
    
public:
    void traversal(TreeNode* cur, vector<int>& vec) {
    
    
        if (cur == NULL) return;
        vec.push_back(cur->val);    // 中
        traversal(cur->left, vec);  // 左
        traversal(cur->right, vec); // 右
    }
    vector<int> preorderTraversal(TreeNode* root) {
    
    
        vector<int> result; //vector就是我们的遍历结果存储向量
        traversal(root, result);//遍历需要输入树的根节点
        return result;
    }
};

Vector is our traversal result storage vector; traversal needs to go down step by step according to the tree, so the root of the tree needs to be passed in, and the root of the tree needs to be passed in, so the parameter TreeNode* cur needs to be passed in.

So we write the logic of in-order and post-order traversal:

  1. **Parameters and return values ​​of the recursive function: a recursion, that is, a data processing unit includes the vector of the incoming tree and the incoming storage list, and does not need to return a value, just do data processing.
  2. **Termination condition: when encountering a space point, return immediately
  3. The logic of single-layer recursion: find a suitable location to read node data, and arrange the subsequent recursive data flow.

Inorder traversal

void traversal(TreeNode *cur, vector<int>& vec){
    
    
	if(cur==NULL) return;
	traversal(cur->left, vec);
	vec.posh_back(cur->val);
	traversal(cur->right, vec);
}

post order traversal

void traversal(TreeNode *cur, vector<int>& vec){
    
    
	if (cur==NULL) return;
	traversal(cur->left, vec);
	traversal(cur->right, vec);
	vec.push_back(cur->val);
}

Non-recursive traversal of binary tree (iterative traversal)

The concept of iteration: Baidu Encyclopedia
Iteration is the activity of repeating the feedback process, its purpose is usually to approach the desired goal or result. Each repetition of the process is called an "iteration", and the result of each iteration is used as the initial value for the next iteration.
In computers, we usually use loop structure programming to realize this iterative process. (The other two types: select structural programming, modular programming implemented with functions "Tan Haoqiang C Programming")

We know in the stack and queue part that the implementation of recursion is: each recursive call will push the local variables, parameter values ​​and return address of the function onto the stack, (it can also be understood as, once recursion is encountered, the current recursion The current execution state of the stack is pushed into the stack) When the recursion returns, the parameters of the last recursion are popped from the top of the stack, so this is why the recursion can return to the previous level. (It can also be understood that when the recursion returns, the recursive state that was previously pushed into the stack is sequentially fetched from the stack, and then executed, and then the same operation is used again when encountering recursion)

Therefore, we can also use the stack to realize the traversal of the binary tree, which is essentially the process of simulating recursion with the stack. This process is called iteration.

Iterative writing of pre-order traversal

class Solution {
    
    
public:
    vector<int> preorderTraversal(TreeNode* root) {
    
    
        stack<TreeNode*> st;
        vector<int> result;
        if (root == NULL) return result;
        st.push(root);
        while (!st.empty()) {
    
    
            TreeNode* node = st.top();// 中                 
            st.pop();
            result.push_back(node->val);
            if (node->right) st.push(node->right);// 右(空节点不入栈)           
            if (node->left) st.push(node->left);// 左(空节点不入栈)             
        }
        return result;
    }
};

At this point, you will find that it seems that it is not difficult to write a preorder traversal using the iterative method, and it is indeed not difficult.

At this time, do you want to change the order of the pre-order traversal code a little bit to get out the in-order traversal?

In fact, it really doesn't work!

But then, when you use the iterative method to write in-order traversal, you will find that the routine is different. The current logic of pre-order traversal cannot be directly applied to in-order traversal.

Inorder traversal (iterative code writing)

class Solution{
    
    
public:
	vector<int> inorderTraversal(TreeNode* root){
    
    
		vector<int> result;
		stack<TreeNode*> st;
		TreeNode* cur = root;
		while(cur!=NULL||!st.empty()){
    
    
		//只要当前的指针不为空或者栈不空
		if(cur!=NLL){
    
    //如果指针不空
			st.push(cur);//访问节点入栈
			cur=cur->left;//继续往左走
			
		}else{
    
    
			//当往左走空了,开始出栈
			cur = st.top();
			st.pop();
			result.push_back(cur->val);
			cur = cur->right; //往右走
		}
		}
		return result;
	}
	
};

post order traversal

class Solution{
    
    
public:
   vector<int> postorderTraversal(TreeNode* root){
    
    
   	stack<TreeNode*> st;
   	vector<int> result;
   	//注意,当root为空时要返回;
   	if (root==NULL) return;
   	st.push(root);
   	while(!st.empty()){
    
    
   			TreeNode* node = st.top();
   			st.pop();
   			result.push_back(node->val);
   			if (node->left!=NULL) st.push(node->left);
   			if (node->right!=NULL) st.push(node->right);
   		}
   	reverse(result.begin(),result.end());
   	return result;

   }
}

Iterative traversal of binary tree (unified format of front, middle and back order)

class Solution {
    
    
public:
    vector<int> inorderTraversal(TreeNode* root) {
    
    
        vector<int> result;
        stack<TreeNode*> st;
        if (root != NULL) st.push(root);
        while (!st.empty()) {
    
    
            TreeNode* node = st.top();
            if (node != NULL) {
    
    
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node->right) st.push(node->right);  // 添加右节点(空节点不入栈)

                st.push(node);                          // 添加中节点
                st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。

                if (node->left) st.push(node->left);    // 添加左节点(空节点不入栈)
            } else {
    
     // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.top();    // 重新取出栈中元素
                st.pop();
                result.push_back(node->val); // 加入到结果集
            }
        }
        return result;
    }
};

Binary tree level order traversal

Two sets of codes (just memorize)

iterative implementation


recursive implementation

Binary tree recursion with backtracking

"Code Caprice Record" Algorithm Video Open Class: Recursion brings backtracking, do you feel it? | LeetCode: 257. All paths of the binary tree(opens new window), I believe that watching the solution of this question in combination with the video will be more helpful for everyone to understand this question.
Backtracking (also called backtracking)
backtracking function is actually a recursive function, because there is no single implementation of backtracking function, backtracking is a concept implied in recursion.

How to understand the backtracking method: backtracking is a recursive process, and the recursion must be terminated. The problems to be solved by the backtracking method can be abstracted into an n-ary tree. The width of this tree is the size of the set of problems we want to deal with. , we use a for loop to traverse; the depth of this tree is the depth of the recursion, and after the termination, it will be reversed layer by layer.

insert image description here

Generally speaking, the recursive function in the backtracking method has no return value, that is, void. The name of these recursive functions is generally backtracking. Of course, everyone can have their own habits, but it is generally called this way in the industry. The parameters of the backtracking method are generally There are many cases, so it is not convenient to set the parameters at the beginning. We can add parameters here when we write the logic part. Then we have to carry out a termination condition, because the recursion must be terminated. When the termination condition is reached, it is generally time for us to collect the results. Most problems (except for subset problems) collect results on leaf nodes, and only subset problems collect results at each node. [It is not easy to understand here, it needs to be combined with specific problems]. The termination condition needs to collect results, usually at leaf nodes, so what results should be collected? For example, the problem of combination: combining these results should be put into the result set, remember not to forget return. After processing the termination condition, it enters the logic of single-level search.
The logic of single-level search is generally a for loop. The parameters of this for loop are suitable for processing each element in the collection. Usually, each element in the collection is placed in the for loop, and the collection is traversed by the for loop. Each element in , can also correspond to the number of all child nodes (the number of processing nodes), what node does the processing node process? For example, the combination problem: put 1 and 2 into an array during the process of processing nodes in the for loop. So that in the termination condition, 1 and 2 will be put into the result set when collecting the results. Below the processing node is recursion, recursive function, and recursive process. That is to say, go down layer by layer in the tree diagram, and the bottom of the recursion is the backtracking operation. The backtracking operation is to undo the operation of the processing node. The meaning of the backtracking operation is to undo: because for the combination problem, if 1, 2, 3, 4 choose 2; 1, 2 is there, only 2 is canceled, and then 3 is put in, only 1, 3 will be

Guess you like

Origin blog.csdn.net/adreammaker/article/details/128594745