线索二叉树的建立和遍历(附源码)(前/中/后序)「数据结构算法精讲」

前言:二叉树的非递归遍历算法(点此学习算法)避免了系统栈的调用,提高了执行效率。而线索二叉树可以将用户栈也省去,把二叉树的遍历过程线性化,进一步提高效率(但代价就是会使用较多的的存储空间)。

完整代码见文章末尾


普通的二叉链表,n个结点的二叉树会有n+1个空链域,为了把这些空链域有效的利用起来,线索二叉树出现了。
线索二叉树的结点构造如下:

lchildltagdatartagrchild
左孩子左标识域数据域右孩子右标识域
typedef struct TBTNode // 定义线索二叉树的节点
{
    char data; // 节点数据类型
    int ltag, rtag; // 线索标记
    struct TBTNode *lchild; // 左孩子
    struct TBTNode *rchild; // 右孩子
}TBTNode;

中序线索化二叉树

由于二叉树的(前序/中序/后序)线索化都极为相似,所以本篇着重讲解中序线索化

二叉树线索化的过程中,会把树中的空指针利用起来作为寻找当前结点前驱或后继的线索,但这样会出现一个问题,即线索和树中原有的指向孩子结点的指针无法区分。所以在线索二叉树的结构中额外添加了 ltag 和 rtag 两个标识域,用来区分他们的孩子是否存在,具体含义如下:

  1. 如果 ltag=0,表示 lchild 为指针,指向结点的左孩子;如果 ltag=1,表示 lchild 为线索,指向结点的直接前驱。
  2. 如果 rtag=0,表示 rchild 为指针,指向结点的右孩子;如果 rtag=1,表示 rchild 为线索,指向结点的直接后继。

分析:使用二叉树中序递归算法框架,在遍历过程中连接上合适的线索即可

规则

  1. 在某一时刻 p 指针指向当前正在访问的结点,pre指向 p 的前驱结点;
  2. p 的左线索如果存在,则让其指向 pre;
  3. pre 的右线索如果存在则让其指向 p,因为 p 是 pre 的后继结点;
  4. 当 p 将要离开一个访问过的结点的时候,pre 指向 p,当 p 来到一个新的结点时,pre 显然指向的是此时 p 所指结点的前驱结点。

例子
在这里插入图片描述

  1. 如图所示,某一时刻 p 指向 A,pre 指向了中序遍历过程中 A 的前驱结点 D
  2. A 是 D 的后继,D 的右线索存在则指向 A

……当整颗二叉树遍历完的时候,线索化也就完成了。代码如下:

void InThread(TBTNode *p, TBTNode *&pre)
{
    if(p != NULL)
    {
        InThread(p->lchild, pre); // 递归,左子树线索化
        if(p->lchild == NULL)
        {
            // 建立当前节点的前驱线索
            p->lchild = pre;
            p->ltag = 1;
        }
        if(pre != NULL && pre->rchild == NULL) // pre != NULL 排除第一个结点前驱为空的情况
        {
            // 建立当前节点的后继线索
            pre->rchild = p;
            pre->rtag = 1;
        }
        pre = p;
        p = p->rchild;
        InThread(p, pre); // 递归,右子树线索化
    }
}

void createInThread(TBTNode *root)
{
    TBTNode *pre = NULL;
    if(root != NULL)
    {
        InThread(root, pre);
        pre->rchild = NULL; // 非空二叉树,线索化
        pre->rtag = 1; // 后处理中序最后一个结点
    }
}

中序遍历线索二叉树

因为二叉树已经被中序线索化,所以我们遍历链表,并按照中序(左->根->右)的方式访问结点即可。

  1. 优先访问根左子树,找到第一个访问的结点,也就位于树最左下的结点(不一定是叶子结点);
  2. 找到结点,若当前结点存在右子树,优先访问右子树的左子树(中序遍历的要求);
  3. 若当前结点不存在右子树,直接访问当前结点的后继结点(线索化时添加的后继线索);
  4. 重复2、3。

由此可见,第一个访问的结点,和后序访问的其他结点的访问方式有所不同,所以需要将逻辑分开,容易得到代码:

TBTNode *First(TBTNode *p) // 中序序列下第一个结点访问算法
{
    while (p->ltag == 0)
        p = p->lchild; // 最左下结点(不一定是叶子结点)
    return p;
}

TBTNode *Next(TBTNode *p) // 中序序列下后继结点访问算法
{
    if(p->rtag == 0)
        return  First(p->rchild);
    else
        return p->rchild; // rtage == 1,直接返回后序线索
}

void Inorder(TBTNode *root)
{
    for(TBTNode *p = First(root); p != NULL; p = Next(p))
        cout << p->data; // 访问结点
}

前序线索化二叉树

前序线索化代码和中序线索化代码极为相似,最大的区别就是把连接线索的代码提到了两递归入口前边,这也符合先序递归遍历的框架。

void preThread(TBTNode *p, TBTNode *&pre) // 前序线索化二叉树子函数
{
    if(p != NULL)
    {
        if(p->lchild == NULL)
        {
            // 建立当前节点的前驱线索
            p->lchild = pre;
            p->ltag = 1;
        }
        if(pre != NULL && pre->rchild == NULL) // pre != NULL 排除第一个结点前驱为空的情况
        {
            // 建立当前节点的后继线索
            pre->rchild = p;
            pre->rtag = 1;
        }
        pre = p;
        // 注意,这里在递归入口处有条件限制,左、右指针不是线索才能继续递归
        if(p->ltag == 0)
            preThread(p->lchild, pre); // 递归,左子树线索化
        if(p->rtag == 0)
            preThread(p->rchild, pre); // 递归,右子树线索化
    }
}

void createPreThread(TBTNode *root) // 前序线索化二叉树
{
    TBTNode *pre = NULL;
    if(root != NULL)
    {
        preThread(root, pre);
        pre->rchild = NULL; // 非空二叉树,线索化
        pre->rtag = 1; // 后处理中序最后一个结点
    }
}

前序遍历线索二叉树

void preorder(TBTNode *root)
{
    if(root != NULL)
    {
        TBTNode *p = root;
        while (p != NULL)
        {
            while (p->ltag == 0) // 左指针不是线索,则边访问边左移
            {
                cout << p->data; // 访问结点
                p = p->lchild; // 左移,访问左子树
            }
            cout << p->data; // 此时p左必为线索,但还没有被访问,则访问
            p = p->rchild; // 此时p左孩子不存在,则右指针若非空,则不论是否为线索都指向其后继
        }
    }
}

后序线索化二叉树

后序线索化代码和中序线索化代码极为相似,最大的区别就是把连接线索的代码提到了两递归入口后边,这也符合后序递归遍历的框架。

void postThread(TBTNode *p, TBTNode *&pre) // 后序线索化二叉树子函数
{
    if(p != NULL)
    {
        preThread(p->lchild, pre); // 递归,左子树线索化
        preThread(p->rchild, pre); // 递归,右子树线索化
        if(p->lchild == NULL)
        {
            // 建立当前节点的前驱线索
            p->lchild = pre;
            p->ltag = 1;
        }
        if(pre != NULL && pre->rchild == NULL) // pre != NULL 排除第一个结点前驱为空的情况
        {
            // 建立当前节点的后继线索
            pre->rchild = p;
            pre->rtag = 1;
        }
        pre = p;
    }
}

后序遍历线索二叉树

考试几乎会不考到后序线索遍历,故省略。
日后有需要的话再来补充。

最后附上源码,希望能对你有所帮助~

#include <stdio.h>
#include <iostream>
using namespace std;

typedef struct TBTNode // 定义线索二叉树的结点
{
    char data; // 结点数据类型
    int ltag, rtag; // 线索标记
    struct TBTNode *lchild; // 左孩子
    struct TBTNode *rchild; // 右孩子
}TBTNode;

TBTNode *createBinaryTree() // 前序递归创建二叉树
{
    TBTNode *p;
    char ch;
    cin >> ch;
    if(ch == '#')
    {
        p = NULL;
    }
    else
    {
        p = (TBTNode*)malloc(sizeof(TBTNode));
        p->data = ch;
        p->lchild = createBinaryTree();
        p->rchild = createBinaryTree();
    }
    return  p;
}

void InThread(TBTNode *p, TBTNode *&pre) // 中序线索化二叉树子函数
{
    if(p != NULL)
    {
        InThread(p->lchild, pre); // 递归,左子树线索化
        if(p->lchild == NULL)
        {
            // 建立当前节点的前驱线索
            p->lchild = pre;
            p->ltag = 1;
        }
        if(pre != NULL && pre->rchild == NULL) // pre != NULL 排除第一个结点前驱为空的情况
        {
            // 建立当前节点的后继线索
            pre->rchild = p;
            pre->rtag = 1;
        }
        pre = p;
        p = p->rchild;
        InThread(p, pre); // 递归,右子树线索化
    }
}

void createInThread(TBTNode *root) // 中序线索化二叉树
{
    TBTNode *pre = NULL;
    if(root != NULL)
    {
        InThread(root, pre);
        pre->rchild = NULL; // 非空二叉树,线索化
        pre->rtag = 1; // 后处理中序最后一个结点
    }
}

TBTNode *First(TBTNode *p) // 中序遍历线索二叉树-第一个结点访问算法
{
    while (p->ltag == 0)
        p = p->lchild; // 最左下结点(不一定是叶子结点)
    return p;
}

TBTNode *Next(TBTNode *p) // 中序遍历线索二叉树-后继结点访问算法
{
    if(p->rtag == 0)
        return  First(p->rchild);
    else
        return p->rchild; // rtage == 1,直接返回后序线索
}

void Inorder(TBTNode *root) // 中序遍历线索二叉树
{
    for(TBTNode *p = First(root); p != NULL; p = Next(p))
        cout << p->data; // 访问结点
}

void preThread(TBTNode *p, TBTNode *&pre) // 前序线索化二叉树子函数
{
    if(p != NULL)
    {
        if(p->lchild == NULL)
        {
            // 建立当前节点的前驱线索
            p->lchild = pre;
            p->ltag = 1;
        }
        if(pre != NULL && pre->rchild == NULL) // pre != NULL 排除第一个结点前驱为空的情况
        {
            // 建立当前节点的后继线索
            pre->rchild = p;
            pre->rtag = 1;
        }
        pre = p;
        // 注意,这里在递归入口处有条件限制,左、右指针不是线索才能继续递归
        if(p->ltag == 0)
            preThread(p->lchild, pre); // 递归,左子树线索化
        if(p->rtag == 0)
            preThread(p->rchild, pre); // 递归,右子树线索化
    }
}

void createPreThread(TBTNode *root) // 前序线索化二叉树
{
    TBTNode *pre = NULL;
    if(root != NULL)
    {
        preThread(root, pre);
        pre->rchild = NULL; // 非空二叉树,线索化
        pre->rtag = 1; // 后处理中序最后一个结点
    }
}

void preorder(TBTNode *root) // 前序遍历线索二叉树
{
    if(root != NULL)
    {
        TBTNode *p = root;
        while (p != NULL)
        {
            while (p->ltag == 0) // 左指针不是线索,则边访问边左移
            {
                cout << p->data; // 访问结点
                p = p->lchild; // 左移,访问左子树
            }
            cout << p->data; // 此时p左必为线索,但还没有被访问,则访问
            p = p->rchild; // 此时p左孩子不存在,则右指针若非空,则不论是否为线索都指向其后继
        }
    }
}

void postThread(TBTNode *p, TBTNode *&pre) // 后序线索化二叉树子函数
{
    if(p != NULL)
    {
        preThread(p->lchild, pre); // 递归,左子树线索化
        preThread(p->rchild, pre); // 递归,右子树线索化
        if(p->lchild == NULL)
        {
            // 建立当前节点的前驱线索
            p->lchild = pre;
            p->ltag = 1;
        }
        if(pre != NULL && pre->rchild == NULL) // pre != NULL 排除第一个结点前驱为空的情况
        {
            // 建立当前节点的后继线索
            pre->rchild = p;
            pre->rtag = 1;
        }
        pre = p;
    }
}


int main()
{
    cout << "开始创建二叉树(输入用例:123##5##4##):" << endl;
    TBTNode *root = NULL;
    root = createBinaryTree();
    cout << "二叉树建立成功!" << endl;
    
    createInThread(root);
    cout << "二叉树中序线索化完成!" << endl;
    cout << "遍历中序线索二叉树:";
    Inorder(root);
    cout << endl;

//    createPreThread(root);
//    cout << "二叉树前序线索化完成!" << endl;
//    cout << "遍历前序线索二叉树:";
//    preorder(root);
//    cout << endl;
    
    return 0;
}

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值