数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第1张图片

 图源:文心一言

考研笔记整理1.4W+字,小白友好、代码先、中序可跑,后序代码有点问题仅作记录~~

第1版:查资料、写BUG、画导图、画配图~

参考用书:王道考研《2024年 数据结构考研复习指导》

参考用书配套视频:5.1.1 树的定义和基本术语_哔哩哔哩_bilibili

特别感谢: Chat GPT老师[修改BUG]、BING AI老师[修改BUG]、文心一言设计师[配图]~


目录

目录

目录

思维导图

基本概念

⏲️定义

推算举栗

⌨️代码实现

中序线索二叉树

 P0:调用库文件

 P1:定义结点与指针

 P2:封装创建结点

 P3:创建传统二叉树

 P4:初始化头结点

 P5:二叉链表线索化

 P6:二叉树遍历

 P7:main函数

 P8:执行结果

先序线索二叉树

 P0~P4:同中序二叉树

 P5:二叉链表线索化

 P6:二叉树遍历

 P7:main函数

 P8:执行结果

后序线索三叉树[失败]

后序线索二叉树无法求后序后继

 P0:调用库文件

 P1:定义结点与指针

 P2:封装创建结点

 P3:创建传统二叉树(三叉链表)

 P4:初始化头结点

 P5:链表线索化

 P6:二叉树遍历

 P7:main函数

 P8:执行结果

结语


思维导图

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第2张图片

备注: 篇幅限制,上篇内容[树:双亲、孩子、兄弟表示法][二叉树:先序、中序、后序遍历]在这里~数据结构05:树与二叉树


基本概念

⏲️定义

  • 传统二叉链表存储仅能体现一种父子关系,不能直接得到结点在遍历中的前驱或后继,且叶子结点与单孩子结点会有很多没有利用的空指针~
  • 引入线索二叉树正是为了加快查找结点前驱和后继的速度,因此,相比传统二叉树,多了两个标志域,分别是ltag与rtag,利用传统二叉树的空指针,指向其前驱与后继结点~
    • 前驱:二叉树→线性表,当前结点在线性表中的前一个结点。
    • 后继:二叉树→线性表,当前结点在线性表中的后一个结点。
线索二叉树的结点结构
变量 lchild ltag data rtag rchild
名称 左孩子指针 左标志域 数据域 右标志域 右孩子指针
用途 ltag=0时,lchild指向结点的左孩子 存放结点数据 rtag=0时,rchild指向结点的右孩子
ltag=1时,lchild指向结点的前驱 rtag=1时,rchild指向结点的后继

推算举栗

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第3张图片

  • 在传统二叉树中,找到指针为空的结点,例如本树中~
    • C结点的左右指针lchild与rchild为空,D结点的左右指针lchild与rchild为空,F结点的左右指针lchild与rchild为空;
    • 根据传统二叉树遍历顺序(数据结构05:树与二叉树数据结构05:树与二叉树),在空结点上增加线索,实现树的线索化~
  • 先序遍历线性,按根左右,输出:A→B→C→D→F
    • 令C结点的左指针→线性表的前驱结点B,右指针→线性表的后继结点D;
    • 令D结点的左指针→线性表的前驱结点C,右指针→线性表的后继结点F;
    • 令F结点的左指针→线性表的前驱结点D,右指针→发现线性表没有后继结点因此指向null;
  • 中序遍历线性,按左根右,输出:C→B→D→A→F
    • 令C结点的左指针→发现线性表没有前驱结点因此指向null,右指针→线性表的后继结点B;
    • 令D结点的左指针→线性表的前驱结点B,右指针→线性表的后继结点A;
    • 令F结点的左指针→线性表的前驱结点A,右指针→发现线性表没有后继结点因此指向null;
  •  后序遍历线性,按左右根,输出:C→D→B→F→A
    • 令C结点的左指针→发现线性表没有前驱结点因此指向null,右指针→线性表的后继结点D;
    • 令D结点的左指针→线性表的前驱结点C,右指针→线性表的后继结点B;
    • 令F结点的左指针→线性表的前驱结点B,右指针→线性表的后继结点A~

以上均为无头结点的代码,可以看到代码有1个甚至是2个null指针,于是有大佬思考,这两个指针能不能利用起来,形成1个环,模拟循环链表~线性表[顺序表+链表]线性表[顺序表+链表]

因此也可以在代码上增加头结点,用语言和图表示大概是这样的~

  • 初始化头结点:
    • 头结点head -> 左孩子lchild(ltag=0):指向树的根节点;
    • 头结点head -> 右孩子rchild(ltag=0):指向头结点head自己;
  • 线索化头结点:
    • 头结点head -> 左孩子lchild(ltag=0):指向二叉树的根节点;    //不是遍历首节点
    • 头结点head -> 右孩子rchid(ltag=1):遍历访问尾结点,本例为结点F,线索指向F;
    • 首结点 -> 左孩子lchild(ltag=1):遍历访问首结点,本例为结点D,线索指向head
    • 尾结点 -> 右孩子rchid(ltag=1):遍历访问尾结点,本例为结点F,线索指向head~

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第4张图片

 在模拟运算时,通常将指针指向头结点,然后开始遍历二叉树,直到循环到头结点为止结束~

下面我们以中序带头结点的程序为例,说明如何创建及遍历线索二叉树~


数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第5张图片

图源:BING AI 

⌨️代码实现

中序线索二叉树

 P0:调用库文件

此次用到输入输出流文件iostream与辅助队列queue~

#include 
#include 

 P1:定义结点与指针

typedef struct ThreadNode {
    char data;    //数据域
    struct ThreadNode* lchild, * rchild;    //左、右孩子指针
    int ltag, rtag;    //左、右线索标志
} ThreadNode, * ThreadTree;

 P2:封装创建结点

  • 创建结点的步骤在创建树时重复出现,因此使用函数封装,具体操作就是创建结点、赋值、并将指针与标志域置空~
//初始化结点
ThreadNode* CreateNode(char data) {
    ThreadNode* newNode = new ThreadNode();    //创建结点
    newNode->data = data;         //数据域置空
    newNode->lchild = nullptr;    //指针置空
    newNode->rchild = nullptr;
    newNode->ltag = 0;            //标志置0
    newNode->rtag = 0;
    return newNode;
}

 P3:创建传统二叉树

  • 二叉树还是使用老生常谈的队列[博文:栈、队列和数组]创建~
  • 令用户键入的根结点,创建辅助队列中,根节点入队;
  • 辅助队列不为空时,循环执行以下操作:
    • 队首元素出队并记录;
    • 二叉树孩子结点的有顺序,不能颠倒,因此采用switch区分4种情况:
      • case-1:具两个孩子结点,令用户键入2个孩子结点,分别初始化,链入队首元素,并加入辅助队列;
      • case-2:仅有左孩子节点,令用户键入1个左孩子结点,初始化,链入队首元素,并加入辅助队列;
      • case-3:仅有右孩子节点,令用户键入1个右孩子结点,初始化,链入队首元素,并加入辅助队列;
      • case-4:没有孩子结点,跳过分支,执行下一个循环~
//创建传统二叉树
void CreateTree(ThreadNode*& T) {
    //用户输入根节点
    char rootData;
    std::cout << "请输入根节点的数据: ";
    std::cin >> rootData;

    //根节点初始化
    T = CreateNode(rootData);

    //创建辅助队列,根节点入队
    std::queue nodeQueue;
    nodeQueue.push(T);

    //辅助队列不为空时,执行以下循环:,
    while (!nodeQueue.empty()) {
        //(1)依次访问辅助队列队首元素
        ThreadNode* currentNode = nodeQueue.front();
        nodeQueue.pop();
    
        //(2)询问孩子结点的情况
        int relation;
        std::cout << "请选择节点 " << currentNode->data << " 的孩子结点个数 (1-双孩子结点, 2-左孩子结点, 3-右孩子结点, 4-空孩子结点): ";
        std::cin >> relation;
        
        //(3)根据孩子结点的情况,创建孩子结点,添加到树中,并入队
        switch (relation) {
            case 1: {
                char lchildData, rchildData;
                std::cout << "请输入左孩子结点的数据: ";
                std::cin >> lchildData;
                std::cout << "请输入右孩子结点的数据: ";
                std::cin >> rchildData;

                ThreadNode* lchildNode = CreateNode(lchildData);
                ThreadNode* rchildNode = CreateNode(rchildData);

                currentNode->lchild = lchildNode;
                currentNode->rchild = rchildNode;

                nodeQueue.push(lchildNode);  // 将左孩子节点加入队列
                nodeQueue.push(rchildNode);  // 将右孩子节点加入队列

                break;
            }
            case 2: {
                char lchildData;
                std::cout << "请输入左孩子结点的数据: ";
                std::cin >> lchildData;

                ThreadNode* lchildNode = CreateNode(lchildData);
                currentNode->lchild = lchildNode;

                nodeQueue.push(lchildNode);  // 将左孩子节点加入队列

                break;
            }
            case 3: {
                char rchildData;
                std::cout << "请输入右孩子结点的数据: ";
                std::cin >> rchildData;

                ThreadNode* rchildNode = CreateNode(rchildData);
                currentNode->rchild = rchildNode;

                nodeQueue.push(rchildNode);  // 将右孩子节点加入队列

                break;
            }
            case 4:
                // Do nothing for empty child node
                break;
            default:
                std::cout << "无效的选择,请重新输入。\n";
                continue;
        }
    }
}

 P4:初始化头结点

此处需要引用指向头结点的指针ThreadNode*& head,以及树的指针ThreadNode* tree,实现让头结点初始化,以及头结点指向树的根节点操作~

void InitNode(ThreadNode*& head, ThreadNode* tree) {
    head = new ThreadNode();    //创建头结点
    head->lchild = tree;    //左孩子指向树的根节点
    head->ltag = 0;    //左标志域=0
    head->rchild = head;    //右孩子指向头结点自己
    head->rtag = 1;    //右标志域=0
}

 P5:二叉链表线索化

此处传入P2创建的树的根结点指针p,以及P3树的头结点指针pre,在传统中序遍历的基础上,增加后继前驱结点的线索~

  • 传统中序遍历的顺序是左、根、右~
  • 线索化二叉树,且如果想增加头结点,就需要遍历二叉树找到首尾结点~
  • 判断指针p是否为空,如果为空则退出递归,不为空则继续执行~
    • p指针遍历树的左孩子,访问非空结点时,表示该指针位无需线索化,递归调用本函数; //步骤同传统二叉树中序遍历
    • p指针左子树遍历完成,访问至空指针时,表示该指针可以线索化,p的左子树指针指向前驱结点pre,ltag根据规则置1,完成结点的前驱线索化
    • 前驱结点pre是否存在且右子树为空,将pre的右子树指针指向后继节点p,ltag根据规则置1,完成结点的后继线索化
    • 更新前驱结点pre为当前p指针,为下一次线索化做准备;
    • p指针遍历树的右孩子,通过非空结点时,表示该指针位无需线索化,ltag置0,递归调用本函数。 //步骤同传统二叉树遍历

有点绕对不对,用图模拟一下这个过程,如果我没有理解错的话是这样的~

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第6张图片

void InThread(ThreadTree& p, ThreadTree& 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->rchild = p;
            pre->rtag = 1;
        }
        pre = p;    //更新前驱结点的位置为本结点

        InThread(p->rchild, pre);    //递归调用本函数,遍历结点右子树
    }
}

 P6:二叉树遍历

此处传入树的头结点指针head~

  • 若树非空,继续执行以下语句;
  • 将p指向头结点的位置head;
    • p指针遍历树的左孩子,直到找到最左侧的结点,即中序遍历的首结点; //步骤同传统二叉树中序遍历
    • p指针未循环回头结点时,执行以下语句,
      • 输出p指针指向当前结点的值;
      • 如果p指针指向当前结点的右子树是线索,则通过线索找到后继结点;
      • 如果p指针指向当前结点的右子树是结点,则循环找到右子树最左侧的结点。

 运行起来应该是这样的~

  • P指针的路径一路向左,结点A、结点B、结点C,结点C没有左孩子,打印结点C
  • 结点C的线索指向结点B,打印结点B
  • 结点B具有右孩子结点D,结点D没有左孩子,打印结点D
  • 结点D的线索指向结点A,打印结点A
  • 结点A具有右孩子结点F,结点F没有左孩子,打印结点F
  • 结点F的线索指向头结点,循环判定失败,退出循环。
void InThreadOrder(ThreadTree head) {
    if (head == NULL) {
        std::cout << "树为空!" << std::endl;
        return;
    }

    std::cout << "线索二叉树中序遍历:";

    ThreadTree p = head;
    while (p->ltag == 0) {  // 寻找第一个被线索化的节点(最左边的节点)
        p = p->lchild;
    }

    while (p != head) {
        std::cout << p->data << " ";  // 输出节点的值

        if (p->rtag == 1) {  // 如果节点的右指针是线索,直接转到后继节点
            p = p->rchild;
        } else {  // 否则,找到右子树的最左边的节点
            p = p->rchild;
            while (p->ltag == 0) {
                p = p->lchild;
            }
        }
    }
}

 P7:main函数

main函数除了P0~P6的函数调用,就创建了2个结点:树的头结点head和头结点指针pre~

int main() {
    ThreadTree tree;   //P1结构:定义树的结点及指针
    CreateTree(tree);  //P3函数:创建二叉树


    ThreadNode* head;          //创建头结点
    InitNode(head, tree);      //P4函数:树结点的头结点初始化

    ThreadNode* pre = head;    //创建指向头结点的指针pre
    InThread(tree, pre);       //P5函数:二叉树中序线索化

    InThreadOrder(head);       //P6函数:二叉树遍历

    delete head;    // 释放头结点内存

    return 0;
}

 P8:执行结果

把P0~P7粘在一起,运行结果如下图所示~

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第7张图片

先序线索二叉树

 P0~P4:同中序二叉树

 P5:二叉链表线索化

此处传入P2创建的树的根结点指针p,以及P3树的头结点指针pre,在传统先序遍历的基础上,增加后继前驱结点的线索~

  • 传统先序遍历的顺序是根、左、右~
  • 线索化二叉树,且如果想增加头结点,就需要遍历二叉树找到首尾结点~
  • 判断指针p是否为空,如果为空则退出递归,不为空则继续执行~
    • p指针左子树遍历完成,访问至空指针时,表示该指针可以线索化,p的左子树指针指向前驱结点pre,ltag根据规则置1,完成结点的前驱线索化
    • 前驱结点pre是否存在且右子树为空,将pre的右子树指针指向后继节点p,ltag根据规则置1,完成结点的后继线索化
    • 更新前驱结点pre为当前p指针,为下一次线索化做准备;
    • p指针遍历树的左孩子,访问非空结点时,表示该指针位无需线索化,递归调用本函数;   //步骤同传统二叉树中序遍历
    • p指针遍历树的右孩子,通过非空结点时,表示该指针位无需线索化,ltag置0,递归调用本函数。 //步骤同传统二叉树遍历

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第8张图片

注意:此处头结点的右孩子没有完成线索化,因此需要在main函数中补充该线索~ 

void PreThread(ThreadTree& p, ThreadTree& pre) {
    if (p != NULL) {
        if (p->lchild == NULL) {       //建立前驱线索
            p->lchild = pre;
            p->ltag = 1;
        }
        if (pre != NULL && pre->rchild == 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);        //递归调用本函数,遍历结点右子树
        }
    }
}

 P6:二叉树遍历

此处传入树的头结点指针head~

  • 若树非空,继续执行以下语句;
  • 将p指向头结点的位置head,其左孩子就是根节点,即为先序遍历的首结点;
    • p指针未循环回头结点时,执行以下语句,
      • 输出p指针指向当前结点的值;
      • 如果p指针指向当前结点的左子树是结点,访问左子树
      • 如果p指针指向当前结点的左子树不是结点,则执行以下语句:
        • 如果p指针指向当前结点的右子树是线索,则循环通过右线索找到后继结点;
        • 如果p指针指向当前结点的右子树是结点,则通过右孩子指针找到后继结点。

 运行起来应该是这样的~

  • P指针的路径为头结点,寻找左孩子结点A,打印结点A
  • 结点A具有左孩子结点B,打印结点B
  • 结点B具有左孩子结点C,打印结点C
  • 结点C的右线索指向结点D,打印结点D
  • 结点D的右线索指向结点F,打印结点F
  • 结点F的线索指向头结点,循环判定失败,退出循环。
void PreThreadOrder(ThreadTree head) {
    if (head == NULL) {
        std::cout << "树为空!" << std::endl;
        return;
    }

    std::cout << "线索二叉树先序遍历:";

    ThreadTree p = head->lchild;
    while (p != head) {
        std::cout << p->data << " ";

        if (p->ltag == 0) {
            p = p->lchild;
        } else {
            while (p != head && p->rtag == 1) {
                p = p->rchild;
                std::cout << p->data << " ";
            }
            p = p->rchild;
        }
    }
    std::cout << std::endl;
}

 P7:main函数

main函数除了P0~P6的函数调用,就创建了2个结点:树的头结点head和头结点指针pre,但是因为头结点在P5完成时没有完全线索化,因此在main函数中增加了头结点指针pre的线索化,且头节点需要复位~

int main() {
    ThreadTree tree;   //P1结构:定义树的结点及指针
    CreateTree(tree);  //P3函数:创建二叉树


    ThreadNode* head;          //创建头结点
    InitNode(head, tree);      //P4函数:树结点的头结点初始化

    ThreadNode* pre = head;    //创建指向头结点的指针pre
    PreThread(tree, pre);      //P5函数:二叉树中序线索化
    pre->rchild = head;        //头结点线索化步骤
    pre->rtag = 1;
    tree = head;               //头结点复位

    PreThreadOrder(head);      //P6函数:二叉树遍历

    delete head;    // 释放头结点内存

    return 0;
}

 P8:执行结果

把P0~P7粘在一起,运行结果如下图所示~

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第9张图片

后序线索三叉树[失败]

后序线索二叉树无法求后序后继

根据刚才的栗子,我们知道,先序、中序二叉树可以直接求后序后继:中序二叉树以最左边的结点为起点,一路向右跑;先序二叉树以根结点为起点,先往左跑再往右跑~

说明这种从上到下、从左到右、甚至是从右到左的遍历,链式都能很有效~

但是后序二叉树这种从下到上就没这么幸运了,我们以图为栗~

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第10张图片

  • 看向最右下角的后序线索二叉树,后序遍历一路向左,找到结点C为起点;
  • 结点C是叶子结点,且是左子树,可以通过线索找到结点D
  • 结点D是叶子结点,且是右子树,可以通过线索找到结点B
  • 结点B非叶子结点,且是左子树,不能通过线索找到结点F
  • 结点F非叶子结点,且是右子树,可以通过线索找到结点A
  • 结点A是根结点,下一个结点是头结点,因此可以结束循环。

出现问题的地方只有B作为非叶子结点,且是左子树时,找不到兄弟结点(如果兄弟结点存在),因此我们引入双亲指针,使结点B通过父节点的孩子结点找到结点F~

话说,万一真考这个也太点背了,不过为了保证博文的完整性还是贴在了这里...

 P0:调用库文件

#include 
#include 

 P1:定义结点与指针

此处增加双亲指针*parent~

typedef struct ThreadNode {
    char data;
    struct ThreadNode* lchild, * rchild, * parent;  // 添加parent指针
    int ltag, rtag;
} ThreadNode, * ThreadTree;

 P2:封装创建结点

增加了parent的赋值,另外注意,parent是指针,不能传nullptr,因此我建了两个函数~

这里测试语句可留可删,看个人爱好~

ThreadNode* CreateNode(char data, ThreadNode*& parent) {    //创建除根结点外的普通结点
    ThreadNode* newNode = new ThreadNode();
    newNode->data = data;
    newNode->lchild = nullptr;
    newNode->rchild = nullptr;
    newNode->parent = parent;    //传递双亲结点参数
    newNode->ltag = 0;
    newNode->rtag = 0;
    //std::cout << "创建结点: 新结点 地址: " << newNode << std::endl; // 测试语句:打印出新创建的节点的地址
    //std::cout << "创建结点: 新结点 数值: " << newNode->data << std::endl; // 测试语句:打印出传入的parent指针变量所指向的值
    //std::cout << "创建结点: 父结点 地址: " << parent << std::endl; // 测试语句:打印出传入的parent指针变量的地址
    //std::cout << "创建结点: 父结点 数值: " << parent->data << "\n" << std::endl; // 测试语句:打印出传入的parent指针变量所指向的值
    return newNode;
}

ThreadNode* CreateRoot(char data, std::nullptr_t nullp) {    //创建根结点
    ThreadNode* newNode = new ThreadNode();
    newNode->data = data;
    newNode->lchild = nullptr;
    newNode->rchild = nullptr;
    newNode->parent = nullptr;
    newNode->ltag = 0;
    newNode->rtag = 0;
    return newNode;
}

话说,代码有问题询问BING AI老师时,她真的有一点凶;虽然学习的道路有点坎坷,不过最后她还是把我教会了...

BING AI老师真的怀疑我有没有认真听讲... 

 P3:创建传统二叉树(三叉链表)

原理中序二叉树中提到的完全类似,采用队列输出,仅仅需要在赋值的时候增加parent结点,在队列中即为当前结点current node~

void CreateTree(ThreadNode*& T) {
    char rootData;
    std::cout << "请输入根节点的数据: ";
    std::cin >> rootData;

    T = CreateRoot(rootData, nullptr);

    std::queue nodeQueue;
    nodeQueue.push(T);

    while (!nodeQueue.empty()) {
        ThreadNode* currentNode = nodeQueue.front();
        nodeQueue.pop();

        int relation;
        std::cout << "请选择节点 " << currentNode->data << " 的孩子结点个数 (1-双孩子结点, 2-左孩子结点, 3-右孩子结点, 4-空孩子结点): ";
        std::cin >> relation;

        switch (relation) {
            case 1: {
                char lchildData, rchildData;
                std::cout << "请输入左孩子结点的数据: ";
                std::cin >> lchildData;
                std::cout << "请输入右孩子结点的数据: ";
                std::cin >> rchildData;

                ThreadNode* lchildNode = CreateNode(lchildData, currentNode);
                currentNode->lchild = lchildNode;
                //std::cout << "左孩子结点 地址: " << lchildNode << std::endl; // 测试语句:打印出lchildNode指针变量的地址
                //std::cout << "左孩子结点 数值: " << lchildNode->data << std::endl; // 测试语句:打印出lchildNode指针变量所指向的值
                //std::cout << "当前父结点 地址: " << currentNode << std::endl; // 测试语句:打印出currentNode指针变量的地址
                //std::cout << "当前父结点 数值: " << currentNode->data << "\n" <rchild = rchildNode;
                nodeQueue.push(rchildNode);
                break;
            }
            case 2: {
                char lchildData;
                std::cout << "请输入左孩子结点的数据: ";
                std::cin >> lchildData;

                ThreadNode* lchildNode = CreateNode(lchildData, currentNode);
                currentNode->lchild = lchildNode;
                nodeQueue.push(lchildNode);
                break;
            }
            case 3: {
                char rchildData;
                std::cout << "请输入右孩子结点的数据: ";
                std::cin >> rchildData;

                ThreadNode* rchildNode = CreateNode(rchildData, currentNode);
                currentNode->rchild = rchildNode;
                nodeQueue.push(rchildNode);
                break;
            }
            case 4:
                break;
            default:
                std::cout << "无效的选择,请重新输入。\n";
                continue;
        }
    }
}

 P4:初始化头结点

此处需要引用指向头结点的指针ThreadNode*& head,以及树的指针ThreadNode* tree,实现让头结点初始化,以及头结点指向树的根节点操作~

void InitNode(ThreadNode*& head, ThreadNode* tree) {
    head = new ThreadNode();    //创建头结点
    head->lchild = tree;    //左孩子指向树的根节点
    head->ltag = 0;    //左标志域=0
    head->rchild = head;    //右孩子指向头结点自己
    head->rtag = 1;    //右标志域=0
    head->parent = nullptr;
}

 P5:链表线索化

此处传入P2创建的树的根结点指针p,以及P3树的头结点指针pre,在传统先序遍历的基础上,增加后继前驱结点的线索~

  • 传统后序遍历的顺序是左、右、根~
  • 原理十分雷同于前、中遍历,算法还是在传统遍历的基础上增加线索化过程,此处不再赘述~
  • 不过也和先序遍历有相同的问题:根结点和头结点的线索需要单独增加,理论上可以通过判断头结点增加,但是我这里测试失败,因此代码以注释的形式保存在这里了~
void PostThread(ThreadTree& p, ThreadTree& pre) {
    if (p != nullptr) {
        if (p->ltag == 0) {
            PostThread(p->lchild, pre);
        }
        if (p->rtag == 0) {
            PostThread(p->rchild, pre);
        }

        if (p->lchild == nullptr) {
            p->lchild = pre;
            p->ltag = 1;
        }
        if (pre != nullptr && pre->rchild == nullptr) {
            pre->rchild = p;
            pre->rtag = 1;
        }
        /*本小段代码是对于头结点的补充,未知原因测试失败,因此注释掉了,在main函数中补充
        if (p == head && p->rchild == nullptr) {
            p->rchild = pre;
            p->rtag = 1;
        }*/

        pre = p;
        std::cout << "PostThread p的数值"<< p->data << "  p的地址"<< p << "\n";
        std::cout << "PostThread pre的数值"<< pre->data << "  pre的地址"<< pre << "\n";
    }
}

 P6:二叉树遍历

此处传入树的头结点指针head~二叉树后序遍历为左、根、右~

代码首先从首结点开始,每一轮都会令指针P走向当前结点的父节点,然后遍历父节点的右子树,具体如下~

  • 若树非空,继续执行以下语句;
  • 设定p指针指向头结点的位置,root指针为根结点,初始置空;
  • 遍历p指针的最左侧,即为后序遍历开始的位置;
  • 如果p指针不为空时开始循环;
    • 如果p指针的右线索存在,且右线索不指向rootp指针根据右线索寻找后继结点;
    • 如果p指针的右线索不在:结合前述判定,这是父结点具有右子树的结点;
      • p指针移动到父节点;
      • 如果p指针指向的结点具有右子树,p指针移动右孩子结点的位置;
        • 如果p指针指向的结点具有左子树,执行循环访问右子树最左侧,即该右子树后序遍历开始的位置~
      • 打印p指针指向的结点~
// 后序遍历线索二叉树
void PostThreadOrder(ThreadNode* head) {
    if (head == nullptr || head->lchild == nullptr) {
        std::cout << "树为空!" << std::endl;
        return;
    }

    std::cout << "线索二叉树后序遍历: ";
    ThreadNode* p = head->lchild;
    ThreadNode* root = head->rchild;
    while (p->ltag == 0 && p->ltag == 0) {  // 寻找第一个被线索化的节点(最左边的节点)
        p = p->lchild;
    }

    while (p != head) {
        std::cout << p->data << " ";  // 输出节点的值

        if (p->rtag == 1 && p->rchild != root) {  // 如果节点的右指针是线索,直接转到后继节点(注意不能等于根结点,否则就会打循环)
            p = p->rchild;
        } else {  // 否则,节点的右孩子是已经遍历的孩子结点,因此先退回到根结点
            p = p->parent;
            //std::cout << "测试点3:" << p->data << " ";  // 输出结点的值
            if (p->rtag == 0) { // 该结点具有右孩子,则访问右孩子
                p = p->rchild;
                //std::cout << "测试点1" << p->data << " ";  // 输出结点的值
                    while (p->ltag == 0) {  // 找到右子树最左侧的结点
                         p = p->lchild;
                   //std::cout << "测试点2" << p->data << " ";  // 输出结点的值
                   }
            std::cout << p->data << " ";  // 输出结点的值
            }
        }
    }
}

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第11张图片

 运行起来应该是这样的~

  • P指针的路径一路向左,结点A、结点B、结点C,结点C设为起始遍历结点;
  • 打印结点C
  • 结点C具有右线索,顺着线索找到结点D,打印结点D
  • 结点D具有右线索,顺着线索找到结点B,打印结点B
  • 结点B没有右线索,需要访问其父节点,如果有父结点有右子树,找到右子树左侧的结点F,打印结点F// 实际测试时程序没有根据判断条件访问结点F,也就是测试点1没有输出结果,原因未知;
  • 结点F具有右线索,顺着线索找到结点A,打印结点A
  • 结点A的线索指向头结点,循环判定失败,退出循环。

 P7:main函数

main函数除了P0~P6的函数调用,就创建了2个结点:树的头结点head和头结点指针pre,但是因为头结点在P5完成时没有完全线索化,因此在main函数中增加了头结点指针pre的线索化,且头节点需要复位~

int main() {
    ThreadTree tree;
    CreateTree(tree);

    ThreadNode* head;
    InitNode(head, tree);

    ThreadNode* pre = head;
    PostThread(tree, pre);

    std::cout << "main1 树的数值"<< head->data << "  树的地址" << head << "\n";
    std::cout << "main1 pre的数值" << pre->data << "  pre的地址" << pre << "\n";

    // 完成头结点到根结点的线索化
    pre->rchild = head;
    pre->rtag = 1;
    tree = head;

    std::cout << "main2 树的数值"<< head->data << "  树的地址" << head << "\n";
    std::cout << "main2 pre的数值" << pre->data << "  pre的地址" << pre << "\n";

    PostThreadOrder(head);

    return 0;
}

 P8:执行结果

把P0~P7粘在一起,就会得到一个跟我一样可能不靠谱的结果,运行结果如下图所示~

自认为代码逻辑勉强可以自圆其说,我也不晓得为什么会这样...

另外输出有一些测试点,影响美观性,在代码里注释掉就好了...

数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]_第12张图片


结语

博文到此结束,写得模糊或者有误之处,欢迎小伙伴留言讨论与批评,督促博主优化内容,不限于以下内容~‍️

  • 有错误:这段注释南辕北辙,理解错误,需要更改~
  • 难理解:这段代码雾里看花,需要更换排版、增加语法、逻辑注释或配图~
  • 不简洁:这段代码瘠义肥辞,好像一座尸米山,需要更改逻辑;如果是C++语言,调用某库某语法还可以简化~
  • 缺功能:这段代码败絮其中,能跑,然而不能用,想在实际运行或者通过考试需要增加功能~
  • 跑不动:呃,代码都是小测过再发的,不能跑的一般会有标注~呃,如果真不能跑,告诉我哪里不能跑我再回去试试...

博文若有帮助,欢迎小伙伴动动可爱的小手默默给个赞支持一下,码字真的很不容易,博主需要精神食粮!

你可能感兴趣的:(#,数据结构,考研,数据结构,c++)