图源:文心一言
考研笔记整理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,利用传统二叉树的空指针,指向其前驱与后继结点~
- 前驱:二叉树→线性表,当前结点在线性表中的前一个结点。
- 后继:二叉树→线性表,当前结点在线性表中的后一个结点。
变量 | lchild | ltag | data | rtag | rchild |
---|---|---|---|---|---|
名称 | 左孩子指针 | 左标志域 | 数据域 | 右标志域 | 右孩子指针 |
用途 | 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指针的右线索存在,且右线索不指向root:p指针根据右线索寻找后继结点;
- 如果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++语言,调用某库某语法还可以简化~
- 缺功能:这段代码败絮其中,能跑,然而不能用,想在实际运行或者通过考试需要增加功能~
- 跑不动:呃,代码都是小测过再发的,不能跑的一般会有标注~呃,如果真不能跑,告诉我哪里不能跑我再回去试试...
博文若有帮助,欢迎小伙伴动动可爱的小手默默给个赞支持一下,码字真的很不容易,博主需要精神食粮!🥰