线索二叉树
什么是线索?

二叉树存储 二叉链表中提到,n个结点的二叉链表中,有 n + 1 个空指针,那这么多的空指针能用来干嘛呢?
大佬们想到用其按照 前/中/后序 将二叉树的结点排列成线性结构(将这些空指针改造为前继或继指针,称为线索)。那线索二叉树必然属于存储结构(二叉链表存储)。
具体怎么实现的看本章内容咯。

怎么将二叉树的结点排列成一个线性结构呢?那就是按照 先 / 中 / 后序遍历 的结点访问顺序咯(除第一个和最后一个外,每个结点都有唯一前驱和唯一后继)。线索二叉树的目的正是方便结点直接获得 其在遍历中的前驱和后继。那么理所应当的有 前/中/后序 线索二叉树o(* ̄▽ ̄*)ブ。

概念

中序线索二叉树

若结点的左指针为空,左指针指向 中序遍历下 该结点的前驱(为空则仍为NULL);

若结点的右指针为空,右指针指向 中序遍历下 该节点的后继(为空则仍为NULL)。

这样处理的好处在于,对于下图结点D,左指针指向B,右指针指向A,对应中序遍历下的 BDA 序列。

先序线索二叉树

后序线索二叉树

存储类型表示

如果直接像上面图片中那样 结点数据仍由 lchild data rchild组成可行嘛?

不行的!不然怎么区分左指针指的是 左孩子还是 遍历前驱,右指针指的是 右孩子 还是 遍历后继。

因此为了区分,还需要加上 ltag 和 rtag。ltag = 0 – 左孩子;ltag = 1 – 遍历前驱;rtag = 0 – 右孩子;rtag = 1 – 遍历后继。

typedef char ElemType;

typedef struct ThreadNode
{
    /* data */
    ElemType data;
    struct ThreadNode *lchild, *rchild;
    int ltag, rtag;
    // 构造函数 (方便建树)
    ThreadNode(ElemType x): data(x), lchild(nullptr), rchild(nullptr), ltag(0), rtag(0){}
};

操作 – 线索化、遍历

在写具体的操作算法前,要分析该操作可行性。

可以看到 先序线索二叉树和中序线索二叉树 均能 从根节点出发,沿着指针获得相应的遍历结果。

但是后序线索二叉树不行(如图找 B结点的后继无法找到),因此用 二叉链表作为存储结构 的 后序线索二叉树 无法实现遍历【只通过指针】操作。(课本:采用带标志域的三叉链表作为存储结构来解决此问题)- 阿Q表示:到一边玩泥巴去,咱不弄了。

中序线索二叉树

线索化

何为线索化? 将二叉树的空指针改成指向前驱或后继的线索,因此需要中序遍历一遍二叉树,并在遍历途中进行线索化操作即可。

阿Q觉得蛮绕的,直接写代码有点复杂,因此先思考写出伪代码(思绪理清)。

阿Q注:可以先看看 阿Q对递归的思考

// 伪代码
// Pre指向前一个访问的结点,P指向当前访问结点
// 整体思想不难: P和Pre都进行了中序遍历,只不过 P比Pre快了一步,即P永远是Pre的 中序遍历后继

线索化:
    若P(指向当前访问结点)不为空;
      线索化左孩子;
      
      若P的左子树为空:
        // 说明当前访问结点的 中序遍历前驱 没有更改,因此要进行线索化
        访问当前结点;
        将上一个访问的结点的地址(Pre) 赋值给 当前结点的左指针;// 当前结点 中序遍历前驱是 上一个访问的结点
      
      若已经访问了结点(Pre不为空) && 上一个访问的结点的右子树为空:
        // 说明上一个访问结点的 中序遍历后继 没有更改,因此要进行线索化
        访问上一个访问结点;
        将当前访问结点的地址(P)赋值给 上一个结点的右指针; // 上一个访问结点的 中序遍历后继是 当前访问结点
      
      将P(指向当前访问结点) 赋给 Pre(指向上一个访问结点); 
      线索化右孩子;

/*
需要注意的是上述线索化过程少了最后一个访问结点的处理(如A,B,C,D,E,F - Pre到了F递归全部就结束了,但此时未对其后继进行处理),因此不是最终线索化的结果
*/
建立线索化:
  Pre 设置为NULL;
  若树不为空树:
    调用线索化函数;
    // 处理最后一个访问结点的后继
    访问最后一个访问结点;
    最后一个访问结点(Pre所指的结点)的右孩子设置为NULL;
// 具体算法实现
// 中序遍历 线索化 -> 中序线索二叉树
void InThread(ThreadNode *&P, ThreadNode *&Pre)
{
    if(!P)
    {
        // 线索化 左孩子
        InThread(P->lchild,Pre);
        
        // P的左子树为空
        // 说明当前访问结点的 中序遍历前驱 没有更改,因此要进行线索化
        if(!P->lchild)
        {
            // 访问当前结点,此处的访问为将 ltag设为1
            P->ltag = 1;
            P->lchild = Pre;
        }

        //若已经访问了结点(Pre不为空) && 上一个访问的结点的右子树为空:
        if(Pre && !Pre->rchild)
        {
            // 访问上一个结点,此处的访问为将 rtag设为1
            Pre->rtag = 1;
            Pre->rchild = P;
        }

        Pre = P;
        // 线索化 右孩子
        InThread(P->rchild,Pre);
    }
}

// 建立中序线索二叉树
void CreateInThread(ThreadTree &T)
{
    ThreadNode *Pre = nullptr;
    if(!T)
    {
        InThread(T,Pre);
        // 处理最后一个访问结点的后继
        // 访问最后一个访问结点,此处的访问为将 rtag设置为1
        Pre->rtag = 1;
        Pre->rchild = nullptr;
    }
}

这种建立的中序线索二叉树仍然有空结点,若要求全部利用起来,则需要建立带头结点的中序线索二叉树。(第一个元素的前驱设为 头结点,最后一个元素的后继设为头结点即可)

遍历

阿Q以下写的均不带头结点!

如何遍历中序线索二叉树从而得到 中序遍历结果呢?只需要解决两个问题即可:

①如何以T为根 找到中序遍历的 第一个结点?

T为根的左子树最左下方的结点即为中序遍历的第一个结点。

②如何找到中序遍历的 当前结点的下一个结点?

若 rtag = 1,直接获取下一个结点;

若 rtag = 0,则需要以当前结点的右孩子为根(T),按①找即可

// 解决①问题
// 以T为根 找中序遍历的 第一个结点
ThreadNode* FirstNode(ThreadNode *T)
{
    // 左子树最左下方的结点即为中序遍历的第一个结点
    while(!T->ltag) T = T->lchild;
    return T;
}
// 解决②问题
// 找到中序遍历的 当前结点的下一个结点
ThreadNode* NextNode(ThreadNode *T)
{
    // 若 rtag = 1,直接获取下一个结点;
    if(T->rtag) return T->rchild;

    // 若 rtag = 0,则需要以当前结点的右孩子为根(T),找第一个结点
    else  return FirstNode(T->rchild);
}
// 中序线索二叉树的中序遍历算法
void InOrder(ThreadTree T)
{
    ThreadTree ptr;
    for(ptr = FirstNode(T); ptr != nullptr; ptr = NextNode(ptr))
    {
        visit(ptr);
    }
}
// 测试代码
int main(){
    ThreadTree Root = new ThreadNode('A');
    Root->lchild = new ThreadNode('B');
    Root->rchild = new ThreadNode('F');
    Root->lchild->lchild = new ThreadNode('C');
    Root->lchild->rchild = new ThreadNode('D');
    CreateInThread(Root);
    InOrder(Root);
    return 0;
}
/*
输出结果:
C  B  D  A  F  
*/

先序线索二叉树

有之前的分析,先序线索二叉树的建立线索化 和 先序遍历过程很快就能写好。

线索化

void PreThread(ThreadNode *&P, ThreadNode *&Pre)
{
    if(P)
    {
        if(!P->lchild)
        {
            P->ltag = 1;
            P->lchild = Pre;
        }

        if(Pre && !Pre->rchild)
        {
            Pre->rtag = 1;
            Pre->rchild = P;
        }

        Pre = P;

        // 线索化 左孩子
        // 仅在左孩子是真实子结点时递归
        if(!P->ltag) PreThread(P->lchild,Pre);

        // 线索化 右孩子
        // 仅在右孩子是真实子结点时递归
        if(!P->rtag) PreThread(P->rchild,Pre);
    }
}

阿Q提醒:在线索化左、右孩子时 需要判断是否为真实结点(先序如果没有加 if(!P->ltag) ,则进入死循环)。(阿Q检查这个问题检查了不少时间,最终还是在DS小姐的帮助下发现的)

遍历

// 先序线索二叉树的先序遍历
ThreadNode* NextNode_Pre(ThreadNode *T)
{
    if(!T->ltag) return T->lchild;
    else return T->rchild;
}

void visit(ThreadNode *ptr)
{
    std::cout << ptr->data << "  ";
}

void PreOrder(ThreadTree T)
{
    ThreadTree ptr;
    for(ptr = T; ptr != nullptr; ptr = NextNode_Pre(ptr))
    {
        visit(ptr);
    }
}
// 测试代码
int main(){
    ThreadTree Root = new ThreadNode('A');
    Root->lchild = new ThreadNode('B');
    Root->rchild = new ThreadNode('F');
    Root->lchild->lchild = new ThreadNode('C');
    Root->lchild->rchild = new ThreadNode('D');
    CreatePreThread(Root);
    PreOrder(Root);
    return 0;
}
/*
输出结果:
A  B  C  D  F  
*/
暂无评论

发送评论 编辑评论


				
上一篇
下一篇