一、思路分析
思考如何遍历二叉树的时候,不能从扁平化的代码入手,要从图形入手,直观而易于理解的图形让编码者能够快速找到建构程序的方法。
那么看下面这个二叉树的示意图
计算机不可能直接去遍历这个图形,它不像我们人类一样可以一眼望去识别图像,它必须按照某种顺序阅读这个图形。我们给他一个小箭头,让它用这个小箭头遍历这个图形。小箭头指到哪个结点,计算机程序就可以读哪个结点的数据。
如上图,现在箭头指向那个结点,计算机就可以读出这个结点的数据——38 。
如果对于每一个结点,我们让程序先读这个结点的数据,再读这个结点左子树的数据,再读这个结点右子树的数据,最终达到这个结点对应子树数据被全部遍历的效果。那么这种遍历方式被称为前序遍历。
好,如果用前序遍历的方式进行遍历,那么我们发现对于任何一个结点,小箭头会并且只会到达它三次,三次之后,小箭头再也不会访问这个结点了。
比方说,储存着38这个数据的结点(下面称为结点38),小箭头一共会到达它三次。
第一次:小箭头从结点50移到结点38,这之后小箭头会跳向结点30
第二次:小箭头从结点30返回结点38,这之后小箭头会跳向结点40
第三次:小箭头从结点40返回结点38,这之后小箭头会返回结点50
需要说明的是,如果这个结点的左孩子指针为空,那我们就理解成小箭头跳到了空域上又跳了回来;如果这个结点的右孩子指针为空,也理解为小箭头跳到了空域又跳了回来。在这个前提下,我们可以认为,所有结点都会到达三次。
这是一个非常重要的结论,我的代码架构就是根据这个结论展开的!
二、代码设计
结点是这样设计的,这一步乏善可陈。
typedef struct node
{
int num;
int count;
struct node* left;
//左孩子指针
struct node* right;
//右孩子指针
struct node* father;
//指向父亲的指针
}Tree;
把一个递归问题转化为一个递归问题,大概率需要使用栈这一数据结构。我使用了两个栈,stack栈储存着从根结点到当前结点的所有地址,stack[0]即是根节点地址,以此类推。isread_stack栈储存着当前路径下各个结点的数据是否被遍历的信息,isread_stack[i]的值如果是1,就意味着这个结点小箭头已经到达过一次了,接下来小箭头就要按第二次到达的流程行动了;isread_stack[i]的值如果是2,就意味着这个结点已经到达过两次了,那么接下来小箭头要按第三次到达的流程行动。对于每一个结点,小箭头都会到达三次,设计一个数据结构去储存小箭头到达了几次是至关重要的!
Tree* stack[NUM];
int isread_stack[NUM];
int top=-1;
注意,这两栈用了一个栈顶指针,也就是说stack[i]是第i层的某个被访问结点的地址,而isread_stack[i]正好记录了它是第几次被访问。
其实你也可以开一个结构体数组来当栈,一次实现我这两个栈的功能。不过本质上,都是一样的!
现在我们可以写程序了,刚开始要把栈初始化,把根结点的地址和被访问状态(也就是被访问了几次了)信息压入两个栈中。这里以前序遍历代码为例:
void read_tree1(Tree* root)
{
top=-1;
stack[++top]=root;
isread_stack[top]=0;
//栈初始化,把根结点的地址和被访问状态信息压入两个栈中
//改写代码时,这两个栈一定要calloc或开全局变量,不然程序会崩溃!!!
Tree* tmp;
//一个临时变量指针,在这里显得突兀,其实不重要,先别管它
while(1)
{
//如果是第一次到达
if (isread_stack[top]==0)
{
printf("num=%d count=%d\n",stack[top]->num,stack[top]->count);
isread_stack[top]=1;
//遍历左子树
if (stack[top]->left!=NULL)
{
tmp=stack[top]->left;
stack[++top]=tmp;
continue;
}
}
//如果是第二次到达
else if (isread_stack[top]==1)
{
isread_stack[top]=2;
//遍历右子树
if (stack[top]->right!=NULL)
{
tmp=stack[top]->right;
stack[++top]=tmp;
continue;
}
}
//如果是第三次到达
else if (isread_stack[top]==2&&top!=0)
{
isread_stack[top]=0;
top--;
}
//如果是第三次到达且这个结点是根结点
//意味着树遍历完了,函数该结束了,退出循环吧
else if (isread_stack[top]==2&&top==0)
{
break;
}
}
}
如果是第一次到达某个结点,做三件事:
1、访问本结点的数据(因为我们以前序遍历为例子)
2、把该结点访问状态信息改为已经被访问过一次(也就是isread_stack[top]=1)
3、看看左孩子指针是不是非空,如果非空,进入左孩子进行访问,(把左孩子的地址压入栈中,然后continue);否则,跳过这一步,进入下一轮循环。
如果是第二次到达某个结点,做两件事:
1、把该结点访问状态信息改为已经被访问过两次(也就是isread_stack[top]=2)
2、看看左孩子指针是不是非空,如果非空,进入右孩子进行访问,(把右孩子的地址压入栈中,然后continue);否则,跳过这一步,进入下一轮循环。
如果是第三次到达某个结点,做两件事:
1、该结点的访问次数改为0(isread_stack[top]=0)。这看起来怪异,其实是因为要为下一次访问作准备。
2、top--
当然,如果回到的是根结点,退出就是了。
注意,如果你觉得第三步有点反人类,或许你可以这样设计:
第一次访问某结点,压入左孩子地址时,顺势把左孩子的访问次数改为0.我没试过,或许是可行。很大可能是可行的!
三、示例代码
#include <stdio.h>
#include <stdlib.h>
#define NUM 100
typedef struct node
{
int num;
int count;
struct node* left;
struct node* right;
struct node* father;
}Tree;
Tree* stack[NUM];
int isread_stack[NUM];
int top=-1;
void build_tree(Tree** proot,int num);
void read_tree1(Tree* root);
int main(void)
{
int n;
scanf("%d",&n);
Tree* root;
int i;
int num;
for (i=1;i<=n;i++)
{
scanf("%d",&num);
build_tree(&root,num);
}
read_tree1(root);
return 0;
}
void build_tree(Tree** proot,int num)
{
Tree* c;
Tree* tmp;
top=-1;
if (*proot==NULL)
{
c=(Tree*)calloc(1,sizeof(Tree));
c->left=NULL;
c->right=NULL;
c->num=num;
c->count=1;
*proot=c;
}
else
{
stack[++top]=*proot;
while(1)
{
if (num==stack[top]->num)
{
(stack[top]->count)++;
break;
}
else if (num<stack[top]->num)
{
if (stack[top]->left==NULL)
{
c=(Tree*)calloc(1,sizeof(Tree));
c->left=NULL;
c->right=NULL;
c->num=num;
c->count=1;
stack[top]->left=c;
break;
}
else
{
tmp=stack[top]->left;
stack[++top]=tmp;
continue;
}
}
else
{
if (stack[top]->right==NULL)
{
c=(Tree*)calloc(1,sizeof(Tree));
c->left=NULL;
c->right=NULL;
c->num=num;
c->count=1;
stack[top]->right=c;
break;
}
else
{
tmp=stack[top]->right;
stack[++top]=tmp;
continue;
}
}
}
}
}
void read_tree1(Tree* root)
{
top=-1;
stack[++top]=root;
isread_stack[top]=0;
//栈初始化,把根结点的地址和被访问状态信息压入两个栈中
Tree* tmp;
//一个临时变量指针,在这里显得突兀,其实不重要,先别管它
while(1)
{
//如果是第一次到达
if (isread_stack[top]==0)
{
printf("num=%d count=%d\n",stack[top]->num,stack[top]->count);
isread_stack[top]=1;
//遍历左子树
if (stack[top]->left!=NULL)
{
tmp=stack[top]->left;
stack[++top]=tmp;
continue;
}
}
//如果是第二次到达
else if (isread_stack[top]==1)
{
isread_stack[top]=2;
//遍历右子树
if (stack[top]->right!=NULL)
{
tmp=stack[top]->right;
stack[++top]=tmp;
continue;
}
}
//如果是第三次到达
else if (isread_stack[top]==2&&top!=0)
{
isread_stack[top]=0;
top--;
}
//如果是第三次到达且这个结点是根结点
//意味着树遍历完了,函数该结束了,退出循环吧
else if (isread_stack[top]==2&&top==0)
{
break;
}
}
}
这里的build_tree是一个构建二叉树的函数,我就不赘述了。