数据结构之树

导读

  树是一种非线性的数据结构,树结构的应用十分广泛,在很多算法中都有应用。本文对树这种数据结构进行了总结。

目录

  1、树的定义

  2、树的基本术语

  3、树的存储结构(3种)

  4、树和森林转化为二叉树

  5、树和森林的遍历(树两种、森林两种)

树的定义

  树结构可以说是我们日常生活中“树”的一种抽象,树只有一个根,其余都是树的枝叶,数据结构中的树类似一颗“倒立”的树,如下:

  这颗树是由若干结点(A~Q)组成,所以树是由唯一的根结点和互不相干的若干子树组成。树的定义:由n(n>=0)个有限结点组成一个具有层次关系的集合。

  当结点数n=0时,称为空树,这是一种特殊状态。由上可知,树的定义是递归的,即在整颗树中,若干子树也是树的定义。

树的基本术语

  以上图为例进行说明。(这些概念不必死记,很好理解)

  结点:A~Q都是结点,结点不仅包含了数据元素,还包含了指向子树的分支信息,如A结点就包含了6个分支信息。(类似链式存储中的结点,既有数据域,又有指针域)。

  结点的度:结点拥有的子树个数(或分支个数)。如A结点的度为6,B结点的度为0。

  树的度:树中各个度的最大值。如该树的度就是A结点的度,为6。

  叶子结点:度为0的结点。如B、C、H、I、K、L、M、N、P、Q都是树的叶子结点。

  孩子:结点子树的根结点称为孩子。如E的孩子I和J。(所以叶子结点莫有孩子)

  双亲:与孩子的定义对应,如E的孩子I和J,那么反过来,I和J的双亲就是E。

  兄弟:同一双亲的孩子互相称为兄弟,比如P和Q。

  祖先:从根结点到某结点的路径上的所以结点,都是该结点的祖先,如路径:A-E-J-Q,那么Q的结点的祖先就是A、E、J。

  子孙:和祖先对应,以某结点作为根结点的所有子树的结点都是该结点的子孙,如路径:A-E-J-Q,那么A的子孙就是E、J、Q。

  层次:从根开始为第一层,根的孩子为第二层,根的孩子的孩子为第三层,依次类推。可以理解为家族树中的辈份概念,所有该树有4层。

  树的高度:或称为树的深度,表示树中结点的最大层次即为树的高度,例如该树的高度为4。(注意和树的度加以区分)。

  结点的高度和深度:两个相反地概念,结点的高度是从叶子结点开启计算层数,而结点的深度是从根结点开始计算层数。例如B的高度为:3,深度为2。

  堂兄弟:双亲在同一层的的结点互称为堂兄弟。

  有序树:树中结点的子树从左往右是有次序的,不能交换,这样的树称为有序树。(类似兄弟有长幼之分,兄必须在左,第必须在右)。

  无序树:与有序树对应,子树的的左右顺序可以交换的树。

  丰满树:即理想平衡树,除了最底层外其他层都是满的(即叶子结点全部在最底层的树)。

  森林:若干互不相交的树组成的集合称为森林。

树的存储结构

  为了方便讲解,我们使用下图作为案例树进行说明。

                        

  1、双亲存储结构

  我们利用树的双亲唯一的这个特点,存储每个结点时只需要存储自身数据和双亲即可完成整颗树的存储,这就是双亲存储的原理。双亲存储结构是一种树的顺序存储结构,使用二维数组即可实现。在存储时我们先从上往下、从左往右依次为树结点标上序号,这个序号也代表数组的角标,这个角标就用来表示这个结点的索引。

  

  ps:双亲存储结构在克鲁斯卡尔算法中有重要应用。

  2、孩子储存结构

  除了围绕双亲来存储我们还可以围绕孩子来存储树,但是树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法(或说孩子存储法)。

  孩子存储结构的结点设计如下:

  

  对于结点的设计,有两种方案可选:

  方案一:每个结点都设计对应的结构。

  如A结点有两个孩子,所以A的指针域就设计为两个;C结点只有一个孩子,所以指针域就设计为1个;以此类推。上图案例的树就可以设计为:

  

  缺点:这样设计有个很明显的缺点,如果有很多结点的结构都不一样,那么就要设计多种结点结构来保存结点信息,这样不利于统一管理。

   方案二:针对方案一的缺点,我们应该将树的结点结构统一(统一的关键在于指针域数量的统一),比如A有两个孩子,而C有一个孩子,那么我们需要使用一样的结构保存A和C就需要设计至少两个指针域的结点。依次类推,我们要使用一个全部结点都能使用的结构,那么我们就需要将指针域的数量设计为最多孩子的结点的度(孩子的数量)。即该树的度作为指针域的数量。所以我们将案例树的结点设计为2个指针域。

  

  缺点:这样设计可以统一管理结点,但是当结点之间的度相差太大时会是否浪费存储空间,比如A结点有1个孩子,B结点有10个孩子,那么按照10个指针域的设计,A结点就会浪费9个指针域。

   方案三(重要):换一种思维。

  首先树可以由两个关系表示出来:结点——孩子——兄弟。(双亲与孩子之间的关系;孩子与孩子之间的兄弟关系)。所以这两个关系都可以用“数据域——指针域”的设计来表示。并且每一个结点都可以这样来设计:结点——孩子——兄弟。

  例如A结点即它的孩子们就可以表示为:

  

  依次类推,我们可以将所有结点都用这种“双亲——孩子——兄弟”的结构表示出来。但这样如果有n个结点就会得到n个单链表,而这n个单链表之间都没有联系,所以我们按照还是按照从上到下、从左到右的顺序为各个结点排序,并用这些下标表示各个结点的位置,这样就可以将这些分散的单链表联系起来。如图:

  

  改进:该方案解决了结点结构不统一和空间浪费的问题,但任然可以进行改进,注意方案三的设计,我们将双亲和孩子串联起来,知道了某一结点,很容易就知道它的孩子,但如果要找到该结点的双亲需要进行遍历比较麻烦,而我们知道双亲是唯一的,所以我们可以将新增一个指针域指向双亲,所以改进结构就变成了:结点——双亲——孩子——兄弟。

  

  ps:其实孩子存储结构的实质就是图的邻接表存储结构(树是一种特殊的图)。

  3、孩子兄弟存储结构(重要)

  这种存储方式是将树转换为二叉树然后再进行孩子兄弟存储。孩子兄弟存储结构与二叉树的关系十分密切,使用也比较广泛,因为这种存储结构能够把树或森林转换为二叉树。

  (由于案例树已经是二叉树,这里我使用下图的树作为新的案例树):

  

树和森林转化为二叉树  

  1、普通树转化为二叉树

  将普通树转换为二叉树的规则

  1)将同一层的孩子结点用虚线串起来(如图1)

  2)将每个结点的分支(实线)除了最左边的第一条线全部剪掉(如图2)

  3)将虚线变为实线就形成了二叉树。(如图3)

  

  孩子兄弟存储结构其实是利用二叉树的规范的优点,先将树转化为二叉树再进行存储问题就变得简单多了。

  ps:我们将一棵以孩子兄弟存储后,只需要使用二叉树的各种遍历(先序、中序和后序)就可以还原普通的树,下文树和森林的遍历会讲到。

  2、森林转换为二叉树

   如果你懂了普通树转换为二叉树的方法,那么森林转化为二叉树也就变得十分简单了。

  注意普通树转换为的二叉树有一个特点:根结点没有右兄弟(这里说的右兄弟是根结点的孩子的兄弟)。而森林是由若干不相连的树组成的,所以我们先将森林里的没颗树转化为二叉树,然后利用“没有右兄弟”这个特性,第二颗树的根结点作为第一颗树的右兄弟,第三颗树作为第二颗树的右兄弟,依次类推。这样就将所有分散的二叉树连接成为一颗二叉树。

  将森林转换为二叉树的规则

  1)分别将森林里的树转化为二叉树(如图1)

  2)从左往右,右边的树的结点作为左边树结点的右兄弟连接起来即可。

  

树和森林的遍历

  我们常说的树的三种遍历方式,其实是二叉树的三种遍历方式:前序遍历、中序遍历和后序遍历。

    注意,普通树的遍历只有先序遍历和后序遍历两种(即普通树没有中序遍历);而森林的遍历方式只有先序遍历和中序遍历两种(即森林没有后序遍历)。不要混淆。由于树和森林都可以转换为二叉树来存储,所以我们一般只需要重点掌握二叉树的三种遍历方式就可以了。

  1、三者之间遍历的联系:

  1)树转化为二叉树,那么树的序遍历对应二叉树的序遍历;树的序遍历对应二叉树的序遍历。

  2)森林转化为二叉树,那么森林的序遍历对应二叉树的序遍历;树的序遍历对应二叉树的序遍历。

  2、遍历规则(注意,由于树的定义本身就存在递归,所以遍历是也存在递归)

    树的先序遍历:先访问根结点,再依次先序遍历根结点的每颗子树。

    树的后序遍历:先依次访问根结点的子树,再访问根结点。

    (森林是由树组成的,所以森林的遍历是在树的遍历继承之上的) 

    森林的先序遍历:先访问森林第一棵树的根结点,先序遍历第一颗树的子树,然后再先序遍历森林中除第一颗树外的其他树。(其实就是从左往右分别对每棵树进行先序遍历的效果)。

    森林的中序遍历:中序遍历第一颗树根结点的子树,访问第一颗树的根结点,然后再中序遍历森林中除第一颗树外的其他树。(其实就是从左往右分别对每棵树进行后序遍历的效果)。

  3、结论

  1、森林的遍历其实就是若干树从左往右进行(先序或后序)遍历的结果。(所以其实森林的中序遍历其实是树后序遍历的结果,我们称为中序遍历而不是后序遍历是因为,后序遍历遇到根结点表示遍历结束,而森林中的根结点不止一个,遇到根结点可能并没有结束,所以不宜称为后序遍历)。

  2、那为什么普通树没有中序遍历呢?因为普通树的孩子为若干个,不像二叉树刚好分为“左根右”,根结点正好符合中序遍历的“中间位置”,而普通树如果有中序遍历,那么这个根结点什么时候遍历(即放在哪个子树之间)算是中呢?所以,树没有中序遍历

posted @ 2018-01-10 13:14  风之之  阅读(974)  评论(3编辑  收藏  举报