树与二叉树 2节
1. 线索二叉树的逻辑结构
线索二叉树要解决什么问题?我们之前学习了先序、中序、后序、层次遍历,可是当给出一个节点时,我们如何知道概节点的下一个是哪个?上一个是哪个?其实就是将树结构线性化,我们不知道前驱和后继,必须遍历才能知道,下次寻找还要再遍历一次,正好之前我们提到完全二叉树的空指针域是n+1个,正好可以被利用记录遍历的前驱后继,以后就不用在遍历了。
线索二叉树的定义:
1)若节点有左子树,则lchild指向左孩子;否则lchild指向其前驱。
2)若节点有右子树,则rchild指向有孩子;否则rchild指向其后继。
为了避免混淆,需要添加两个标志标记是否有左子树、是否有右子树。所以节点变为如下形式。
规定,
若ltag=0,表示lchild指向左子树,若ltag=1,表示lchild指向前驱;
若rtag=0,表示rchild指向右子树,若rtag=1,表示rchild指向后继;
2. 线索二叉树的物理结构
#define ElemType char
#define OK 1
#define ERROR 0
#define Status int
typedef struct ThreadNode {
ElemType data;
ThreadNode* lchild;
ThreadNode* rchild;
int ltag;
int rtag;
}ThreadNode, * ThreadTree;
3. 中序线索二叉树
3.1 逻辑结构
如图就是一棵中序遍历线索二叉树,注意thrt是为了方便设置的头节点,我们暂时不考虑,只有叶子节点的左右链域是指向前驱和后继的,那如何再这棵树的节点上找到前驱和后继呢?
以图中的+节点举例找后继:+节点的rtag为0,所以有右子树,根据中序遍历的规则,其右子树中序遍历的第一个节点即是+节点的后继,所以我们顺着左指针找下去即可找到一个节点没有左孩子,即ltag=1,这就是+号节点的后继,b节点。
以图中的根节点-号节点举例找前驱,(根节点不是头节点),根节点的ltag=0,所以存在左子树,根据中序遍历的规则,其左子树遍历的最后一个节点即是根节点的前驱,我们通过遍历左子树找到其前驱。
头节点:我们令头节点的lchild指向根节点,rchild指向中序遍历的最后一个节点,令中序遍历的第一个节点的lchild指向头节点,中序遍历的最后一个节点的rchild指向根节点,这样我们就可以根据头节点实现正序、反序遍历这棵线索二叉树了。
3.2 代码实现
我们首先写一个中序创建普通二叉树的代码,然后对它进行线索化,第一节我们学习过创建二叉树的代码,只要稍微修改即可,把参数的BiTree换位ThreadTree:
// - + a # # * b # # - c # # d # # / e # # f # #
//是先序创建上面那棵树的输入,但是前两个/是注释别输入进去了
Status CreateTreeByPreOrder(ThreadTree& T) {
ElemType e;
cin >> e;
if (e == '#') {
T = NULL;
}
else {
T = (ThreadNode*)malloc(sizeof(ThreadNode));
if (!T) {
cout << "申请内存错误!" << endl;
exit(1);
}
T->data = e;
CreateTreeByPreOrder(T->lchild);
CreateTreeByPreOrder(T->rchild);
}
return OK;
}
然后我们将一棵树中序线索化:设置一个pre为p的前驱,pre初始化为头节点时pre不需要建立后继,只需要p建立前驱为pre,其他情况pre若没右孩子则建立后继为p,p若无左孩子则建立前驱为pre。
Status InThread(ThreadTree &p, ThreadTree &pre,ThreadTree thrt) {
if (p != NULL) {
InThread(p->lchild, pre, thrt);
if (p->lchild) {
p->ltag = 0;
}
else {
p->lchild = pre;
p->ltag = 1;
}
if (pre != thrt) {
if (pre->rchild) {
pre->rtag = 0;
}
else {
pre->rchild = p;
pre->rtag = 1;
}
}
pre = p;
InThread(p->rchild, pre, thrt);
}
return OK;
}
ThreadTree CreateInThread(ThreadTree T) {
ThreadTree pre = NULL,thrt=NULL;
thrt = (ThreadNode*)malloc(sizeof(ThreadNode));//头节点
if (!thrt) {
cout << "申请内存错误" << endl;
exit(1);
}
thrt->lchild = T; //头节点左链指向根节点
thrt->ltag = 0;
thrt->data = '#'; //方便识别头节点
pre = thrt; //设为头节点使得第一个节点的前驱是头节点
InThread(T, pre, thrt);
pre->rchild = thrt; //最后一个节点后继为头节点
pre->rtag = 1;
thrt->rchild = pre; //头节点右链指向最后一个节点
thrt->rtag = 1;
return thrt;
}
然后我们写一个遍历线索二叉树的方法:
首先要写一个找一棵树最左子树的方法方便找后继
ThreadTree InOrderFirstNode(ThreadTree p) {
while (p->lchild) p = p->lchild;
return p;
}
然后我们实现找一个节点后继的方法:
ThreadTree InOrdernextNode(ThreadTree p) {//返回p的后继节点
if (p->rtag == 0) {
return FirstNode(p->rchild);
}
else {
return p->rchild;
}
}
然后我们就可以遍历线索二叉树了:
Status InOrderThread(ThreadTree thrt) {
ThreadTree p = InOrderFirstNode(thrt->lchild);//从第一个节点开始
while (p != thrt) {
visit(p);
p = InOrdernextNode(p);
}
return OK;
}
我们实现一个找前驱的方法甚至可以倒着遍历:
/*寻找子树中序最后一个节点*/
ThreadTree InOrderLastNode(ThreadTree p, ThreadTree root) {
ThreadTree pre = NULL;
while (p != root) {
pre = p;
p = InOrdernextNode(p);
}
return pre;
}
/*寻找节点的前驱*/
ThreadTree InOrderPreNode(ThreadTree p) {
if (p->ltag == 0) {
return InOrderLastNode(p->lchild, p);
}
else {
return p->lchild;
}
}
/*逆向中序遍历*/
Status reverseInOrderThread(ThreadTree thrt) {
ThreadTree p = thrt->rchild;
while (p != thrt) {
visit(p);
p = InOrderPreNode(p);
}
return OK;
}
4. 先序线索二叉树
我们考虑如何在先序线索二叉树中找后继?
如果节点有左孩子,左孩子必然是后继,如果节点没有做孩子有右孩子,那右孩子是后继,若都没有则右链域指向的是后继。
那如何找前驱?
若有左孩子则:如果节点是双亲节点的左孩子,双亲节点必然是前驱;
如果是双亲节点的右孩子则前驱是双亲结点的左子树先序遍历的最后一个节点是前驱;
如果是根节点则无前驱;
若无左孩子则:左链指向前驱。
由此我们必须知道节点的双亲,采用三叉链表存储结构的二叉树可以方便的找到双亲。我们将在后面实现先序线索二叉树。
5. 后序线索二叉树
如何在后序线索二叉树中找后继?
若节点没有右孩子:右链域指向后继。
若节点有右孩子:该节点若是二叉树的根,则无后继;
该节点若是双亲节点的右孩子,或者是双亲节点的左孩子且双亲没有右孩子,则双亲节点是后继;
该节点若是双亲结点的左孩子且双亲节点有右孩子,则右子树后序遍历的第一个节点是后继;
如何在后序线索二叉树中找前驱?
若节点没有左孩子:左链域指向前驱;
若节点有左孩子且没有右孩子:后序遍历左子树的最后一个节点是前驱;
若节点有左孩子且有右孩子:后序遍历右子树的最后一个节点是前驱;
由此我们知道,后序线索二叉树也必须用三叉链表来存储。
6. 三叉链表的物理结构
#define Status int
#define OK 1
#define ERROR 0
#define ElemType char
typedef struct BiPNode {
ElemType data;
BiPNode* parent = NULL;
BiPNode* lchild;
BiPNode* rchild;
}BiPNode,*BiPTree;
但是我们需要对三叉链表斤建立线索,所以线索二叉树的三叉链表物理结构如下:
#define Status int
#define OK 1
#define ERROR 0
#define ElemType char
typedef struct BiPNode {
ElemType data;
BiPNode* parent = NULL;
BiPNode* lchild;
BiPNode* rchild;
int ltag;
int rtag;
}BiPNode,*BiPTree;
7. 先序线索二叉树的三叉链表存储实现
7.1 先序遍历创建一棵二叉树
在创建节点后,由该节点去找孩子,让孩子认双亲。相比于二叉链表只多了两行代码。
Status CreatBiPTreePreOrder(BiPTree &T) {
ElemType e;
cin >> e;
if (e == '#') T = NULL;
else {
T = (BiPNode*)malloc(sizeof(BiPNode));
if (!T) {
cout << "申请内存错误!" << endl;
exit(1);
}
T->data = e;
CreatBiPTreePreOrder(T->lchild);
CreatBiPTreePreOrder(T->rchild);
if (T->lchild) T->lchild->parent = T;
if (T->rchild) T->rchild->parent = T;
}
return OK;
}
//- + a # # * b # # - c # # d # # / e # # f # #
//先序创建上面的例子树的输入,从-开始
然后我们写个例子实验一下:
/*先序遍历二叉树*/
Status PreOrderBiPTree(BiPTree T) {
if (T) {
cout << T->data;
cout << "\t";
PreOrderBiPTree(T->lchild);
PreOrderBiPTree(T->rchild);
}
return OK;
}
/*找到元素e节点返回给p*/
Status check(ElemType e, BiPTree T,BiPTree &q) {
if (T) {
if (T->data == e) {
q = T;
}
check(e, T->lchild, q);
check(e, T->rchild, q);
}
return OK;
}
int main() {
BiPTree T;
cout << "先序创建二叉树:" << endl;
CreatBiPTreePreOrder(T);
PreOrderBiPTree(T);
/*检查一下*节点的双亲是否是+ */
cout << "" << endl;
BiPTree p;
check('*', T, p);//获得*节点
cout << p->data;
cout << "节点的双亲是:"<< endl;
cout << p->parent->data << endl;
}
7.2 二叉树的先序线索化
中序先遍历p的左子树,然后修改p的前驱,pre的后继,再遍历p的右子树。
而先序先修改p的前驱和pre的后继,再遍历时可能出现访问的是前驱后继而不是左右子树的情况
Status createThredBiPNodePre(BiPTree& p, BiPTree& pre) {
if (p) {
if (p->lchild) {
p->ltag = 0;
}
else {
p->lchild = pre;//左链指向前驱
p->ltag = 1;
}
if (pre->rchild) {
pre->rtag = 0;
}
else {
pre->rchild = p; //即使pre是头节点也没关系,我们在执行后会在CreateThreadBIPTreePre修改的
pre->rtag = 1;
}
pre = p;
if (p->ltag == 0) createThredBiPNodePre(p->lchild, pre);
//防止左链被修改为前驱 限制有左孩子才遍历左子树
if(p->rtag == 0)createThredBiPNodePre(p->rchild, pre);
//防止右链被修改为后继 限制有有孩子才遍历右子树
}
return OK;
}
BiPTree CreateThreadBIPTreePre(BiPTree T) {
BiPTree pre, thrt;
thrt = (BiPNode*)malloc(sizeof(BiPNode));
if (!thrt) {
cout << "申请内存错误!" << endl;
exit(1);
}
thrt->lchild = T;//头节点左链指向根节点;
thrt->ltag = 0; //根节点作为头结点的左孩子
thrt -> data = '#';
pre = thrt; //前驱节点赋值头节点,使得第一个节点的前驱指向头节点
createThredBiPNodePre(T,pre);
thrt->rchild = pre; //头节点右链指向最后一个节点
thrt->rtag = 1;
pre->rchild = thrt; //最后一个节点后继为头节点
pre->rtag = 1;
return thrt;
}
7.3 遍历先序线索二叉树
首先实现一个找节点后继的方法:
/*先序后继*/
BiPTree nextBiPNode(BiPTree p) {
if (p->ltag == 0) {//有左孩子,左孩子必然是后继
return p->lchild;
}
else { //无左孩子,有右孩子右孩子是后继没有右链指向后继
return p->rchild;
}
}
然后是遍历算法:
Status BiPTreePreOrder(BiPTree p,BiPTree thrt) {
while (p != thrt) {
cout << p->data;
cout << "\t";
p = nextBiPNode(p);
}
return OK;
}
小实验如下:
int main() {
BiPTree T,thrt;
cout << "先序创建二叉树:" << endl;
CreatBiPTreePreOrder(T);
PreOrderBiPTree(T);
cout << "" << endl;
thrt = CreateThreadBIPTreePre(T);
cout << "遍历线索二叉树" << endl;
BiPTreePreOrder(thrt->lchild, thrt);
}
7.4 逆向遍历
三叉链表在先序遍历的用处就是找前驱了,先实现一个找前驱的方法:
/*返回root左子树先序遍历的最后一个节点*/
BiPTree preLastBiPNode(BiPTree root) {
BiPTree p = root->lchild,pre=root;
while (p != root->rchild) {
pre = p;
p = nextBiPNode(p);
}
return pre;
}
/*返回节点的前驱*/
BiPTree preBiPNode(BiPTree p) {
if (p->ltag == 1) {
return p->lchild;
}
else {
if (p == p->parent->lchild) {
return p->parent;
}
else {
if (p == p->parent->rchild) {
return preLastBiPNode(p->parent);
}
}
}
}
然后我们实现遍历算法:
Status reverseBiPTreePreOrder(BiPTree p, BiPTree thrt) {
while (p != thrt) {
cout << p->data;
cout << "\t";
p = preBiPNode(p);
}
return OK;
}
小实验:
int main() {
BiPTree T,thrt;
cout << "先序创建二叉树:" << endl;
CreatBiPTreePreOrder(T);
PreOrderBiPTree(T);
cout << "" << endl;
thrt = CreateThreadBIPTreePre(T);
cout << "正向遍历线索二叉树" << endl;
BiPTreePreOrder(thrt->lchild, thrt);
cout << "" << endl;
cout << "正向遍历线索二叉树" << endl;
reverseBiPTreePreOrder(thrt->rchild, thrt);
}
8. 后序线索二叉树的三叉链表存储实现
在先序中我们已经完整的实现了三叉链表存储二叉树,后序遍历也是一样的道理,只要熟记找前驱和后继的方法即可。代码稍微改动就好了。
(树与二叉树第三节:)