1. Why use clue binary tree?
Let's first look at the shortcomings of ordinary binary trees. The following is an ordinary binary tree (chained storage method):
At first glance, does it seem inconsistent? The entire structure has a total of 7 nodes and a total of 14 pointer fields, of which 8 pointer fields are empty. For a binary tree with n nodes, there will be a total of n+1 null pointer fields. This rule applies to all binary trees.
Doesn’t so many null pointer fields seem wasteful? The focus of our learning of data structures and algorithms is to find ways to improve time efficiency and space utilization. So many pointer fields are wasted, what a waste!
So we have to find ways to make good use of them and use them to help us better use the binary tree data structure.
So how to take advantage of it?
The essence of traversing a binary tree is to convert the nodes of the nonlinear structure in the binary tree into a linear sequence , so that we can traverse it easily.
For example, the in-order traversal sequence in the above figure is: DBGEACF.
For a linear sequence (linear table), it has the concepts of direct predecessor and direct successor. For example, in an in-order traversal sequence, the direct predecessor of B is D and the direct successor is G.
The reason why we can know the direct predecessor and direct successor of B is because we write out the in-order traversal sequence of the binary tree according to the in-order traversal algorithm, and then use this sequence to tell whose predecessor and successor is who.
The direct predecessor and direct successor cannot be obtained directly through the binary tree, because there is only a direct relationship between the parents and the child nodes in the binary tree, that is, the node pointer field of the binary tree only stores the address of its child node.
The current requirement is that I want to be able to directly obtain the direct predecessor and direct successor of a node in in-order traversal mode from the binary tree.
At this time, you need to use the clue binary tree.
2. What is a clue binary tree?
Of course, we definitely need to use the pointer field of the node to save the addresses of the immediate predecessor and immediate successor.
In fact, in the ordinary binary tree in the above figure (the sequence obtained by traversing in intermediate order), some nodes (nodes whose pointer fields are not empty) can find their direct predecessors or successors, such as the left child G of node E It is the direct predecessor of node E; the right child C of node A is the direct successor of node A.
But it doesn't work for some nodes (the pointer field is empty). For example, the direct successor of node G is E and the direct predecessor is B. However, such a conclusion cannot be drawn in a binary tree. How to do it? We noticed that the two pointer fields of node G are both NULL and have not been used. So wouldn't it be good if we use these two pointers to point to its predecessor and successor respectively?
It’s truly the best of both worlds, a match made in heaven! But the problem is not solved!
Because we use the null pointer field to point to the predecessor or successor, this is contradictory for those nodes whose pointer field is not empty, such as node E and node B.
Since there is a conflict, we must discover the root cause of the conflict and resolve it.
The source of the contradiction is: when the pointer field of the node is empty and not empty, the pointing of the pointer is inconsistent. That is, there is a contradiction between the pointer pointing to the child when it is not empty and the predecessor or successor when the pointer is empty.
Then we take the right medicine, distinguish the pointer field that is empty and not empty, and clearly tell the pointer: when it is not empty, it points to the child, and when it is empty, it points to the predecessor or successor. This requires us to add a flag bit to each of the two pointers.
And agree on the following rules:
When left_flag == 0, the pointer left_child points to the left child
. When left_flag == 1, the pointer left_child points to the immediate predecessor.
When right_flag == 0, the pointer right_child points to the right child.
When right_flag == 1, the pointer right_child points to the immediate predecessor.
The nodes of the binary tree need to change:
/*线索二叉树的结点的结构体*/
typedef struct Node {
char data; //数据域
struct Node *left_child; //左指针域
int left_flag; //左指针标志位
struct Node *right_child; //右指针域
int right_flag; //右指针标志位
} TTreeNode;
With the flag bit, everything can be sorted out. We call pointers to immediate predecessors and successors clues. A pointer with a flag of 0 is a pointer to a child, and a pointer with a flag of 1 is a clue.
A binary linked list tree has the node structure as above. We turn all null pointers into clues. Such a binary tree is a binary clue tree.
3. How to create a clue binary tree?
In an ordinary binary tree, if we want to obtain the direct predecessor or successor of a node in a certain traversal order, we need to traverse to obtain the traversal order each time before we can know it. In the clue binary tree, we only need to traverse it once (traversal when creating the clue binary tree). After that, the clue binary tree can "remember" the direct predecessor and successor of each node, and there is no need to obtain it through the traversal order in the future. Precursor or successor.
The process in which we transform an ordinary binary tree into a clued binary tree according to a certain traversal method is called threading of a binary tree.
Next, we use in-order traversal to convert the following binary tree clue into a clue binary tree.
Use the pointer with the flag bit 1 to traverse the sequence in in-order order so that it points to the predecessor or successor:
among them, node D has no direct predecessor, Node F has no direct successor, so the pointer is NULL.
At this point, we have solved the waste caused by n+1 null pointer fields in a binary tree with n nodes. The solution is to add a flag bit to the pointer of each node to make use of the null pointer field. The flag bit stores a Boolean value of 0 or 1, which is relatively cost-effective compared with the wasted null pointer field. Moreover, the binary tree has a new feature - the predecessor and successor relationships between nodes in a certain traversal order can be saved in the binary tree.
4. Realization of clues
Please note that the clue binary tree is obtained from the ordinary binary tree, and it is obtained in a certain traversal order. Because clues can only be set after knowing the predecessor and successor of a node, and the relationship between the predecessor and successor cannot be directly reflected through the binary tree. The relationship can only be obtained through the linear sequence obtained by traversing the binary tree. Therefore, after obtaining the sequence with the predecessor and successor relationship through some kind of traversal method, the null pointer of the node can be modified, and then the clue can be set.
That is: the essence of threading is the process of modifying the null pointer of a node during the process of traversing a binary tree in a certain traversal order so that it points to its immediate predecessor or direct successor in that traversal order.
So, the general structure of the code is still the same, we just need to replace the printing code in the traversal code with the threaded code and make some other changes.
The following figure is an example to introduce three types of clues:
An unthreaded binary tree has all flags defaulting to 0.
4.1. Inter-sequence threading
After clueing according to the in-order traversal order, the following figure can be obtained:
Let us first clarify the following content again:
- We perform threading while traversing the binary tree.
- The order of inorder traversal is: left subtree >> root >> right subtree.
- Threading modifies two things: the null pointer field and its corresponding flag bit.
- How to modify? Sets the null pointer field to the immediate predecessor or successor.
So our question becomes:
- Find all null pointer fields.
- Find the node to which the null pointer field belongs, the direct predecessor and direct successor in preorder order.
- Modify the contents of the null pointer field and its flag so that the pointer is called a clue.
Note: We used recursion when traversing the binary tree, so we will also use it when threading.
The specific code is as follows:
//全局变量 prev 指针,指向刚访问过的结点
TTreeNode *prev = NULL;
/**
* 中序线索化
*/
void inorder_threading(TTreeNode *root)
{
if (root == NULL) {
//若二叉树为空,做空操作
return;
}
inorder_threading(root->left_child);
if (root->left_child == NULL) {
root->left_flag = 1;
root->left_child = prev;
}
if (prev != NULL && prev->right_child == NULL) {
prev->right_flag = 1;
prev->right_child = root;
}
prev = root;
inorder_threading(root->right_child);
}
4.2. Preorder threading
After clueing according to the pre-order sequence, the following picture can be obtained:
The specific code is as follows:
// 全局变量 prev 指针,指向刚访问过的结点
TTreeNode *prev = NULL;
/**
* 先序线索化
*/
void preorder_threading(TTreeNode *root)
{
if (root == NULL) {
return;
}
if (root->left_child == NULL) {
root->left_flag = 1;
root->left_child = prev;
}
if (prev != NULL && prev->right_child == NULL) {
prev->right_flag = 1;
prev->right_child = root;
}
prev = root;
if (root->left_flag == 0) {
preorder_threading(root->left_child);
}
if (root->right_flag == 0) {
preorder_threading(root->right_child);
}
}
4.3. Post-order threading
After clueing according to the post-order traversal order, the following figure can be obtained:
The specific code is as follows:
//全局变量 prev 指针,指向刚访问过的结点
TTreeNode *prev = NULL;
/**
* 后序线索化
*/
void postorder_threading(TTreeNode *root)
{
if (root == NULL) {
return;
}
postorder_threading(root->left_child);
postorder_threading(root->right_child);
if (root->left_child == NULL) {
root->left_flag = 1;
root->left_child = prev;
}
if (prev != NULL && prev->right_child == NULL) {
prev->right_flag = 1;
prev->right_child = root;
}
prev = root;
}
5. Summary
The threaded binary tree makes full use of the null pointer field in the binary tree and gives the binary tree a new feature - after threading through one traversal, the predecessor and successor relationships between its nodes can be saved in the binary tree.
Therefore, if we need to frequently traverse the binary tree to find the direct predecessor or successor node of a node, it is very appropriate to use the clue binary tree.