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

news2024/11/25 4:31:44

 图源:文心一言

考研笔记整理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:树与二叉树


🧵基本概念

⏲️定义

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

🌰推算举栗

  • 在传统二叉树中,找到指针为空的结点,例如本树中~
    • 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~

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

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


图源:BING AI 

⌨️代码实现

🧵中序线索二叉树

 🔯P0:调用库文件

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

#include <iostream>
#include <queue>

 🔯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<ThreadNode*> 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,递归调用本函数。 //步骤同传统二叉树遍历

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

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粘在一起,运行结果如下图所示~

🧵先序线索二叉树

 🔯P0~P4:同中序二叉树

 🔯P5:二叉链表线索化

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

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

注意:此处头结点的右孩子没有完成线索化,因此需要在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粘在一起,运行结果如下图所示~

🧵后序线索三叉树[失败]

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

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

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

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

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

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

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

 🔯P0:调用库文件

#include <iostream>
#include <queue>

 🔯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<ThreadNode*> 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" <<std::endl; // 测试语句:打印出currentNode指针变量所指向的值
                nodeQueue.push(lchildNode);

                ThreadNode* rchildNode = CreateNode(rchildData, currentNode);
                currentNode->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 << " ";  // 输出结点的值
            }
        }
    }
}

 运行起来应该是这样的~

  • 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粘在一起,就会得到一个跟我一样可能不靠谱的结果,运行结果如下图所示~

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

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


🔚结语

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/663202.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

领域事件解读

文章目录 EventBus简介DDD领域事件架构简析快速入门pom依赖bean配置PublisherSubscriber 设计原理PublisherSubscriber 事件总线(EventBus)&#xff0c;设计初衷是解耦系统模块&#xff0c;将系统中的各类业务操作抽象为事件模型&#xff0c;我们把产生事件的部分称之为事件的发…

电容笔哪个厂家的产品比较好?时下热门的平替苹果笔

苹果原装的Pencil&#xff0c;在市场上可是炙手可热的&#xff0c;而且苹果的这款pencil&#xff0c;也不是什么便宜的。当然&#xff0c;你可以用这个苹果笔搭配iPad&#xff0c;不过&#xff0c;如果你不想花很多钱&#xff0c;那就可以换一支普通的平替电容笔。就当前的技术…

一篇搞定C语言操作符(详解含示例)

目录 一.操作符是什么&#xff1f; 基本特征 语义 优先级 结合性 二.操作符的分类 三.操作符各类详解 1.算数操作符&#xff08; - * / %&#xff09; &#xff08;1&#xff09;优先级&#xff1a; &#xff08;2&#xff09;除法操作符&#xff08;…

数学基础-标量,向量,张量

前言 数学中&#xff0c;如何描述事务&#xff0c;以棍子为例子&#xff1a; 棍子的长度棍子方向棍子转向… 标量 单纯的形容事务的一个特征&#xff0c;如果体积&#xff0c;长度。 向量 指具有大小&#xff08;magnitude&#xff09;和方向的量。它可以形象化地表示为带…

利用腾讯云推流做7*24小时云直播

早在10年前&#xff0c;直播刚刚火的的时候&#xff0c;我就写过一个基于RTMP推流的直播工具&#xff0c;但没有利用起来&#xff0c;一直荒废了。想想真是可惜&#xff0c;不过谁怪咱精力有限切没有商业头脑呢。 最近刷B站&#xff0c;一位UP分享了无人值守的云直播方案&…

21JS12——内置对象

文章目录 一、内置对象二、查文档1、 MDN2、如何学习对象中的方法 三、Math对象1、Math对象2、案例-封装自己的数学对象3、Math的几个方法&#xff08;1&#xff09;绝对值&#xff08;2&#xff09;三个取整方法&#xff08;3&#xff09;随机数方法random&#xff08;&#x…

【深度学习】3-2 神经网络的学习- mini-batch学习

机器学习使用训练数据进行学习。使用训练数据进行学习&#xff0c;就是针对训练数据计算损失函数的值&#xff0c;也就是说,训练数据有100个的话&#xff0c;就要把这 100个损失函数的总和作为学习的指标。 求多个数据的损失函数&#xff0c;要求所有训练数据的损失函数的综合…

INTERSPEECH2023|达摩院语音实验室入选论文全况速览

近日&#xff0c;语音技术领域旗舰会议INTERSPEECH 2023公布了本届论文审稿结果&#xff0c;阿里巴巴达摩院语音实验室有17篇论文被大会收录。 01 论文题目&#xff1a;FunASR: A Fundamental End-to-End Speech Recognition Toolkit 论文作者&#xff1a;高志付&#xff0c;…

基于 AntV G2Plot 来实现一个 堆叠柱状图 加 折线图 的多图层案例

前言 最近研究了一下antv/g2的组合图例&#xff0c;并尝试做了一个不算太难的组合图&#xff0c;下面介绍一下整个图里的实现过程。 最终效果图 先来看一下最终的效果图 该图表有两部分组成&#xff0c;一部分是柱状图&#xff0c;准确说是堆叠的柱状图&#xff0c;一个柱…

【TA100】图形 3.5 Early-z和Z-prepass

一、深度测试&#xff1a;Depth Test 1.回顾深度测试的内容 深度测试位于渲染管线哪个位置 ○ 深度测试位于逐片元操作中、模板测试后、透明度混合前 为什么做深度测试 ● 深度测试可以解决&#xff1a;物体的可见遮挡性问题 ○ 我们可以用一个例子说明 ■ 图的解释&…

windows应急整理

windows应急整理 Virustotal 网站分析恶意样本 BrowingHistoryView 查看浏览器所有历史记录,可能会请求攻击者的恶意网站或者下载东西 启动项检查 开机启动项文件夹 msconfig 注册表run 键值查看 启动项 临时文件检查,temp 目录权限特殊,容易成为被利用对象 %temp%查看 tem…

华为HCIP第一天---------RSTP

一、介绍 1、以太网交换网络中为了进行链路备份&#xff0c;提高网络可靠性&#xff0c;通常会使用冗余链路&#xff0c;但是这也带来了网络环路的问题。网络环路会引发广播风暴和MAC地址表震荡等问题&#xff0c;导致用户通信质量差&#xff0c;甚至通信中断。为了解决交换网…

C# WebSocketSharp 框架的用法

效果: 一、概述 WebSocketSharp 是一个 C# 实现 websocket 协议客户端和服务端,WebSocketSharp 支持RFC 6455;WebSocket客户端和服务器;消息压缩扩展;安全连接;HTTP身份验证;查询字符串,起始标题和Cookie;通过HTTP代理服务器连接;.NET Framework 3.5或更高版本(包括…

腾讯云服务器云监控是什么?

腾讯云服务器云监控是什么&#xff1f;云监控用于监控云服务器性能资源指标如CPU利用率、内存使用量、内网外网出入带宽、TCP连接数、硬盘IOPS、硬盘IO等性能指标&#xff0c;云服务器吧建议免费开通云监控功能。 什么是云监控&#xff1f; 腾讯云服务器CVM云监控是什么&…

从小白到大神之路之学习运维第43天---第三阶段----LVS-----keepalived+LVS(DR)搭建部署

第三阶段基础 时 间&#xff1a;2023年6月19日 参加人&#xff1a;全班人员 内 容&#xff1a; keepalivedLVS(DR)搭建部署 目录 一、作用 技术特点&#xff1a; 与nginx的区别&#xff1a; 安全性&#xff1a; 配置文件&#xff1a; 二、环境简介 三、操作步骤 …

SPEC 2006 gcc version 8.3.0 (Uos 8.3.0.3-3+rebuild) x86_64 源码编译tools 错误处理笔记

编译tools 拷贝tools到安装目录 cp /mnt/iso/tools /opt/speccpu2006/ -r 执行编译 su rootcd /opt/speccpu2006/tools/src sh -x buildtools 错误 undefined reference to __alloca 编辑./make-3.82/glob/glob.c&#xff0c;注释掉以下宏判断 you should not run config…

unittest教程__测试报告(6)

用例执行完成后&#xff0c;执行结果默认是输出在屏幕上&#xff0c;其实我们可以把结果输出到一个文件中&#xff0c;形成测试报告。 unittest自带的测试报告是文本形式的&#xff0c;如下代码&#xff1a; import unittestif __name__ __main__:# 识别指定目录下所有以tes…

springcloud 中RestTemplate 是怎么和 ribbon整合,实现负载均衡的?源码分析

一、RestTemplate 拦截器了解 RestTemplate 内置了一个 ClientHttpRequestInterceptor,这个是一个拦截器操作,我们可以在请求的前后做一些事情。然后我们看一下这个类,这个类里面 有一个 intercept方法。我们看下这个实现类,里面有一个 LoadBalancerInterceptor实现类。 …

pm2详解

对于后台进程的管理&#xff0c;常用的工具是crontab&#xff0c;可用于两种场景&#xff1a;定时任务和常驻脚本。关于常驻脚本&#xff0c;今天介绍一款更好用的工具&#xff1a;pm2&#xff0c;基于nodejs开发的进程管理器&#xff0c;适用于后台常驻脚本管理&#xff0c;同…