一、名称定义
node:节点
root node:根节点
front-node/previous node:前驱节点/前驱
behind-node/back-node:后继节点/后继
inOrder-traverse:中序遍历
pointer space: 指针空间
left-pointer space:左指针空间
right-piinter space:右指针空间
List:链式二叉树
Tree:树状二叉树
binary tree:二叉树
clue-binary tree:线索二叉树
recursion-theory:递归原理
tag-assignment theory:tag赋值原理
二、理解
在线索化二叉树的时候,要先了解其构建的原理。即为:将树状结构的二叉树转化为链式结构。对于一个二叉树的前序,中序和后序的构建原理是有些许的不同,但是底层的逻辑是相同的。这里我们以中序的构建来分析。
首先对于如上的二叉树我们的中序遍历逻辑顺序是:4-2-5-1-3。因此,我们就以这样的一个顺序构建一个类似链表的结构,我们定义为List,同时定义这个链式结构List中的每一个部分为“节点(node)”,其中,节点的结构为如下:
#define ElemType char // define the data type
typedef struct TreeNode
{
ElemType data; // this parameter is used to save the information
struct TreeNode* lchild; // create one pointer which is used to point to the left-child
struct TreeNode* rchild;
int ltag; // used to record the condition of the left and right node if it is nullptr
int rtag;
}TreeNode;
首先,我们要明确中序化二叉树的基本逻辑。对于一个节点node,它分别有左孩子和右孩子,分别由指针lchild和rchild管理。对于一个node,其有对应的左右指针空间(pointer space),里面有指针 lchild 和 ltag。其中,lchild 用来管理指向左孩子,ltag用来记录做孩子的情况。如果孩子为空,则 ltag=1;反之同理。
它的左孩子若为nullptr,则 lchild 指向List中的前一个node,同时 ltag = 1。否则,指向这个节点,同时 ltag = 0。
它的右孩子若为nullptr,则 rchild 指向List中的后一个node,同时 rtag = 1。否则,只想这个节点,同时 rtag = 0。
即:除了List连边的node中的 ltag 和 rtag,中间的node中的tag的赋值都遵循这个原则。当node指向Tree中的node时,tag = 0;当node指向List中的node时, tag = 1。
线索化后的链式图如图所示:
在链式结构图中,黑色线表示各个node在Tree中的链接关系,红色线表示各个node在List中基于赋值原理的链接关系。故对于一个二叉树线索化的基本步骤是:
(1)用对应的遍历方式将所有node排列成为List结构
(2)将各个节点在Tree结构中的连接关系连接
(3)利用“tag赋值原理”,将pointer space中的tag进行赋值,分别只想前驱(front-node)和后继(back-node)
了解了其基本的构建原理后,我们开始用代码实现。
三、代码实现
1.creatTree() 函数:用于创建一个Tree结构的二叉树
1.1 二叉树的搭建
void createTree(TreeNode**T, ElemType* data, int* index)
{
ElemType ch; //used to record the information in the data
ch = data[*index]; // assign
*index += 1; // move the pointer
if(ch == '#') //define the '#' is empty
{
*T = nullptr;
}
else
{
*T = (TreeNode*)malloc(sizeof(TreeNode));
(*T)->data = ch;
(*T)->ltag = 0; // initate
(*T)->rtag = 0;
createTree(&((*T)->lchild),data,index);
createTree(&((*T)->rchild),data,index);
}
}
int main(void)
{
TreeNode* root; //create the root of the tree
int index = 0; //define one pointer in order to locate the information you want to save
//TreeNode* pre = nullptr; //
char data[] = "ABD##E##CF##G##";
createTree(&root,data,&index);
//pre->rtag = 1;
//pre->rchild = nullptr;
return 0;
}
1.2 解析
使用递归实现树的搭建。传入函数的有3个参数,其中T用二级指针接受是因为要对一个node中的lchild和rchild等进行改变。(这里的T并不是指二叉树的树根。使用递归时我们要明确每三个node就可以构成一个二叉树,而且这个算法的逻辑就是基于此的。每次处理一个二叉树,并一次递归)
这里的基本逻辑就是,在每一次进入函数,就读取一个data里面的数据信息并赋值给node中存储数据的变量。在此之前进行一个判断:
若读取为空(‘#’),则赋值为空指针nullptr。否则则赋值并初始化tag,并进入递归,不断的对下一个子节点进行判断并赋值。当左右孩子都为空时,通过两个递归函数都对其进行了赋值为nullptr,这个node就是尾节点。这里传入函数的是一个数组,并且在主函数中定义了一个类整形指针用来定为数据。
2.inThreadTree() 函数:将Tree结构的二叉树转化为List结构的二叉树
2.1 二叉树的转换
void inThreadTree(TreeNode* T, TreeNode** pre_node)
{
//pre_node stands for the previous node in the List construction
//you can also think it as the fton-node
if(T != nullptr)
{
inThreadTree(T->lchild, pre_node);
if(T->lchild == nullptr)
{
T->ltag = 1;
T->lchild = *pre_node;
}
if(*pre_node != nullptr && (*pre_node)->rchild == nullptr)
{
(*pre_node)->rtag = 1;
(*pre_node)->rchild = T;
}
*pre_node = T;
inThreadTree(T->rchild, pre_node);
}
}
int main(void)
{
TreeNode* T;
int index = 0;
TreeNode* pre = nullptr; // define the front-node
char data[] = "ABD##E##CF##G##";
createTree(&T, data, &index);
//change the tree into the list
inThreadTree(T, &pre);
pre->rtag = 1;
pre->rchild = nullptr;
return 0;
}
2.2 解析
初始化:在main函数中,初始化一个pre,用来计入Tree结构中的上一个node的信息,主要是为了在进递归的时候能够找到上一个node。初始化问为空。
函数接收两个参数,一个是当前的node,另一个是上一个节点(previous node),这里要和List结构中的前驱区分开来。这里可以类比链表的搭建,为了将链表连接,要有指向上一个节点和下一个节点的指针。在第一次使用该函数的时候,即在main中调用函数的时候,传入的的节点是根节点。下一步,进入if 判断,当T==nullptr 时,即根节点为空时,树为空,就没有继续的必要了。这里的 if 时为了防止段错误的产生。然后是 if 中主要部分的分析。
首先是进入递归(recursion),从左到右开始遍历变形。inThreadTree(T->child, pre_node) 进行递归,传入某节点的左孩子和上一个节点,直到出现 T == nullptr 的情况的时候,即找到了最左边的那个节点,然后开始递归开始返回,此时T,pre_node 和pre_node->rchild 构成一个一元二叉树。
if (T->lchild == nullptr)
{
//is the left-child is nullptr, assign the ltag is "1"
T->ltag = 1;
//and then let the left-child point to the previous node
T->lchild = *pre_node;
}
这段 if 判断当某节点的左孩子为空的时候,根据tag赋值原理(tag-assignment theory),node中的左指针区域应该指向上一个node,同时 ltag = 1,这样就处理完成了了这个一元二叉树的左孩子。然后判断这个一元二叉树的根,即pre_node,若其右孩子也为空,再次使用tag赋值原理;若部位空,则将pre_node赋值为T,开始进入下一个递归。
inThreadTree(T->rchild, pre_node),对右孩子进行处理。原理和上面的一样。下面我们用刚刚的例子进行分析:
1.开始传入T = 1,进行递归。由于1->lchild != nullptr, 则进入递归循环。与此同时,有*pre_node = T,即pre_node不断更新为当前node的 lchild,这样就实现了一个一元二叉树的更新。
2. 当递归到 T = 4 的时候,4->lchild == nullptr, 就将4的左指针区域进行“tag赋值原理”。令:4->ltag = 1; T->lchild = *pre_node 即:左指针空间指向上一个node,即:pre_node. 故,当前的结构为:List: 4->2
3. 进入第二个 if 判断。当pre_node != nullptr(显而易见2 != nullptr),同时有(*pre_node)->rchild == nullptr,由于2->5 != nullptr, 故不执行该 if 。
4. 执行*pre_node = T 更新代码,上一个节点(previous node)为T = 4,故赋值pre_node = 4; 这里也说明了一点就是为什么
需要传递二级指针,为的是对其值进行改变
5. 执行inThreadTree(T->rchild, pre_node)。(现在pre_node更新为了4)。执行下一步递归。将右孩子传入,即执行 inThreadTree(5, 2)。接着就是再一次执行上诉的过程。
需要注意的是,在mian中还有:
pre->rtag = 1;
pre->rchild = nullptr;
这是因为在头和尾部的node两侧的指针空间是没有被赋值的,这里要加上。
3. getFirst():找到最左边的node
3.1 代码
TreeNode* getFirst(TreeNode* T)
{
while(T->ltag == 0)
{
T = T->lchild;
}
return T;
}
3.2 解析
这应该就不用做过多的解释了,自己都能理解吧。
4. getNext():主要和getFirst联合使用对二叉树进行遍历。
4.1 代码
TreeNode* getNext(TreeNode* node)
{
if(node->rtag == 1)
{
return node->rchild;
}
else
{
return getFirst(node->rchild);
}
}
四、完整代码框架
#include"iostream"
#include"cstdlib"
#include"cstring"
using namespace std;
/*
Clue-Binary Tree(二叉树线索化)
1.Define: Order the binary tree in the linear construction at a regular way
We achieve it using inOrder as an example. Let every node has one front-node
and after-node except the head and tail.
2.Regulation: add two new pointers: ltag and rtag.
When "ltag == 0", lchild points to the left-child(means it is not nullptr)
When "ltag == 1", lchild points to the front_node(means it is nullptr)
When "rtag == 0", rchild points to the right-child(means it is not nullptr)
When "rtag == 1", rchild points to the behind_node(means it is nullptr)
*/
typedef struct TreeNode
{
char data;//used to save the data
struct TreeNode* lchild; // child pointers
struct TreeNode* rchild;
int ltag;//used to record the condition of the left and right node
int rtag;
}TreeNode;
//create the binary-tree
//the basic logic is similar to the previous achievement
//but you shuold initate the ltag and rtag
void createTree(TreeNode** T, char* data, int* index)
{
char ch;
ch = data[*index];
*index += 1;
if (ch == '#')
{
*T = nullptr;
}
else
{
*T = (TreeNode*)malloc(sizeof(TreeNode));
(*T)->data = ch;
(*T)->ltag = 0;
(*T)->rtag = 0;
//we define the "false" means the node is not visited
createTree(&((*T)->lchild), data, index);
createTree(&((*T)->rchild), data, index);
}
}
/*
the inOrder traverse Clue-Binary tree
change the binary-tree into the clue-binary tree
*/
void inThreadTree(TreeNode* T, TreeNode** pre_node)
{
/* Before you write the tree, you should change your mind first or in order words, you
shoud tranform the perspective you see the binary-tree. Every time you manipulate the binary-tree,
you should see the "left-child", "middle-child" and "right-child" as one binary-tree because you
will use recursion to continue manipulating the next node. So, the next step is delivering the
left-child or the right-child to the function.
*/
//
//
//There, "T" is the root of the binary tree
//traverse the tree from the left to middle and then the right.
//because the root node doesn't have previous node, so the pre_node is "nullptr"
//FRONT-NODE
if (T != nullptr)
{
//if T->lchild != nullptr, execute this function, or execute the "if-program"
inThreadTree(T->lchild, pre_node);
/*
There, we use recursion to achieve the creatation of the clue-binary tree.
The basic logic is: deliver T->lchild and pre_node to the function's paramters: T and pre_node
untill the T->lchild == nullptr. So, you shuold know that the node "T" is no longer the root ot the tree.
"T" is the next child node
"pre_node" is the front-node(前驱)
*/
if (T->lchild == nullptr)
{
//is the left-child is nullptr, assign the ltag is "1"
T->ltag = 1;
//and then let the left-child point to the previous node
T->lchild = *pre_node;
}
//BEHIND-NODE
//after you finish manipulating the lchild, you should deal with the previous node.
if (*pre_node != nullptr && (*pre_node)->rchild == nullptr)
{
(*pre_node)->rtag = 1;
(*pre_node)->rchild = T;
}
*pre_node = T;
//after you have finished manipulating the left-child, then you should deal with the
//right-child
inThreadTree(T->rchild, pre_node);
}
}
//traverse the clue-binary tree
//because the binary-tree is linear construction now, you can find the head of the list
TreeNode* getFirst(TreeNode* T)
{
//find the node whose ltag is 1 which means its front-node is nullptr
//So, that means this is the head because we visit the tree from the left to the middle and then the right
while (T->ltag == 0)
{
T = T->lchild;
}
return T;
}
TreeNode* getNext(TreeNode* node)
{
if (node->rtag == 1)
{
return node->rchild;
}
else
{
return getFirst(node->rchild);
}
}
int main(void)
{
TreeNode* T;
int index = 0;
TreeNode* pre = nullptr;
char data[] = "ABD##E##CF##G##";
createTree(&T, data, &index);
inThreadTree(T, &pre);
pre->rtag = 1;
pre->rchild = nullptr;
for (TreeNode* node = getFirst(T); node != nullptr; node = getNext(node))
{
cout << node->data;
}
cout << endl;
return 0;
}