数据结构与算法(十)——树的存储

目录

一、前情提要

二、树的三种存储方式

(一)双亲表示法

(二)孩子表示法

方案一

方案二

(三)孩子兄弟表示法

三、代码实现

(一)双亲表示法

(二)孩子表示法

(三)孩子兄弟表示法


一、前情提要

        上篇解释了树的一系列概念,这篇围绕树的三种存储方式:双亲表示法,孩子表示法和孩子兄弟表示法进行解释,并给出相应代码并附以注释。

二、树的三种存储方式

树的基本操作

     相对于线性结构,树的操作就完全不同了,这里我们给出一些基本和常用的操作。

ADT  树(Tree)
Data
     树是由一个根结点和若干棵子树构成的。树中结点具有相同数据类型及层次关系。
	 
Operation
     InitTree(*T);//构造空树T
     DestroyTree(*T);//销毁树T
     CreateTree(*T,definition);//按definition中给出树的定义来构造树
     ClearTree(*T);//若树T存在,则将树T清空为空树
     TreeEmpty(T);//若T为空树,返回True,否则返回False
     TreeDepth(T);//返回T的深度
     Root(T);//返回T的根结点
     Value(T,Cur_E);//Cur_E是树T中一个结点,返回此结点的值。
     Assign(T,Cur_E,Value);//给树T的结点Cur_E赋值为Value。
     Parent(T,Cur_E);//若Cur_E是树T的非根结点,则返回它的双亲,否则返回空。
     LeftChild(T,Cur_E);//若Cur_E是树T的非叶结点,则返回它的最左孩子,否则返回空。
     RightChild(T,Cur_E);//若Cur_E有右兄弟,则返回它的右兄弟,否则返回空。
     InsertChild(*T,*P,i,c);//其中p所指向树T的某个结点,i为所指结点p的度+1,
	                        //非空树c与T不相交,操作结果为插入c为树T中p所指结点的第i棵子树。	 
	 
	 DeleteChild(*T,*p,i);//其中p所指树T的某个结点,i为所指结点p的度
	                      //操作结果为删除T中p所指结点的第i棵子树。
endADT

(一)双亲表示法

         对于一棵树而言,除了根结点外,其余的每个结点,它不一定有孩子,但是一定有且仅有一个双亲。

         我们假设以一组连续的空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点在数组中的位置。也就是说,每个结点除了知道自己是谁之外,还知道它的双亲在哪里。

         所以它的结构就有两部分:

         data:数据域,存储结点的数据信息。                         

         parent:指针域,存储该结点的双亲在数组中的下标。

如下图所示:

         

        由于根结点是没有双亲的,所以我们约定根结点的指针域设置为-1,这就意味着,我们所有的结点都存有它双亲的位置。

优缺点说明:

       优点:这样的存储结构,我们可以根据结点的parent指针,很容易的找到他们的双亲结点。所用的时间复杂度为O(1),知道parent为-1时,表示找到了树的根。

       缺点:那如果我们想知道结点的孩子是什么?没有办法,只能遍历一边才能知道。

Q1:那如果我们想知道结点的孩子,怎么办???

A:我们增加⼀个结点最左边孩子的域,不妨叫它长子域,这样就可以很容易得到结点的孩子。如果没有孩⼦的结点,这个长子域就设置为-1  

Q2:那如果我们想知道结点的兄弟,怎么办???

A:如果我们很关注各兄弟之间的关系,双亲表示法无法体现这样的关系,那我们怎么办? 嗯,可以增加⼀个右兄弟域来体现兄弟关系,也就是说,每⼀个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1
     
       如果结点的孩子很多,超过了 2 个。我们又关注结点的双亲、又关注结点的孩⼦、还关注结点的兄弟,而且对时间遍历要求还比较高,那么我们还可以把此结构扩展为有双亲域、长子域、再有右兄弟域。
        综上所述,存储结构的设计是⼀个非常灵活的过程⼀个存储结构设计得是否合理,取决于基于该存储结构的运算是否适合、是否方便,时间复杂度好不好等。
(二)孩子表示法

        由于树中每个结点可能存在多棵子树,可以考虑用多重链表。

        即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。

        但是,树的每个结点的度,也就是它的孩子个数是不同的。所以可以设计两种方案来解决。

方案一

        一种是指针域的个数就等于树的度。我们知道树的度就是树中各个结点度的最大值。

        结构如下图所示:

        其中Data是数据域,Child1~Childn是n个指针域,都用来指向该结点的孩子结点。

对于上面这张图来说,树的度就是3。所以我们指针域的个数就是3。

树的指针指向如下图所示:

缺点Or优点?

     这种方法在面对树中各结点的度相差很大时,显然是很浪费空间的,因为有很多的结点,它没有那么多孩子结点,所以造成了指针域是空的。不过如果树的各结点的度相差很小的时候,那就意味着开辟的空间被充分利用了,这时存储结构的缺点反而成为了优点。

     既然很多指针域都可能为空,为什么不按需分配呢?于是我们就想出了第二种方法。

方案二

      第二种方案每个结点指针域的个数等于该结点的度,我们专门取一个位置Degree来存储结点指针域的个数,其结构如下表所示:

       其中,Data是数据域;Degree是度域,也就是存储该结点的孩子结点个数;Child1~Childn就是指针域,指向该结点的各个孩子结点。

       这种方法说白了,就是在方案一中进一步优化,多一个Degree。图的样子和方案一的差不多。

优点Or缺点?

        这种方法克服了浪费空间的缺点,对空间的利用率是很高了,但是由于各个结点的链表是不同的结构,加上要维护结点的度的数值,在运算上就会带来时间的损耗。

那有没有办法,既可以提高空间利用率又能使结点的结构相同呢?

        其实是可以的。在之前我们为了遍历整棵树,把每一个结点放到一个顺序存储结构的数组中是合理的。但是每个结点的孩子是有多少是不确定的。所以我们可以对每个结点的孩子建立一个单链表体现它的关系。

        这就是我们要讲的孩子表示法。

        具体办法是,把每个结点的孩子排列起来,以单链表作为存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。

以下图为参照:

左边一条ABCDEFGHIGJ都是头结点,被存在一维数组中。

每一个头结点后面都连着一条单链表,单链表连接着这个结点的孩子结点。

但是,这也存在着问题,我们如何知道某个结点的双亲是谁呢?

        如果不修改代码的话,我们必须遍历一整棵树,会很费时。但是也有解决方法,就是将双亲表示法和孩子表示法结合一下,多开辟一块地方,存放该结点的双亲。我们将这种方法叫做“双亲孩子表示法”。应该算是孩子表示法的一种改进。

(三)孩子兄弟表示法

         上面分别从双亲的角度和孩子的角度去研究树的存储结构,如果我们从树结点兄弟的角度考虑一下呢?当然,对于树这样的层级结构来说,只研究结点的兄弟是不行的,我们观察后发现,任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的兄弟。 

         结点的结构如图所示:

我们可以看出来,左边的指针指向该结点的第一个孩子,右边的则指向它的第一个兄弟。

这种方法实现的示意图如下图所示:

        这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过孩子指针域,来找到该结点的第一个孩子,然后再通过孩子结点的兄弟指针域来找到孩子的兄弟,接着一直下去,直到找到具体的孩子

        当然,如果想找到某个结点的双亲,这个表示法也是有缺陷的,那怎么办呢?

        如果真的有必要的话,其实完全可以再增加一个双亲指针域来解决快速查找双亲的问题。这里就不细说了。

三、代码实现

(一)双亲表示法
#include <stdio.h> 
#include <stdlib.h>

#define MaxSize 10

typedef struct TreeNode 
{
	int Data;//结点中存放的数据 
	int Parent;//双亲指针域 
}Node;

Node* Tree[MaxSize];//双亲表示法顺序存储 
int Size=0;//目前存储结点的数量 
int Max=MaxSize;//存储的最大个数 


//创建根结点 
void Insert_Root(int Data)
{
	Node* New_Node=(Node*)malloc(sizeof(Node));   //开辟结点的内存空间 
	if(New_Node==NULL)
	{
		printf("申请内存失败");
		return;
	} 
	New_Node->Data=Data;
	New_Node->Parent=-1;    //给结点赋值 
	Tree[Size++]=New_Node;  //将指针装进数组中。注意Size++,和++Size的区别。 
}

//寻找双亲结点的下标 
int Find_Parent(int Parent)
{
	for(int i=0;i<Size;i++)  //遍历存储结点的数组 
	{
		if(Tree[i]->Data==Parent)
		{
			return i;//找到就返回 
		}
	}
	return -1;
}

//插入元素 
void Insert_Child(int Data,int Parent)
{
	if(Size==Max)  //如果满了就退出 
	{
		printf("元素已满");
		return;
	}
	else
	{
		int Parent_Index=Find_Parent(Parent);//找到双亲结点的下标 
		if(Parent==-1)//没有找到 
		{
			printf("不存在此双亲结点");
			return; 
		}
		else
		{
			Node* New_Node=(Node*)malloc(sizeof(Node));
			if(New_Node==NULL)
			{
				printf("申请内存失败");
				return;
			}
			New_Node->Data=Data;
			New_Node->Parent=Parent;//处理新的结点,使新结点的指针指向自己的双亲结点。 
			Tree[Size++]=New_Node;//最后存入结点数组中 
		}
	}
}
//展示树中的结点,顺带检验一下。 
void Show()
{
	for(int i=0;i<Size;i++)
	{
		printf("%d %d\n",Tree[i]->Data,Tree[i]->Parent);
	}
}
int main()
{
	
	Insert_Root(1);//创建根结点 
	Insert_Child(2,1);//下面创建的结点的双亲结点都是根结点。 
	Insert_Child(3,1); 
	Insert_Child(4,1);
	Insert_Child(5,1);
	Show();
	return 0;
}

(二)孩子表示法

#include <stdio.h>
#include <stdlib.h>
 
#define MaxSize 10

typedef struct TreeNode
{
	int Data;  //数据域 
	struct TreeNode* Next;//指针域指向孩子结点 
}Node;

Node* Tree[MaxSize]; //线性表存储头结点 
int Size=0;          //目前存储结点的个数 
int Max=MaxSize;     //存储的最大容量 


//初始化操作
void Init(int Data)
{
	Size=0; //目前的存储个数清零 
	
	Tree[Size]=(Node*)malloc(sizeof(Node));//创建根结点 
	Tree[Size]->Data=Data;
	Tree[Size]->Next=NULL;  //根结点指针域指向NULL 
	Size++;
}

//找到父节点
int Find_Parent(int Parent)
{
	for(int i=0;i<Size;i++)//遍历结点,找到双亲结点 
	{
		if(Tree[i]->Data==Parent)
		{
			return i;//找到就返回 
		}
	}
	return -1;  //没找到就返回-1 
}

//插入结点 
void Create_Tree(int Data, int Parent)
{
	if(Size==Max)//先判断是否存满了 
	{
		printf("已满");
		return;
	}
	
	Tree[Size]=(Node*)malloc(sizeof(Node));
	Tree[Size]->Data=Data;
	Tree[Size]->Next=NULL;
	Size++; 
	
	int Parent_Index=Find_Parent(Parent);
	if(Parent_Index==-1)//没找到双亲结点就退出 
	{
		printf("未找到该双亲结点");
		return;
	}
	else
	{
		Node* New_Node=(Node*)malloc(sizeof(Node));
		if(New_Node==NULL)
		{
			printf("申请内存失败"); 
			return; 
		}
		else
		{
			New_Node->Data=Data;//给新的结点赋值 
			New_Node->Next=Tree[Parent_Index]->Next;//跟单链表头插的方式一样。 
			Tree[Parent_Index]->Next=New_Node; 
		}
	}
}

//展示一下,顺带检验 
void Show()
{
	for(int i=0;i<Size;i++)
	{
		printf("下标为%d的双亲结点的孩子结点数据分别为:\n",i);
		if(Tree[i]->Next==NULL) printf("无孩子结点");
		for(Node* j=Tree[i]->Next;j!=NULL;j=j->Next)
		{
			printf("%d ",j->Data); 
		}
		printf("\n\n");
	}
}
int main()
{
	Init(1);
	Create_Tree(2,1);
	Create_Tree(3,1);
	Create_Tree(4,1);
	Create_Tree(5,1);
	Create_Tree(6,1);
	Create_Tree(7,2);
	Create_Tree(8,2);
	
	Show();
	return 0;
}

 

(三)孩子兄弟表示法

 

#include <stdio.h>
#include <stdlib.h>

typedef struct ChildSibling
{
	int Data;  //数据域 
	struct ChildSibling* Child;//孩子指针域 
	struct ChildSibling* Sibling;//兄弟指针域 
}Node;

Node* Root;//根结点 
Node* T;//临时结点 

//初始化,建立根节点
void Init(int Data)
{
	Root=(Node*)malloc(sizeof(Node));
	Root->Data=Data;
	Root->Child=NULL;//根结点的孩子指针和兄弟指针初始化为NULL 
	Root->Sibling=NULL;
}

//在以R为根的树中,查找数据为Key的结点
Node* GetNode(Node* R,int Key)
{
	if(R->Data==Key)  //如果该结点的值和Key相同,那么就直接返回 
	{
		return Root;
	}
	
	if(R->Child!=NULL)//如果该结点的值和Key不同,并且有孩子结点 
	{
		Node* P=GetNode(R->Child,Key);//递归查找以R->Child为根结点的树中,查找数据为Key的结点 
		if(P!=NULL&&P->Data==Key)  //符合要求就返回 
		{
			return P;
		}
	}
	
	if(R->Sibling!=NULL)//如果该结点的值和Key不同,并且有兄弟结点 
	{
		Node* P=GetNode(R->Sibling,Key);//递归查找以R->Sibling为根结点的树中,查找数据为Key的结点 
		if(P!=NULL&&P->Data==Key)//符合要求就返回
		{
			return P;
		}
	}
	
	return NULL;
}

//插入 :Key是插入的数据,parent是Key的父亲结点的数据 
void Insert(int Data,int Parent)
{
	T=GetNode(Root,Parent);//先找到双亲结点 
	if(T!=NULL)//如果存在 
	{
		Node* P=(Node*)malloc(sizeof(Node));
		P->Data=Data;
		
		if(T->Child!=NULL)  //如果双亲结点其他有孩子结点 
		{
			T=T->Child;    //单链表的插入操作 
			P->Sibling=T->Sibling;
			T->Sibling=P;
			P->Child=NULL;
		}
		else                //如果双亲结点没有孩子结点 
		{
			T->Child=P;    //让此结点称为双亲结点的孩子结点 
			P->Sibling=NULL;
			P->Child=NULL;
		} 
	}
	else
	{
		printf("未找到对应的双亲结点");
	}
}

int main()
{
	Init(1);
	Insert(2,1);
	Insert(3,1);
	Insert(4,1);
	Insert(5,1);
	Insert(6,1);
	return 0;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值