数据结构——树:二叉树的线索化及中序构建遍历

一、名称定义

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值