树和森林的实现——树的四种存储结构、树、森林和二叉树的转换

树和森林

  • 6.7 树和森林的实现
    • 6.7.1 树的存储结构
      • 1.双亲表示法
      • 2.孩子表示法
      • 3.双亲-孩子表示法
      • 4. 孩子-兄弟表示法
      • 总结
    • 6.7.2 树、森林和二叉树的转换
      • 1. 树转化为二叉树
      • 2. 森林转化为二叉树
      • 3. 二叉树转化为森林
    • 6.7.3 树的遍历
      • 小结
    • 6.7.4 森林的遍历
      • 小结

6.7 树和森林的实现

6.7.1 树的存储结构

树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第1张图片

1.双亲表示法

  1. 由树的定义可以知道,在树中除根结点外的每个结点都有唯一的一个双亲结点,因此,可以考虑用一组连续的存储空间存储树中的每一个结点,数组中的一个元素表示为树中的一个结点。在数组元素中除包括结点本身的数据信息外,还保存该结点的双亲结点在数组中的序号(根结点的双亲域赋予-1)树的这种存储方法称为双亲表示法。
  2. 每一个数组元素有两个域:data 和 parent, data域存储结点的数据信息;parent 域存储结点的双亲在数组中的序号。
typedef struct  Node{
     
	char data;
	int parent;
}PTNode;
typedef struct{
     
	PTNode nodes[100];
	int n;
}PTree;

树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第2张图片
3. 树的双亲表示法【优点】对于实现求双亲操作很方便,时间复杂度为O(1),但【缺点】①对于求某结点的孩子结点的操作,则需要询整个数组。②另外,这种存储方式不能直接反映各兄弟结点之间的关系,所以实现求兄弟的操作也比较困难(若要找结点的孩子或者兄弟,要遍历整个树)。
4. 实际上,如果需要实现这些操作,只需将上述存储结构稍加改进:给每个数组元素增加两个域,一个域存储该数组元素所表示的结点的第一个孩子结点在数组中的序号,另一个域存储该结点的右兄弟结点在数组中的序号。在这种改进的存储结构下,就能够较方便地实现树的各种基本操作。
(1)改进一:方便获取孩子结点

/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 100

typedef int TElemType;

typedef struct PTNode    //结点结构
{
     
    TElemType data;    //结点数据
    int parent;        //双亲位置
    int child1;        //孩子结点1
    int child2;        //孩子结点2
    int child3;        //孩子结点3
}PTNode;

typedef struct //树结构
{
     
    PTNode nodes[MAX_TREE_SIZE];    //结点数组
    int r, n;    //r是根位置,n是结点数
}PTree;

树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第3张图片

  • 【缺点】消耗了大量的空间,是不必要的
  • 我们尽可能使用较小的空间,所以我们一般只添加一个长子域(最左边孩子的域),可以获取到有0个或1个孩子结点,甚至两个子树都可以获取,但是对于较多的孩子我们若是非得使用顺序存储,就得使用上面方法。
/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 100

typedef int TElemType;

typedef struct PTNode    //结点结构
{
     
    TElemType data;    //结点数据
    int parent;        //双亲位置
    int firstchild;    //长子域
}PTNode;

typedef struct //树结构
{
     
    PTNode nodes[MAX_TREE_SIZE];    //结点数组
    int r, n;    //r是根位置,n是结点数
}PTree;

(2)改进二:方便获取各兄弟之间的关系
我们只需要增加一个有兄弟域,即可依次获取所有的兄弟结点。

/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 100

typedef int TElemType;

typedef struct PTNode    //结点结构
{
     
    TElemType data;    //结点数据
    int parent;        //双亲位置
    int rightsib;    //右兄弟结点
}PTNode;

typedef struct //树结构
{
     
    PTNode nodes[MAX_TREE_SIZE];    //结点数组
    int r, n;    //r是根位置,n是结点数
}PTree;

树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第4张图片
以上改进参考:https://www.cnblogs.com/ssyfj/p/9459887.html
5. 小结
存储结构的设计是一个十分灵活的过程。
若是我们既关注孩子又关注兄弟,而且对时间遍历要求高,那么我们可以扩展上面结构含有双亲域,长子域,右兄弟域。

2.孩子表示法

  1. 树的孩子表示法有两种形式:
    (1)多重链表法
    1)用一个多重链表表示树,链表中的每个结点包括一个数据域和多个指针域。数据域存储树中结点的自身信息,每个指针指向该结点的一个孩子结点,通过各个指针反映树中各结点之间的关系。在这种表示法中,树中每个结点有多个指针域,形成了多重链。
    2)在一棵树中,由于各结点的度数各异,因此结点的指针域个数的设置有两种方法:
  • 方法一,每个结点指针域的个数等于该结点的度数;虽然【优点】在一定程度上节约了存储空间,但【缺点】由于链表中各结点是不同构的,各种操作不容易实现,所以这种方法很少采用。
  • 方法二,每个结点指针域的个数等于树的度数。【优点】在方法二中各结点是同构的,相对来讲,在这种存储方式下,各种操作容易实现,【缺点】但由于树中多数结点的度小于树的度,它为此所付出的代价是存储空间的浪费。当然,当树中各结点的度数都比较接近树的度时,可以考虑采用这种存储结构;当树中大多数结点的度数远小于树的度数时,这种存储方法就不可取了。
//根据树的度来设置孩子域的个数,例如本例中度为3,设置3个孩子域
/*树的孩子表示法结点结构定义*/
#define MAX_TREE_SIZE 100

typedef int TElemType;

typedef struct PTNode    //结点结构
{
     
    TElemType data;    //结点数据
    int child1;    //孩子1结点
    int child2;    //孩子2结点
    int child3;    //孩子3结点
}PTNode;

typedef struct //树结构
{
     
    PTNode nodes[MAX_TREE_SIZE];    //结点数组
    int r, n;    //r是根位置,n是结点数
}PTree;

【缺点】 占用了大量不必要的孩子域空指针,以上例为标准:需要3n个指针域,实际上有用n-1个(除了根节点,其他n-1个都向上需要一条边),则有2n+1个无用,浪费。
3)改进一:为每个结点添加一个结点度域,方便控制指针域的个数
树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第5张图片
【缺点】维护困难,不易实现
改进二:结合顺序结构和链式结构

/*树的孩子表示法结点结构定义*/
#define MAX_TREE_SIZE 100

typedef int TElemType;

typedef struct CTNode    //孩子结点
{
     
    int child;
    struct CTNode* next;
}*ChildPtr;

typedef struct    //表头结构 
{
     
    TElemType data;
    ChildPtr firstChild;  //这里只是一个头指针,指向第一个结点
}CTBox;

typedef struct //树结构
{
     
    CTBox nodes[MAX_TREE_SIZE];    //结点数组
    int r, n;    //r是根位置,n是结点数
}CTree;

(2)一维数组顺序存储
用一维数组顺序存储树中的各结点的信息,并将各结点的孩子信息组成一个单链表。孩子信息量中的每一个结点表示一个孩子结点,它由两个域组成,其中一个域表示该孩子结点在数组中的序号,另外一个域存储指向兄弟结点的指针。在结点数组中,每个元素包括结点的自身信息以及该结点的信息结点链表的头指针。
2. 小结
由于每个结点可有多个子树(无法确定子树个数),可以考虑使用多重链表来实现。

3.双亲-孩子表示法

将双亲表示法和孩子表示法结合起来。分别将各结的孩子结点组成一个单链表,同时用一维数组顺序存储树中的各结点,数组元素包括结点的自身信息、双亲结点在数组中的序号以及该结点的孩子结点链表的头指针。单链表中的每一个结点表示一个孩子结点,它两个域组成,其中一个城表示该孩子结点在数组中的序号,另外一个城存储指向兄弟结点的指针。

/*树的孩子表示法结点结构定义*/
#define MAX_TREE_SIZE 100

typedef int TElemType;

typedef struct CTNode    //孩子结点
{
     
    int child;
    struct CTNode* next;
}*ChildPtr;

typedef struct    //表头结构 
{
     
    TElemType data;
    int parent;
    ChildPtr firstChild;    //指向第一个孩子的指针
}CTBox;

typedef struct //树结构
{
     
    CTBox nodes[MAX_TREE_SIZE];    //结点数组
    int r, n;    //r是根位置,n是结点数
}CTree;

树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第6张图片

4. 孩子-兄弟表示法

  1. 在这种链式存储结构中,链表中一个结点代表树中的一个结点,它除信息域外,另外还有两个指针城分别指向该结点的第一个孩子结点和下一个兄弟结点。在这种存储结构中,由于每个结点有两个指针域,所以又称这种方法为二重链表表示法
    树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第7张图片
    任意一棵树,他的结点的第一个孩子如果存在就是唯一结点,他的右兄弟如果存在,也是唯一的,因此,我们设置两个指针,分别指向该结点的第一个孩子和该结点的右兄弟。
    n个结点,有2n个指针域,有n-1条边,空n+1个指针域
typedef int TElemType;

typedef struct CSNode
{
     
    TElemType data;
    struct CSNode* firstchild, *rightsib;
}CSNode,*CSTree;
  1. 在树的孩子-兄弟表示法中,每一个结点有两个指针域,并且它们是有序的,这很自然地使人联想到二叉树的二叉链表。确实,在下面讨论树和二叉树转化时可以看到:树转化为相应二叉树的二叉链表和树的孩子一兄弟链表在结构上是一样的
  2. 在这种存储结构上要【优点】查找某结点的孩子结点的操作是比较方便的。通过结点的孩子指针可以直接找到它的第一个孩于结点,再由孩子结点的兄弟指针很容易找到它的其他孩子结点。如果在每一个结点中增加一个指向双亲的指针,就可以方便地找到各结点的祖先。

总结

对于双亲表示法:我们先将双亲结点存入,我们每插入一个结点都是知道双亲结点位置的,数据可以直接插入。使用顺序存储结构更加方便;而对于孩子表示法,我们每次插入一个结点,对其子树的位置存放暂不确定,主要使用链式存储结构。

6.7.2 树、森林和二叉树的转换

1. 树转化为二叉树

  1. 约定树中每一个结点的孩子结点按从左到右的次序顺序编号,也就是说,把树作为有序树看待。
  2. 将一棵树转化为二叉树的方法如下。
    1)连线:树中所有相邻兄弟结点之间加一条线。
    2)删线:对树中的每个结点,只保留它与第一个孩子结点之间的连线,删去它与其他孩子结点之间的连线。
    3)美化:以树的根结点为轴心,将这棵树顺时针转动45°使其层次分明。
    可以证明,树作这样的转化所构成的二叉树是唯一的。
    树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第8张图片
  3. 从这个转化过程可以看出,树中的任意一个结点都对应于二叉树中的一个结点,树中结点p的第一个孩子结点在二叉树中是结点p的左孩子结点,就树中的相邻兄弟结点而言,原树中结点p的右兄弟结点在二叉树中是结点p的右孩子结点。也就是说,在二叉树中,左分支上的各结点在原来的树中是父子关系,而右分支上的各结点在原来的树中是兄弟关系。由于树的根结点没有兄弟,所以变换后的二叉树的根结点的右孩子必定为空,事实上,一棵树采用孩子-兄弟表示法所建立的存储结构与它所对应的二叉树的二叉链表存储结构是完全相同的,只是两个指针域的名称和解释不同而己。

2. 森林转化为二叉树

  1. 森林是若干棵树的集合。树可以转化为二叉树,同样森林也可以转化为二叉树。森林转化为二叉树的方法如下。
    1)依次将森林中的每棵树转化成相应的二叉树。
    2)从第二棵二叉树开始,依次把当前的二叉树作为前一棵二叉树根结点的右子树,此时所得到的二叉树就是由森林转化得到的二叉树。
  2. 下面是这一方法的描述设F={T1,T2,.……,Tn}是森林,它所对应的二叉树为B(T1,T2,.……,Tn)则有:
    1)若F为空,即n=0,则对应的二叉树B为空二叉树。
    2)若F不空,则对应的二叉树B的根 root(B)是F中第一棵树T的根 root(T):其左子树为B(T11,T12,…,T1m)。其中,T11,T12,…,T1m是rool(T1)的子树;其右子树为B(T2,T3, …,Tn),其中,T2,T3,…,Tn是F中除T外其他树构成的森林。
    树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第9张图片

3. 二叉树转化为森林

  1. 树和森林都可以转化为二叉树,二者不同的是:
    树转化成的二叉树,其根结点无右子树;
    而森林转化后的二叉树,其根结点有右子树。
  2. 显然这一转化过程是可逆的,即可以依据二义树的根结点有无右子树,将一棵二叉树转化为树或森林,具体方法如下。
    1)连线:若结点p是其双亲结点F的左孩子,则把从结点p沿右分支所找到的所有结点和结点F用线连起来。
    2)删线:删除二叉树中所有结点和其右孩子结点之间的连线。
    3)美化:整理由1)、2)两步所得到的树或森林,使之结构层次分明。
    树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第10张图片

6.7.3 树的遍历

同二叉树的遍历类似,树的遍历是指按照某种顺序访问树中的每个结点,并使每个结点被访问一次且只被访问一次。

  1. 树的先根遍历
    树的先根遍历的定义为:
    若树为空,遍历结束。否则,
    1)访问根结点;
    2)按照从左到右的顺序先根遍历根结点的每一棵子树。
  2. 树的后根遍历
    树的后根遍历的定义为:
    若树为空,遍历结束。否则,
    1)按照从左到右的顺序先根遍历根结点的每一棵子树;
    2)访问根结点。
  3. 树的层次遍历
    (1)树的层序遍历也称为树的广度遍历,它同二叉树的层序遍历一样,就是从树的第一层(根结点)开始,自上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问。
    在进行层序遍历时,对一层结点访问完后,再按照它们的先后访问次序对各个结点的孩子结点依次访问。
    (2)在进行树的层序遍历时,需要设置一个队列结构,并按下述步骤层序遍历树。
    1)初始化队列,并将根结点入队。
    2)当队列非空时,取出队头结点p,转步骤3):如果队列为空,则结束遍历。
    3)访问取出的结点p:如果结点p有孩子,则依次将它们入队列。
    4)重复步骤2)、3),直到队列为空。

小结

树的先根遍历与其转化后相应二叉树的先序遍历的结果序列相同;树的后根遍历与其转化后相应二叉树的中序遍历的结果序列相同。

树的遍历 对应二叉树的遍历
先根遍历 先序遍历
后根遍历 中序遍历

因此,在孩子-兄弟链表存储结构中,树的遍历算法也可采用相应的二义树的遍历算法实现。
树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第11张图片
树的先根遍历的结果序列为 A,B,E,F,K,L,C,G,D,H, I,M,N,J。
树的后根遍历的结果序列为E,K,L,F,B,G,C,H,M,N,I,J,D,A。
层序遍历所得到的结点序列为A,B,C,D,E,F,G,H,I,J,K,L,M,N。

6.7.4 森林的遍历

  1. 森林的先根遍历
    若森林为空,返回;否则,
    1)访问森林中第一棵树的根结点;
    2)先根遍历第一棵树的根结点的子树森林;
    3)先根遍历除第一棵外其他树组成的森林。
  2. 森林的中根遍历
    若森林为空,返回;否则,
    1)中根遍历第一棵树的根结点的子树森林;
    2)访问森林中第一棵树的根结点;
    3)中根遍历除第一棵外其他树组成的森林。
  3. 森林的后根遍历
    若森林为空,返回;否则,
    1)后根遍历第一棵树的根结点的子树森林;
    2)后遍历除第一棵外其他树组成的森林;
    3)访问森林中第一棵树的根结点。

小结

森林的先根遍历与其转化后相应二叉树的先序遍历的结果相同;森林的中根遍历与其转化后相应二叉树的中序遍历的结果相同;森林的后根遍历与其转化后相应二叉树的后序遍历的结果相同。
森林遍历结果=二叉树遍历结果
因此,森林的遍历算法也可采用相应的二叉树的遍历算法实现。
树和森林的实现——树的四种存储结构、树、森林和二叉树的转换_第12张图片
森林的先根遍历的结果序列为 A,B,C,D,E,F, G,H,I。
森林的中根遍历的结果序列为B,C,D,A,F,E,H,I,G。
森林的后根遍历的结果序列为 D,C,B,F,I,H,G,E,A。

你可能感兴趣的:(#,数据结构,数据结构)