一、线索二叉树的定义
我们使用二叉树时,n个节点的二叉树至少会产生n+1个空指针;同时,在某种遍历方式下寻找到其前驱或后驱节点并不方便,为了解决这个问题,我们可以将这些空指针记录前后驱的关系,从而在节约空间的同时方便遍历。
我们使所有原本为空的rchild指针改为指向该节点在某序序列中的后继,所有原本为空的lchild指针改为指向该节点的某序序列的前驱。
由以上定义可知,二叉树的线索化何其遍历的方式有关。下图就是不同遍历顺序形成的线索二叉树
那么,我们又如何知道二叉树的lchild指向的是左子节点还是前驱呢?我们可以定义标位符ltag和rtag。并规定:ltag==0,指向左子节点;ltag==1,指向前驱结点。rtag==0,指向右子节点;rtag==1,指向后继结点。这样,我们写出线索二叉树的存储结构:
typedef struct bitreenode {
int data;
struct bitreenode* lchild, * rchild;
int ltag, rtag;
}bitreenode;
二、中序线索化算法
在二叉树中序遍历过程中,“访问结点”的行为就是将结点的空指针改为指向中序前驱或中序后继的线索。设指针T指向当前访问的结点,为了记下结点的前驱、后继关系,设置一个初值为NULL 的全局指针变量 pre,令 pre始终指向当前访问结点T的直接前驱,即pre是T的中序前驱,T是pre的中序后继。这样,“访问结点”执行的操作是:如果pre有空的rchild,则填写pre的rtag,令其指向T;如果T有空的lchild,则填写T的ltag,令其指向pre。
// 通过中序遍历对二叉树线索化的递归算法:
bitreenode* pre = NULL;//保持pre为T的前驱节点
void inthread(bitreenode* T) {
if (T != NULL) {
inthread(T->lchild);//对二叉树的左子树进行线索化
if (!T->lchild) {//左子树为空:ltag赋为-1,指向前驱节点
T->ltag = 1;
T->lchild = pre;
}
if (pre && !pre->rchild) {//pre不为空而且其右子树为空:
pre->rtag = 1;
pre->rchild = T;//添加T的前驱
}
pre = T;
inthread(T->rchild);
}
}
注意,这里我用的是bitreenode*,并没有使用二级指针。这是没有错误的。是否使用双重指针取决于你是否需要在函数内部修改指针本身。对于创建二叉树的根节点或者在树中递归地插入或删除节点,使用双重指针可以更直接地修改指针,简化代码逻辑。然而,对于已经存在的树进行遍历或添加附加信息(如线索化)时,并不需要修改指针本身,因此单个指针就足够了。
三、利用中序线索树正向或逆向遍历二叉树
在完成了线索二叉树的建立之后,在进行中序遍历的时候只需找出节点p的后继节点就可以了。p的后继有两种情况:
(1).p->rtag==1,说明p->rchild指向p的下一个节点
(2).p->rtag==0,此时一路向下寻找最左下节点
由此,写出getnext函数:
bitreenode* getnext(bitreenode* p) {
bitreenode* temp;
if (p->rtag == 1) {//如果rtag为1,说明右线索指向下一个节点
return p->rchild;
}
else {//否则,一路寻找到最左下的节点,具体表现为ltag的值为0
temp = p->rchild;
while (temp&&temp->ltag == 0) temp = temp->lchild;
return temp;
}
}
得到了getnext函数之后,我们便可以写出正向中序遍历:
void inorder(bitreenode* root) {//利用中序线索化进行遍历
bitreenode* p;
if (root) {
p = root;
while (p->ltag == 0) p = p->lchild;
do {
visit(p);//visit函数为对p节点的操作函数
p = getnext(p);
} while (p != NULL);
}
}
如果我们向反向遍历二叉树,可以相应的写出getprior函数:
bitreenode* getprior(bitreenode* p) {
bitreenode* temp;
if (p->ltag == 1) {//如果ltag为1,说明左线索指向下一个节点
return p->lchild;
}
else {//否则,其左子树的最右下角就是他的前驱
temp = p->lchild;
while (temp && temp->rtag == 0) temp = temp->rchild;
return temp;
}
}
写出逆向中序遍历:
void reinorder(bitreenode* root) {
bitreenode* p;
if (root) {
while (root->rchild != NULL) {
root = root->rchild;
}
p = root;
do {
visit(p);
p = getprior(p);
} while (p != NULL);
}
}
建议在写代码时画出示意图,这样更加直观。
应当指出,这种利用二叉链表中的空指针实现的线索二叉树,仅中序线索二叉树最实用,因为在中序线索的指引下,找每个结点的中序前驱和中序后继都比较方便。但是对于先序线索二叉树而言,利用先序线索只能方便地找到所有结点的先序直接后继和那些有左线索(ltag= =1)的结点的先序直接前驱;而对于那些Itag= =0的结点,找其先序直接前驱需要知道该结点的双亲,故影响了先序线索二叉树的实用性。对于后续线索二叉树也是一样的。所以,若要建立先序线索二叉树和后序线索二叉树,建议对于每个节点T,多使用两个指针prior和next,其中prior指向T的某序前驱,next指向T的某序后驱。
(附:完整代码,在vs2022中经过测试运行)
#include<stdio.h>
#include<stdlib.h>
typedef struct bitreenode {
int data;
struct bitreenode* lchild, * rchild;
int ltag, rtag;
}bitreenode;
void creat(bitreenode** root);
void inthread(bitreenode* p);
bitreenode* getnext(bitreenode* p);
bitreenode* getprior(bitreenode* p);
void inorder(bitreenode* root);
void reinorder(bitreenode* root);
void visit(bitreenode* root);
int main() {
bitreenode* root, * pre = NULL;
creat(&root);
inthread(root);
inorder(root);
reinorder(root);
return 0;
}
void creat(bitreenode** root) {
int ch;
scanf_s("%d", &ch);
if (ch == -1) *root = NULL;
else {
(*root) = (bitreenode*)malloc(sizeof(bitreenode));
(*root)->data = ch;
(*root)->ltag = (*root)->rtag = 0;
creat(&(*root)->lchild);
creat(&(*root)->rchild);
}
}
// 通过中序遍历对二叉树线索化的递归算法:
bitreenode* pre = NULL;//保持pre为T的前驱节点
void inthread(bitreenode* T) {
if (T != NULL) {
inthread(T->lchild);//对二叉树的左子树进行线索化
if (!T->lchild) {//左子树为空:ltag赋为-1,指向前驱节点
T->ltag = 1;
T->lchild = pre;
}
if (pre && !pre->rchild) {//pre不为空而且其右子树为空:
pre->rtag = 1;
pre->rchild = T;//添加T的前驱
}
pre = T;
inthread(T->rchild);
}
}
bitreenode* getnext(bitreenode* p) {
bitreenode* temp;
if (p->rtag == 1) {//如果rtag为1,说明右线索指向下一个节点
return p->rchild;
}
else {//否则,一路寻找到其右子树最左下的节点,具体表现为ltag的值为0
temp = p->rchild;
while (temp&&temp->ltag == 0) temp = temp->lchild;
return temp;
}
}
bitreenode* getprior(bitreenode* p) {
bitreenode* temp;
if (p->ltag == 1) {//如果ltag为1,说明左线索指向下一个节点
return p->lchild;
}
else {//否则,其左子树的最右下角就是他的前驱
temp = p->lchild;
while (temp && temp->rtag == 0) temp = temp->rchild;
return temp;
}
}
void inorder(bitreenode* root) {//利用中序线索化进行正向遍历
bitreenode* p;
if (root) {
p = root;
while (p->ltag == 0) p = p->lchild;
do {
visit(p);//visit函数为对p节点的操作函数
p = getnext(p);
} while (p != NULL);
}
}
void reinorder(bitreenode* root) {
bitreenode* p;
if (root) {
while (root->rchild != NULL) {
root = root->rchild;
}
p = root;
do {
visit(p);
p = getprior(p);
} while (p != NULL);
}
}
void visit(bitreenode* root) {
printf("%d ", root->data);
}
参考资料:
2.《数据结构及应用算法》