线索二叉树构造及遍历算法
线索二叉树构造以及遍历算法
- 线索二叉树(中序遍历版)
- 构造线索二叉树
- 构造双向线索链表
- 遍历中序线索二叉树
线索二叉树(中序遍历版)
中序遍历找到对应结点的前驱(土方法)
typedef struct BiNode{
int data;
BiNode *lchild, *rchild;
}BiNode, *BiTree;
BiNode *p; // 指向目标节点
BiNode *pre = NULL; // 指向当前访问节点的前驱
BiNode *final = NULL; // 记录最终结果
void InOrder(BiTree T){
if(T != NULL){
InOrder(T -> lchild);
visit(T);
InOrder(T -> rchild);
}
}
// 找到目标节点的前驱节点
void visit(BiNode *q){ // q代表当前访问节点
if(p == q)
final = pre; // 如果当前访问节点和目标节点一致了,那么pre就是我们需要找的前驱
else
pre = q; // 如果不一致,那么更新前驱节点
}
由上述代码我们可以知道,如果使用土方法来寻找目标节点的前驱节点,那么每找一次,就需要对二叉树进行一次遍历,这样对资源的浪费是不言而喻的,所以我们需要采用线索二叉树来更加快速地寻找对应节点的前驱后继,通过线索二叉树,我们可以实现对二叉树的随机访问。
问题1:为什么在visit函数中不需要对q进行迭代?
回答1:因为 q q q的迭代是在 I n O r d e r InOrder InOrder中进行的,在每一次对 I n O r d e r InOrder InOrder的递归中,传入 v i s i t visit visit的节点会不会·不断变化,也就实现了对 q q q的迭代。
线索二叉树实际就是用空的 n + 1 个空指针指向直接前驱和直接后继。如果一个节点的左孩子为空,则左孩子指针指向当前节点的前驱,改 l t a g ltag ltag为1;如果一个节点的右孩子为空,则用右孩子指针指向当前节点的后继,改 r t a g rtag rtag为1。
lchild | ltag | data | rtag | rchild |
---|---|---|---|---|
指示左孩子 | 0 | 0 | 指示右孩子 | |
指示直接前驱 | 1 | 1 | 指示直接后继 |
// 线索二叉树的存储结构
typedef struct ThreadTree{
int data;
struct ThreadTree *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
构造线索二叉树
通过中序遍历对二叉树线索化
void InThread(ThreadTree &p, ThreadTree &pre){ // p是当前访问节点,pre为当前访问节点的前驱
if(p != NULL){
InThread(p -> lchild);
if(p -> lchild == NULL){ // 如果左孩子为空,则更新左孩子为前驱,ltag为1
p -> lchild = pre;
ltag = 1;
}
if(pre != NULL && pre -> rchild == NULL){ // 若前驱节点非空且其右子树为空,则更新其右孩子为后继,rtag为1
pre -> rchild = p;
pre -> rtag = 1;
}
pre = p;
InThread(p -> rchild);
}
}
void CreateInThread(ThreadTree T){
Thread pre = NULL;
if(T != NULL){
InThread(T, pre);
pre -> rchild = NULL; // 处理最后一个节点
pre -> rtag = 1;
}
}
问题2:为什么在创建二叉树的时候需要判断pre是否为空?
回答2:为了避免空指针引用,我们在创建线索二叉树的时候,会把pre初始化为NULL(也就是其实并没有这个节点),因为第一个节点没有直接的前驱,而如果我们不对空指针进行判断的话,那么pre的后继就会是当前节点p,那么究竟谁才是第一个节点呢?运行起来就会导致程序崩溃。只有当pre不为空时,才有意义去判断其右子树是否为空,为它建立线索二叉树才有意义。
构造双向线索链表
但是这样的线索二叉树还是存在一些问题,比如没有办法从第一个节点直接遍历到最后一个节点,为此我们可以建立一个头节点,让其lchild
指向二叉树的根节点,其rchild
指向中序遍历时访问的最后一个节点。令中序遍历的第一个节点的lchild
指向头节点,也就是第一个节点的前驱不再是NULL
,而是head
;令中序遍历的最后一个节点的rchild
指向头节点,也就是最后一个节点的后继也不再是NULL
,而是head
。这样一来,我们就获得了一个双向线索链表。
void InThread(ThreadTree &p, ThreadTree &pre){ // p是当前访问节点,pre为当前访问节点的前驱
if(p != NULL){
InThread(p -> lchild);
if(p -> lchild == NULL){ // 如果左孩子为空,则更新左孩子为前驱,ltag为1
p -> lchild = pre;
ltag = 1;
}
if(pre != NULL && pre -> rchild == NULL){ // 若前驱节点非空且其右子树为空,则更新其右孩子为后继,rtag为1
pre -> rchild = p;
pre -> rtag = 1;
}
pre = p;
InThread(p -> rchild);
}
}
void CreateInThread(ThreadTree T){
ThreadNode *head = (ThreadTree*)malloc(sizeof(ThreadTree));
if(head == NULL)
return ;
head -> ltag = 0; // 指向根节点
head -> rtag = 1; // 指向最后一个节点
head -> rchild = head; // 初始化右指针指向自己
if(T == NULL){
head -> lchild = head;
}else {
head -> lchild = T;
ThreadTree pre = head;
InTread(T, pre);
// 处理最后一个节点
pre -> rchild = head;
pre -> rtag = 1;
// 处理第一个节点
ThreadNode *first = head -> lchild; // 初始化第一个节点为根节点,方便找到第一个节点
// 寻找中序遍历的第一个节点
while(first -> ltag == 0) // 如果lchild是指向左孩子则迭代
first = first -> lchild;
first -> lchild = head;
}
}
遍历中序线索二叉树
只要先找到序列中的第一个节点,然后依次找节点的后继,直到其后继为空便可完成遍历;
1. 求第一个节点
ThreadNode *FirstNode(ThreadNode *p){
while(p -> ltag == 0) p = p -> lchild;
return p;
}
2. 求中序线索二叉树中节点p在中序序列下的后继
ThreadNode *NextNode(ThreadNode *p){
if(p -> rtag == 0) return FirstNode(p -> rchild); // 右子树中最左下节点
else return p -> rchild;
}
3. 求中序线索二叉树的最后一个节点
ThreadNode *LastNode(ThreadNode *p){
while(p -> rtag == 0) p = p -> rchild;
return p;
}
4. 求节点p前驱
ThreadNode *PreNode(ThreadNode *p){
if(p -> ltag == 0) return LastNode(p -> lchild);
return p;
}
利用上述1.2.两个算法,我们可以写出不含头节点的中序线索二叉树的中序遍历算法:
void InOrder(ThreadNode *T){
for(ThreadNode *p = FirstNode(T); p != NULL; p = NextNode(p))
visit(p); // 访问节点,可自由设定
}