浅谈二叉树线索化相关问题

二叉树线索化最简单的就是中序了,无脑按照中序遍历递归算法套就OK了。

现在我们有如下代码实现

cpp 复制代码
//file:threadBiTree.h
#ifndef THREAD_BI_TREE_H
#define THREAD_BI_TREE_H

typedef struct threadBiNode {
    int data;
    struct threadBiNode *lchild, *rchild;
    int ltag, rtag;
}threadBiNode, *threadBiTree;

void _thread_in(threadBiTree &T, threadBiNode *&pre);
void create_thread_in(threadBiTree &T);

#endif

/**********************/

//file:threadBiTree.cpp
#include "threadBiTree.h"

void _thread_in(threadBiTree &T, threadBiNode *&pre)
{
    if (!T)
        return;

    _thread_in(T->lchild, pre);
    if (pre && pre->rchild == nullptr)
    {
        pre->rtag = 1;
        pre->rchild = T;
    }

    if (T->lchild == nullptr)
    {
        T->ltag = 1;
        T->lchild = pre;
    }

    pre = T;
    _thread_in(T->rchild, pre);
}

//assume that the tags of tree have been initialize by zero
void create_thread_in(threadBiTree &T)
{
    if (!T)
        return;
    threadBiNode *pre = nullptr;
    _thread_in(T, pre);
    if (pre)
    {
        pre->rtag = 1;
        pre->rchild = nullptr;
    }
}

在笔者看来中序二叉树是一种很方便的数据结构,中序线索二叉树类似于一个双链表,可以沿着中序下第一个节点向后作中序遍历也可以沿着中序下最后一个节点向前作遍历,但是对于任意前序线索二叉树不一定能作反向前序遍历,任意后序线索二叉树不一定能作正向后序遍历。而要实现中序二叉树的双向遍历功能,只需要几个简单的辅助函数即可。而这些辅助函数的实现也很好理解,因为中序的第一个结点,最后一个节点,某个节点的直接前驱或后继都非常好找,而要怎么找这里就请看下面代码注释不赘述

于是我们有如下辅助函数和用于正反向中序遍历的函数

cpp 复制代码
//file: threadBiTree.cpp

/**********用于正向中序遍历**********/
// assume that the data structure is validate
threadBiNode *first_thread_in(threadBiTree &T)
{
    if (!T)
        return nullptr;
    threadBiNode *p = T;
    while (p->ltag == 0)	//中序下第一个节点必然是整棵树最左下节点
        p = p->lchild;
    return p;
}

// assume that the data structure is validate
threadBiNode *next_thread_in(threadBiNode *&p)
{
    if (!p)
        return nullptr;
    if (p->rtag == 1)	//某个节点的后继要么是由其后继指针指定(即无右子树)
        return p->rchild;
    else
        return first_thread_in(p->rchild);	//要么是其右子树在中序下的第一个节点,简单理解为因为中序是LNR,现在存在右子树那就是R了,另外中序遍历是递归的
}

// assume that the data structure is validate
void traverse_thread_in(threadBiTree &T, void (*visit)(threadBiNode *&p))
{
    for (threadBiNode *p = first_thread_in(T); p != nullptr; p = next_thread_in(p))
        visit(p);
}

/**********用于逆向中序遍历**********/
// assume that the data structure is validate
threadBiNode *last_thread_in(threadBiTree &T)
{
    if (!T)
        return nullptr;
    threadBiNode *p = T;
    while (p->rtag == 0)	//中序下最后一个节点必然是整棵树最右下节点
        p = p->rchild;
    return p;
}

// assume that the data structure is validate
threadBiNode *previous_thread_in(threadBiNode *&p)
{
    if (!p)
        return nullptr;
    if (p->ltag == 1)	//前驱就往左子树上找,和找后继原理是一样的,无左子树则其前驱为该节点左线索所指明的节点
        return p->lchild;
    else
        return last_thread_in(p->lchild);	//有左子树,前驱就是左子树中序遍历下的最后一个节点
}

// assume that the data structure is validate
void traverse_reverse_thread_in(threadBiTree &T, void (*visit)(threadBiNode *&p))
{
    for (threadBiNode *p = last_thread_in(T); p != nullptr; p = previous_thread_in(p))
        visit(p);
}

中序和后序二叉树实现方法上是一致的,照搬递归遍历算法即可,只是需要注意一下在最后一个节点线索化时不能照搬中序的,因为中序遍历下的最后一个结点必然无右孩子,于是这个结点的右指针必然是线索,但是后序显然不是

于是我们有如下二叉树后序线索化实现

cpp 复制代码
/******postorder thread******/
// cannot be called by user
void _thread_post(threadBiTree &T, threadBiNode *&pre)
{
    if (!T)
        return;
    _thread_post(T->lchild, pre);
    _thread_post(T->rchild, pre);
    if (T->lchild == nullptr)
    {
        T->ltag = 1;
        T->lchild = pre;
    }
    if (pre && pre->rchild == nullptr)
    {
        pre->rtag = 1;
        pre->rchild = T;
    }
    pre = T;
}

// assume that the tags of tree have been initialize by zero
void create_thread_post(threadBiTree &T)
{
    if (!T)
        return;
    threadBiNode *pre = nullptr;
    _thread_post(T, pre);
    if (pre && pre->rchild == nullptr)	//如果最后一个结点右指针确实为空那么可以设其为线索
    {
        pre->rtag = 1;
    }
}

至于用于反向遍历后序线索二叉树得到代码这里不给出,我也没去细想,反正考研不考,也就问问线索怎么个连法

最后一个就是前序二叉树线索化,前序二叉树线索化需要注意的就是避免某个结点被重复线索化,因为前序遍历的特点,必定是根先被线索化。可以这样想某个前序线索化到某个节点后,直接调用前序线索化函数,如果左指针是线索,那么就开始处理之前已经线索化的结点。试想,如果采用中序、后序类似的代码而不对函数递归调用作出限制,那么至少会产生无限递归的问题,例如我们可以考虑对图中对节点A的线索化

所以,限制以下递归调用的条件会产生正确结果,从而我们有如下二叉树先序线索化实现

cpp 复制代码
//file: threadBiTree.cpp

/******preorder thread******/
// cannot be called by user
void _thread_pre(threadBiTree &T, threadBiNode *&pre)
{
    if (!T)
        return;
    if (pre && pre->rchild == nullptr)
    {
        pre->rtag = 1;
        pre->rchild = T;
    }
    if (T->lchild == nullptr)
    {
        T->ltag = 1;
        T->lchild = pre;
    }
    pre = T;
    if (T->ltag == 0) // 若已经线索化再次进入则会因为指针不空再次被线索化所以当未被线索化才递归执行线索化
        _thread_pre(T->lchild, pre);
    if (T->rtag == 0)
        _thread_pre(T->rchild, pre);
}

// assume that the tags of tree have been initialize by zero
void create_thread_pre(threadBiTree &T)
{
    if (!T)
        return;
    threadBiNode *pre = nullptr;
    _thread_pre(T, pre);
    if (pre && pre->rchild == nullptr)
    {
        pre->rtag = 1;
        pre->rchild = nullptr;
    }
}

另外,先序遍历下的第一个节点明显根节点,某个节点的后继也很好找,如果该节点有左子树,那么左孩子就是其后继,若无左子树有右子树,那么就是右孩子,如果为叶节点,那么就是后继线索所指向的节点,这样我们能非常轻松地写出先序线索二叉树的正向先序遍历代码,如下所示

cpp 复制代码
//file: threadBiTree.cpp

/**********用于正向中序遍历**********/
// assume that the data structure is validate 
threadBiNode *first_thread_pre(threadBiTree &T) {
    return T;
}

// assume that the data structure is validate 
threadBiNode *next_thread_pre(threadBiNode *&p) {
    if(!p)
        return nullptr;
    threadBiNode *q = p;
    if(q->ltag == 0)
        return q->lchild;
    else
        return q->rchild;
}

// assume that the data structure is validate 
void traverse_thread_pre(threadBiTree &T, void (*visit)(threadBiNode *&p)) {
    for(threadBiNode *p = first_thread_pre(T); p != nullptr; p = next_thread_pre(p))
        visit(p);
}