树
一. 树的简介
树(Tree)是n(n>=0)个结点的优先级。n=0时成为空树。在任意一棵非空树中:(1)有且仅有一个特定的成为根(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互补相交的有限集T1、T2 ······Tm,其中每一个集合本身又是另一棵树,并且成为根的子树。
结点分类:
结点拥有的子树数成为结点的度(Degree)
度为0的结点称为叶结点(Leaf)或终端结点
度不为0的结点称为非终端结点或者分支结点
除了根结点以外,分支结点也称为内部结点
树的度是树内各结点的度的最大值
结点之间的关系
结点的子树的根成为结点的孩子(Child),相应的,改结点称为孩子的双拼(Parent)
同一个双亲的孩子之间胡成为兄弟(Sibling)
结点的祖先是从根结点到该结点所经分支上除了自己以外的所有的结点,相应的,这个结点也称为祖先结点的子孙
其他概念:
深度:树中结点的最大层次称为树的深度(Depth)或高度
如果将树中的结点的各子树看成从左至右是有次序的,不能互换的,则成该数为有序数,否则称为无序树
森林(Forest)是m(m>=0)棵树互不相交的树的集合
二. 树的抽象数据类型
Root 返回树的根结点
Depth 返回树的深度
IsEmpty 返回是否为空树
Value 返回结点的数值
Parent 返回结点的父结点
Child 返回结点的子结点
LeftChild 返回结点的左子结点
RightChild 返回结点的右子结点
RightSibling 返回结点的左兄弟结点
RightSibling 返回结点的右兄弟结点
InsertChild 插入子结点
DeleteChild 删除子结点
三. 树的存储结构
树是一种一对多的数据结构,而存储位置必然是线性的关系,无论是选择顺序存储还是链式存储,树结点的存储位置都无法直接反映逻辑关系。
不过充分利用顺序存储和链式存储结构的特点,完全可以实现对树的存储结构的表示。下面我们介绍三种不同的表示方法:双亲表示法、孩子表示法、孩子兄弟表示法。
四. 双亲表示法
在树形结构中,除了根结点以外,一个结点可能没有孩子但是一定有双亲。
我们假设有一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示双亲结点在数组中的位置。也就是说,每个结点除了知道自己是谁,还要知道自己的双亲在哪里。数据类型如下:
Data
int Parent
使用这样的存储结构,我们可以根据结点的parent指针找到它的双亲结点,所用的时间复杂度为o(1),直到parent为-1时,表示找到了树结点的根。但是如果想找一个结点的孩子是什么,就只能遍历整个结构才行,时间复杂度为o(n)。
能不能改进查找孩子结点的算法呢?当然可以,增加一个结点最左孩子的指针,这样就很容易找到结点A的第一个孩子,并往下遍历,直到有一个结点的parent指针保存的不是结点A,在此之间的结点都是结点A的孩子结点。如果一个结点的最左孩子指针为-1,说明他没有孩子结点。
另外一个场景,我们很关注各兄弟之间的关系,双亲表示法无法体现这样的关系,那怎么办呢?可以增加一个最右兄弟指针来体现兄弟关系。如果一个结点有右兄弟,那就记录最右的兄弟的下标,在此之间的结点都是该结点的兄弟结点。同样的,如果右兄弟不存在,则赋值为-1。
如果我们的关注点很多,对时间遍历要求比较高,我们还可以把此结构扩展位有双亲域、长子域等等。存储结构的设计是一个非常灵活的过程,一个存储结构设计的是否合理,取决于基于该存储结构的运算是否合适、是否方便,时间复杂度好不好等。
五. 孩子表示法
换一种完全不同的考虑方法。由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。不过,树的每个结点的度,也就是它的孩子个数是不同的,所以可以设计两种方案来解决。
-
方案一:
将指针域上的个数设置为树的度。树的度是树各个结点度的最大值,也就不存在指针域长度不够的情况了。
这种方法对于树中各结点的度相差很大时,显然是浪费了空间的,因为有很多结点,它的指针域都是空的。
-
方案二:
第二种方案每个结点指针域的个数等于该结点的度,我们专门取一个位置来存储指针域的个数。
这种方法克服了浪费空间的缺点,空间利用率是很高了,但是由于各结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的消耗。
能否有更好的方法,既可以减少空指针的浪费有能使结点结构相同。
仔细观察,我们为了要遍历正棵树,把每个结点放到一个顺序存储结构的数组中是合理的,单每个结点的孩子有多少是不确定的,所以我们再对每个结点的孩子简历一个单链表体现他们的关系。
这就是我们的孩子表示法,具体方法时,把每个结点的孩子结点排列起来,以单链表做存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。简单来说,孩子表示法就是把所有结点先存储在一个数组里,再给每个结点增加一个其孩子链表的数据项,如果是叶结点,就为空。
这样的结构对于我们要查找某个结点的某个孩子,只需要查找这个结点的孩子单链表即可。遍历正棵树也是很方便的,对头结点的数组循环即可。
但是,这也存在着问题,我如何知道某个结点的双亲是谁呢?比较麻烦,需要遍历正棵树才行,我们可以将双亲表示法和孩子表示法综合起来解决这个问题。结点数组中的对象增加一个父结点数据项即可。
六. 孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
这种表示方法,给查找某个结点的某个孩子带来了方便,只需要找到第一个孩子,然后通过第一个孩子的右兄弟向右查找。当然,如果要找某个结点的双亲,这个表示法也是有缺陷的,如果有这个需求,再加一个父结点就好了。
七. 二叉树的定义
有这样一个小游戏,我给定一个100以内的数字,给你7次机会去猜它的数值,我的回答只会告诉你是大了还是小了,怎么样能保证最大猜中率呢?我们把这个问题简化一下,猜数字被告知是大了还是小了,它的信息其实是每次猜测都可以过滤掉一部分错误答案,那么只要保证每次猜数字都能最大可能性的过滤掉更多的错误答案就可以了。
这就是很经典的折半查找算法,也叫做二分法。我们每次都猜测给定范围内的中间值,不管是大了还是小了都可以去掉一半的错误选项。
二叉树:是n个结点(n>=0)个结点的有限集合,该集合或者空集(称为空二叉树),或者由一个跟结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
八. 二叉树的特点
-
每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者只有一棵子树都是可以的。
-
左子树和右子树是有顺序的,次序不能任意颠倒。也就是说,要让左子树和右子树有一定的逻辑规律。
-
即使树中某结点只有一棵子树,也要区分它是左子树还是右子树,这是完全不同的两种树。
二叉树的吴总基本形态:
-
空二叉树
-
只有一个根结点
-
根结点只有左子树
-
根结点只有右子树
-
根结点既有左子树又有右子树
九. 特殊二叉树
-
斜树
斜树一定是斜的,但是往哪里斜还是有讲究。所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树
-
满二叉树
所有的分支结点都存在左子树和右子树,并且所有的叶子都在同一层上,这样的二叉树被成满二叉树
-
完全二叉树
从最右一个叶结点开始往前,如果每个结点的位置都跟满二叉树的位置相同,则被称为完全二叉树。从图形上来看,完全二叉树就是满二叉树的最后一层的末尾少了n(n>=0)个结点
完全二叉树的特点:
-
叶子结点只能出现在最下两层
-
最下层的叶子一定出现在左部连续位置
-
倒数二层,若有叶子结点,一定都在右部连续位置
-
如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况
-
同样结点树的二叉树,完全二叉树的深度最小
十. 二叉树
1.二叉树性质
性质1:在二叉树的第i层上至多有2n-1个结点
性质2:深度为k的二叉树至多有2k-1个结点
性质3:对任何一棵二叉树T,如果其终端结点树为n0,度为2的结点数为n2,则n0=n2+1
终端结点数其实就是叶子结点数,而一棵二叉树,除了叶子结点外,剩下的就是度为1或2的结点数了,我们设n1为度是1的结点数
性质4:具有n个结点的完全二叉树的深度为[log2n]+1
性质5:如果对一棵有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号(从第1层到第[log2n]+1层,每层从做到右),对任一结点i(1<=i<=n)有:
a)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]
b)如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
c)如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1