开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第27天,点击查看活动详情.
用二叉树表示家谱关系并实现各种查找功能
- 目的:掌握二叉树遍历算法的应用,熟练使用先序、中序、后序3种递归遍历算法进行二叉树问题的求解。
- 内容:采用一棵二叉树表示一个家谱关系,要求具有以下功能:①、文件操作功能,即家谱记录的输入、家谱记录的输出、清除全部文件记录和将家谱记录存盘。②、家谱操作功能,即用括号表示法输出家谱二叉树、查找某人的所有儿子、查找某人的所有祖先。
需求分析:
-
模块1:功能选择
-
分析:功能选择模块函数,主要提供 1:文件操作 2:家谱操作 两个功能模块让用户选择。输入数字 1 的时候,出现界面 1:输入 2:输出3:清盘 4:更换始祖 0:存盘返回。返回后输入数字 2,出现界面1:括号表示法 2:二叉树表示法 3:找某人的所有儿子 4:找某人所有祖先 ,用户可以根据自己的需求进行选择。
-
模块2:二叉树的建立
-
分析:二叉树的结点有三个域,数据域和两个指针域,数据域用来存放数据,两个指针域用来分别存放指向该结点左右孩子的指针。并且还有个 root 结点,称二叉树的根节点。
-
模块3:家族成员信息的输入与输出
-
分析:依次输入一个家庭的父亲、母亲和孩子的姓名,先用定义好的结构体数组存储数据,并通过i/o流将它们保存在相应的文件里。通过循环依次输出fam数组中的数据,即输出每个家庭的父亲、母亲和孩子的姓名。
-
模块4:查找某人的儿子
-
分析:首先输入父亲的姓名,在二叉树中查找是否有此人,如果没有就输出不存在这样的父亲。如果有就先查看它的左孩子是否存在,不存在就输出这个父亲没有妻子,如果存在就查找左孩子的右孩子,没有右孩子就输出这个父亲没有孩子,存在就输出右孩子的姓名,即为查找到的儿子。
-
模块5:查找某人的祖先
-
分析:采用后序非递归遍历方法输入从根结点到*s 结点的路径,首先输入一个成员的姓名,用一个栈存入查找的路径,当找到时栈中的元素即为它的所有祖先。
-
模块6:二叉树的各种表示法
-
分析:①、括号表示法:通过先序遍历,先输出根节点,若根节点的左孩子结点或右孩子结点非空,则先输出(,然后递归左子树;如果根节点的右孩子结点非空,递归右子树,然后输出)。②、二叉树树形表示法:在二叉树凹入表示法的基础上,对母亲结点的左右子树如何输出做了一些控制,设置每一个结点的固定宽度,分清其左右结点,循环输出至为空。
具体存储方式如下图:
问题概述:设计一个对数据输入,输出,储存,查找的多功能程序,需要保存家族的基本信息,包括姓名及它们的关系,并采用二叉树来表示它们的关系,头结点作为父亲节点,他的左孩子为他的妻子,妻子结点的右孩子为他的孩子,依次存储每个家庭的信息。该题还需要具有保存文件的功能,以便下次直接使用先前存入的信息。家谱的功能是查询家族每个人的信息,并且输出它们的信息,而且还要具有查询输出的功能。
主程序流程:首先创建始祖名称,完成整个家谱的重构。进入主菜单界面,输入不同的数字进行不同的操作,1:文件操作;2:家谱操作;0:退出系统;输入其他选择,则报错。若输入的操作为1,则进入文件操作界面,1:输入 调用InputFam(fam,n)函数,完成数据输入操作;2:输出 调用OutputFile(fam,n)函数,完成数据的输出操作;3:一键清空 调用DelAll(fam,n)函数;0:存盘返回 调用SaveFile(fam,n)函数运用i/o流将已保存到数组的数据存到相应的文件中,输入其他选择,则报错。若输入的操作为2,则进入家谱操作界面,1:括号表示法 调用DispTree1(bt)函数,通过先序遍历,输出(,),完成其括号表示法的输出;2:二叉树家谱 调用DispTree2(bt)函数,在二叉树凹入表示法的基础上,对母亲结点的左右子树如何输出做了一些控制,设置每一个结点的固定宽度,分清其左右结点,循环输出至为空,以期望达到能够输出二叉树树形结构的目标;3:找某人所有儿子 调用FindSon(bt)函数 通过输入父亲姓名来检索该姓名所位于的位置,判断其是否有妻子、是否有儿子,如果有儿子将儿子输出;4:找某人所有祖先 调用Ancestor(bt)函数,输入某姓名,对该姓名先进行检索,若成功检索,调用Path(bt,p)路径函数,通过后序非递归遍历将该姓名所处位置的结点的祖先结点全部输出;0:返回;输入其他选择,则报错。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <conio.h>
#include<iostream>
using namespace std;
#define MaxSize 30 //代表了姓名字符、最大场宽、数组元素树
typedef struct fnode
{
char father[MaxSize]; //父
char wife[MaxSize]; //母
char son[MaxSize]; //子
} FamType;
typedef struct tnode
{
char name[MaxSize];
struct tnode *lchild,*rchild;
} BTree; //家谱树类型
//创建二叉树
BTree *CreatBTree(char *root,FamType fam[],int n) //从fam(含n个记录)递归创建一棵二叉树
{
int i=0,j;
BTree *bt,*p;
bt=(BTree *)malloc(sizeof(BTree)); //创建父亲节点
strcpy(bt->name,root); //将root中的COPY复制到bt->name中
bt->lchild=bt->rchild=NULL;
while (i<n && strcmp(fam[i].father,root)!=0) //比较两个字符串是否相同
i++;
if (i<n) //找到了该姓名的记录
{
p=(BTree *)malloc(sizeof(BTree)); //创建母亲节点
p->lchild=p->rchild=NULL;
strcpy(p->name,fam[i].wife);
bt->lchild=p;
for (j=0;j<n;j++) //找所有儿子
if (strcmp(fam[j].father,root)==0) //找到一个儿子
{
p->rchild=CreatBTree(fam[j].son,fam,n);
p=p->rchild;
}
}
return(bt);
}
//以括号表示法输出二叉树
void DispTree1(BTree *b) //先序遍历
{
if (b!=NULL)
{
cout<<b->name;
if (b->lchild!=NULL || b->rchild!=NULL)
{
cout<<"(";
DispTree1(b->lchild);
if (b->rchild!=NULL)
cout<<",";
DispTree1(b->rchild);
cout<<")"; //累计
}
}
}
//二叉树家谱表示法
void DispTree2(BTree *bt)
{
BTree *St[MaxSize],*p;
int Level[MaxSize][2],top,n,i,x=0,width=4;
int flag=0;
if (bt!=NULL)
{
cout<<" >>二叉树家谱表示法:"<<endl;
top=1;
St[top]=bt; //根节点进栈
Level[top][0]=width;
while (top>0)
{
p=St[top]; //退栈并凹入显示该节点值
n=Level[top][0];
for (i=1;i<=n;i++) //其中n为显示场宽,字符以右对齐显示
cout<<" ";
cout<<p->name<<endl;
if(Level[top][1]==2&&p->lchild!=NULL)
{
cout<<p->lchild->name;
flag=1;
n=0;
}
top--;
if (p->lchild!=NULL)
{ //将左子树根节点进栈
top++;
St[top]=p->lchild;
Level[top][0]=0; //显示场宽增width
Level[top][1]=1; //为左孩子节点
}
if(p->lchild!=NULL&&p->rchild!=NULL)
{
top++;
St[top]=p->rchild;
Level[top][0]=n+width; //显示场宽增width
Level[top][1]=2; //为右孩子节点
}
if (p->rchild!=NULL&&p->lchild==NULL)
{
//将右子树根节点进栈
top++;
St[top]=p->rchild;
Level[top][0]=n+width; //显示场宽增width
Level[top][1]=2; //为右孩子节点
}
}
}
}
//以凹入表示法输出
void DispTree3(BTree *bt)
{
BTree *St[MaxSize],*p;
int Level[MaxSize][2],top,n,i,width=4;
if (bt!=NULL)
{
cout<<" >>家谱凹入表示法:"<<endl;
top=1;
St[top]=bt; //根节点进栈
Level[top][0]=width;
while (top>0)
{
p=St[top]; //退栈并凹入显示该节点值
n=Level[top][0];
for (i=1;i<=n;i++) //其中n为显示场宽,字符以右对齐显示
cout<<" ";
cout<<p->name;
if (Level[top][1]==1)
cout<<"(l)";
else
cout<<"(r)";
for (i=n+1;i<=MaxSize-6;i+=2)
cout<<"--";
cout<<endl;
top--;
if (p->rchild!=NULL)
{ //将右子树根节点进栈
top++;
St[top]=p->rchild;
Level[top][0]=n+width; //显示场宽增width
Level[top][1]=2; //为右孩子节点
}
if (p->lchild!=NULL)
{ //将左子树根节点进栈
top++;
St[top]=p->lchild;
Level[top][0]=n+width; //显示场宽增width
Level[top][1]=1; //为左孩子节点
}
}
}
}
//采用先序递归算法找name为xm的节点
BTree *FindNode(BTree *bt,char xm[])
{
BTree *p=bt;
if (p==NULL)
return(NULL);
else
{
if (strcmp(p->name,xm)==0)
return(p);
else
{
bt=FindNode(p->lchild,xm);
if (bt!=NULL)
return(bt);
else
return(FindNode(p->rchild,xm));
}
}
}
//输出某人的所有儿子
void FindSon(BTree *bt)
{
char xm[MaxSize];
BTree *p;
cout<<" >>父亲姓名:";
cin>>xm;
p=FindNode(bt,xm);
if (p==NULL)
cout<<" >>不存在"<<xm<<"的父亲"<<endl;
else
{
p=p->lchild;
if (p==NULL)
cout<<" >>"<<xm<<"没有妻子"<<endl;
else
{
p=p->rchild;
if (p==NULL)
cout<<" >>"<<xm<<"没有儿子!"<<endl;
else
{
cout<<" >>"<<xm<<"的儿子:"<<" ";
while (p!=NULL)
{
cout<<p->name<<" ";
p=p->rchild;
}
cout<<endl;
}
}
}
}
//采用后序非递归遍历方法输出从根节点到*s节点的路径
int Path(BTree *bt,BTree *s)
{
BTree *St[MaxSize];
BTree *p;
int i,flag,top=-1; //栈指针置初值
do
{
while (bt) //将bt的所有左节点进栈
{
top++;
St[top]=bt;
bt=bt->lchild;
}
p=NULL; //p指向当前节点的前一个已访问的节点
flag=1; //设置bt的访问标记为已访问过
while (top!=-1 && flag)
{
bt=St[top]; //取出当前的栈顶元素
if (bt->rchild==p) //右子树不存在或已被访问,访问之
{ if (bt==s) //当前访问的节点为要找的节点,输出路径
{
cout<<" >>所有祖先:";
for (i=0;i<top;i++)
cout<<St[i]->name<<" ";
cout<<endl;
return 1;
}
else
{
top--;
p=bt; //p指向则被访问的结
}
}
else
{
bt=bt->rchild; //bt指向右子树
flag=0; //设置未被访问的标记
}
}
} while (top!=-1); //栈不空时循环
return 0; //其他情况时返回0
}
//输出某人的所有祖先
void Ancestor(BTree *bt)
{
BTree *p;
char xm[MaxSize];
cout<<" >>输入姓名:";
cin>>xm;
p=FindNode(bt,xm);
if (p!=NULL)
Path(bt,p);
else
cout<<" >>查无此人"<<endl;
}
//清除家谱文件全部记录
void DelAll(FamType fam[],int &n)
{
FILE *fp;
if ((fp=fopen("fam.dat","wb"))==NULL)
{
cout<<" >>不能打开家谱文件"<<endl;
return;
}
n=0;
fclose(fp);
}
//读家谱文件存入fam数组中
void ReadFile(FamType fam[],int &n)
{
FILE *fp;
long len;
int i;
if ((fp=fopen("fam.dat","rb"))==NULL)
{
n=0;
return;
}
fseek(fp,0,2); //家谱文件位置指针移到家谱文件尾
len=ftell(fp); //len求出家谱文件长度
rewind(fp); //家谱文件位置指针移到家谱文件首
n=len/sizeof(FamType); //n求出家谱文件中的记录个数
for (i=0;i<n;i++)
fread(&fam[i],sizeof(FamType),1,fp);//将家谱文件中的数据读到fam中
fclose(fp);
}
//将fam数组存入数据家谱文件
void SaveFile(FamType fam[],int n)
{
int i;
FILE *fp;
if ((fp=fopen("fam.dat","wb"))==NULL)
{
cout<<" >>数据家谱文件不能打开"<<endl;;
return;
}
for (i=0;i<n;i++)
fwrite(&fam[i],sizeof(FamType),1,fp);
fclose(fp);
}
//添加一个记录
void InputFam(FamType fam[],int &n)
{
cout<<" >>输入父亲、母亲和儿子姓名:";
cin>>fam[n].father;
cin>>fam[n].wife;
cin>>fam[n].son;
n++;
}
//输出家谱文件全部记录
void OutputFile(FamType fam[],int n)
{
int i;
if (n<=0)
{
cout<<" >>没有任何记录"<<endl;
return;
}
cout<<" 父亲 母亲 儿子"<<endl;
cout<<" ------------------------------"<<endl;
for (i=0;i<n;i++)
printf(" %10s%10s%10s\n",fam[i].father,fam[i].wife,fam[i].son);
cout<<" ------------------------------"<<endl;
}
算法结果与分析:
- 菜单函数功能测试:
- 输入功能函数测试
- 输出功能函数测试
- 查询功能函数测试
- 二叉树的各种表示法函数测试
心得体会: 程序的关键是掌握二叉树的相关操作、二叉树的创建和运用、结点的查找、祖宗结点的查找等。在编程的过程中,出现了很多问题,如二叉树无法建立、程序内存读取不了、忘了添加头文件等错误。在单步调试和添加提示输出的情况下修改程序运行正确。查找首先要判断该结点是否为空,再与查找到的结点比较,否则会内存无法读取,强行结束程序。祖宗结点的查找一直是个大问题,在参考书的帮助下想到了后序遍历,是可以从孩子往上找到。家谱的功能是查询家族每个人的信息,并且输出它们的信息,还要具有查询输出功能。这样复习了一下查询、插入、删除函数的应用。并学习了i/o流的文件储存及调用功能,复习了子函数和递归调用功能,并熟练运用这些函数。