【C++编程】用二叉树表示家谱关系并实现各种查找功能

459 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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流的文件储存及调用功能,复习了子函数和递归调用功能,并熟练运用这些函数。