数据结构复习笔记5.1:树

        之前,我们介绍的所有的数据结构都是线性存储结构。本章,我们所介绍的树的结构是⼀种⾮线 性的存储结构。存储的是具有⼀对多的关系的数据元素的集合。

1.树的定义

树是由n(n>=0)个结点组成的有限集,n=0时为空树,且对于非空树:

有且仅有一个特定的称为根的结点;

当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每一个集合本身又是一 棵树,称为根的子树(subtree)。

这里使用了递归定义。树中的每个结点都是某一子树的根。结点包含数据项和指向其它结点的分支信息。

2.基本术语

1.结点:一个数据元素+若干指向子树的分支

2.结点拥有的子树数(或分支数)称为结点的。结点A的度是3,C的是1,G的是零。

3.度为零的结点称为叶子结点/终端结点。K,L,F,G,M,I,J是叶子结点。度不为零的结点称为分支结点/非终端结点。

4.结点的子树的根称为该结点的孩子,该结点称为孩子的双亲。D的孩子是H,I和J,D的双亲是A。

4.同一个双亲的孩子是兄弟。  H,I和J是兄弟。

5.双亲在同一层的结点称为堂兄弟 

6.结点的祖先是从根到该结点所经过的所有结点。 M的祖先是A、D和H。

7.结点的子孙是该结点下层子树中的任意结点。 B的子孙为E、F、K和L。

8.结点的层次:从根到该结点的层数(根结点为第一层)。

9.树的深度: 指所有结点最大的层数(Max{各结点的层数})。

10.树的度:所有结点度的最大值(Max{各结点的度})。上图中树的度为3

11.有序树: 结点各子树从左至右有序,不能互换(左为第一)

12.无序树 :结点各子树可互换位置

13.森林: 指m棵互不相交的树的集合

14.任何一棵非空树是一个二元组Tree =(root,F) root 被称为根结点,F被称为子树森林

3.树结构和线性结构的比较

4.树的性质

参考文档:数据结构:树(Tree)【详解】_数据结构 树-优快云博客

5.树的存储结构

1.双亲表示法

1.代码展示
#include <stdlib.h>
#include <stdio.h>
# include <string.h>
struct node{
	int data;
	int fi;//father_i,如果fi==-1,认为是根节点。	
}t[1000];
int size;//节点的真实个数 
void initTree(int x)
{
	//加入根节点;
	t[1].data=x;
	t[1].fi=-1; 
	size++; 
}
int find(int fx)
{
      for(int i=1;i<=size;i++)
	  {
	  	  if(t[i].data==fx)
			{
				return i;
			 } 
	   } 
	   return -1;
}
void insert(int x,int fx)
{
	size++;
	t[size].data=x;
	
	int fx_i=find(fx);
	if(fx_i==-1)
	{
		printf("%d的父亲不存在\n",x);
		return ;
	}
	t[size].fi=fx_i;
}
void find_ch(int x)
{
	int x_i=find(x);
	int sum=0;//
	for(int i=1;i<=size;i++)
	{
		if(t[i].fi==x_i)
		{
			sum++;
			printf("%d ",t[i].data);
		}
	}
	if(sum==0)
	{
		printf("%d没有孩子节点",x);
	}
	printf("\n");
}
void find_fa(int x)
{
	int x_i=find(x);
	int fa_i=t[x_i].fi;
	if(fa_i==-1)
	{
		printf("该节点是根节点,没有父亲节点");
	}
	else{
		printf("%d ",t[fa_i].data);
	} 
	printf("\n");
}
int main()
{
	int n;//节点的总数
	scanf("%d",&n);
	int root;//根节点的数据
	scanf("%d",&root);
	initTree(root); 
	int x,fx;//fx是x的父亲 
	for(int i=2;i<=n;i++)
	{
		scanf("%d %d",&x,&fx);
		insert(x,fx);
	}
	
	//寻找x的孩子和父亲 
	scanf("%d",&x);
	find_ch(x); 
	find_fa(x);
	
	return 0; 
	
 } 
2.优缺点说明

        由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,这也就意味着,我们所有的结点都存有它双亲的位置。这样的存储结构,我们可以根据结点的parent指针很容易找到它的双亲结点,所⽤的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。

        可如果我们要知道结点的孩子是什么,对不起,请遍历整个结构才行。 这真是麻烦,能不能改进⼀下呢?当然可以。我们增加⼀个结点最左边孩⼦的域,不妨叫它长子域,这样就可以很容易得到结点的孩⼦。如果没有孩⼦的结点,这个长子域就设置为-1。 对于有0个或1个孩子结点来说,这样的结构是解决了要找结点孩⼦的问题了。甚⾄是有2个孩子,知道了长子是谁,另⼀个当然就是次子了。

        另外⼀个问题场景,我们很关注各兄弟之间的关系,双亲表示法⽆法体现这样的关系,那我们怎么办?嗯,可以增加⼀个右兄弟域来体现兄弟关系,也就是说, 每⼀个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1。 但如果结点的孩⼦很多,超过了2个。我们⼜关注结点的双亲、⼜关注结点的孩⼦、还关注结点的 兄弟,⽽且对时间遍历要求还⽐较⾼,那么我们还可以把此结构扩展为有双亲域、⻓⼦域、再有右兄弟域。存储结构的设计是⼀个⾮常灵活的过程。⼀个存储结构设计得是否合理,取决于基于该存储结构的 运算是否适合、是否⽅便,时间复杂度好不好等。

2.孩子表示法

1.代码展示
#include <stdlib.h>
#include <stdio.h>
# include <string.h>
//孩子链表的结点结构
typedef struct chNode{
	char data;
	struct chNode* next;
}chNode; 
//树的结构
struct Tree{
	char data;
	chNode* first;
}t[1005]; 
int size;
void initTree(char root)
{
	size++;
	t[size].data=root;
	t[size].first=NULL;
}
int find(char fx)
{
	for(int i=1;i<=size;i++)
	{
		if(t[i].data==fx)
		{
			return i;
		}
	}
	return -1;
}
void insert(char x,char fx)
{
	size++;
	t[size].data=x;//先把数据放到数组中
	t[size].first=NULL;
	//建立 x和 fx的父子关系
	int fx_i=find(fx);//fx_i是fx的下标 
	if(fx_i!=-1)
	{
		chNode* s=(chNode*)malloc(sizeof(chNode));
		s->data=x;
		//头插法
		s->next=t[fx_i].first; 
		t[fx_i].first=s;	
	 } 
}
void find_ch(char x)
{
	int x_i=find(x);
	chNode* p=t[x_i].first;
	if(p==NULL)
	{
		printf("%c没有孩子结点\n",x);
		return; 
	}
	while(p!=NULL)
	{
		printf("%c ",p->data);
		p=p->next;
	}
	printf("\n");
}
void find_fa(char x)
{
	chNode* p=NULL;
	int flag=0;
	for(int i=1;i<=size;i++)
	{
		p=t[i].first;
		while(p!=NULL&&p->data!=x)
		{
		  p=p->next;
		}
		if(p!=NULL&&p->data==x)
		{
			printf("%c\n",t[i].data);
			flag=1;
			break;
		}
		
	}
	if(flag==0)
	{
		printf("%c是根节点\n",x);
	}
	
	
}
int main()
{
	int n;//结点个数 
	scanf("%d",&n);
	char root;
	getchar();
	scanf("%c",&root); 
	initTree(root);
	char x,fx;
	for(int i=2;i<=n;i++)
	{
		getchar();
		scanf("%c %c",&x,&fx);
	 	insert(x,fx);
	}
	getchar();
	scanf("%c",&x);
	find_ch(x);
	find_fa(x);
	return 0; 
	
 } 
 
3.孩子兄弟表示法

1.代码展示
#include <stdlib.h>
#include <stdio.h>
# include <string.h>
//孩子兄弟表示法---二叉链表
typedef struct Node{
	char data;
	struct Node* first;//指向第一个孩子
	struct Node* bro;//指向右边第一个亲兄弟 
 }TNode,*TreeList; 
TreeList initTree(char root)
{
	TNode* s=(TNode*)malloc(sizeof(TNode));
	s->data=root;
	s->first=s->bro=NULL;
	return s;
}
//大问题是:在以r为根节点的树中,找到fx所在的结点 
//1.和根结点对比,r->data==fx,reture r; 否则执行2
//2.问题转化为去以r->first为根的子树中找fx find(r->fisrt,fx) 
//3.或者 以r->bro为根的子树中找fx    find(r->beo,fx)
//递归出口: r==NULL 
TNode* find(TreeList r,char fx)
{
	
	if(r==NULL||r->data==fx)
	{
		return r;
	}
	if(r->first!=NULL)
	{
		TNode* ans=find(r->first,fx);
		if(ans!=NULL&&ans->data==fx)
		{
			return ans;
		}
		
	}
	if(r->bro!=NULL)
	{
		TNode* ans=find(r->bro,fx);
		if(ans!=NULL&&ans->data==fx)
		{
			return ans;
		}	
	}
	return NULL;//若fx不存在树中,NULL 
	
}
//往以r为根节点的树中,插入数据x,x的父亲数据是fx 
void insert(TreeList r,char x,char fx)
{
	TNode* f=find(r,fx);
	if(f==NULL)
	{
		printf("该父亲结点不存在,插入失败\n"); 
		return ;
	 } 
	 TNode* s=(TNode*)malloc(sizeof(TNode));
	 s->data=x;
	 //s->first=NULL;
	 if(f->first==NULL)
	 {//x是长子
	    f->first=s; 
	    s->first=NULL;
	    s->bro=NULL;
	 }
	 else
	 {//x不是长子 
	 	TNode* fir=f->first;
	 	s->bro=fir->bro;
	 	fir->bro=s;
	 	s->first=NULL;
	 }
}
int main()
{
	int n;//结点个数 
	char root;
	TreeList r=NULL;
	scanf("%d",&n);
	getchar(); 
	scanf("%c",&root);
	r=initTree(root);
	char x,fx;
	for(int i=2;i<=n;i++)
	{
		getchar(); 
		scanf("%c %c",&x,&fx);
		insert(r,x,fx);
	}
	getchar(); 
    scanf("%c",&x);
    TNode* p=find(r,x);
    
    TNode* fir=p->first;
    if(fir!=NULL)
    {
    	p=fir;
    	while(p!=NULL)
    	{
    		printf("%c ",p->data );
    		p=p->bro;
		}
	}
	 
}
/*
10
A
B A
C A
D A
E B
F B
G B
H D
I D
J E
D
*/
/*
H I
*/
2.优缺点说明

        这种表示法,给查找某个结点的某个孩⼦带来了⽅便,只需要通过firstchild找到此结点的⻓⼦, 然后再通过⻓⼦结点的rightsib找到它的⼆弟,接着⼀直下去,直到找到具体的孩⼦。当然,如果想找某个结点的双亲,这个表示法也是有缺陷的,那怎么办呢? 对,如果真的有必要,完全可以再增加⼀个parent指针域来解决快速查找双亲的问题,这⾥就不再细谈了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值