第一部分:定义
树(Tree)是n(n>=0)个结点的有限集。 n = 0 时成为空树。在任意一颗非空树中:
(1). 有且仅有一个特定的成为根(root)的节点。
(2). 当 n > 1 时,其余结点可以分为m个互不相交的有限集 T1、 T2、 T3 .....Tm,其中每一个集本身又是一棵树,并且称为根的子树。
第二部分:基本概念
1. 结点分类
在一棵树中,结点分为 根结点、内部结点和叶结点(又称终端结点)。
结点拥有的子树的数目称为结点的度(Degree)。 B的度为1, D的度为3
在一棵树中, 最大的结点的度为树的度。 由于D的度最多,所以树的度就是3.
2.节点间关系
其中B是A的孩子, A是B的双亲, C是B的兄弟。 之所以称之为双亲是因为对于结点来说父母同体,只有这么称呼是比较合适的了。
3. 其他相关概念
结点的层次: 从根开始算起, 根为第一层,然后第二层,第三层....
树的深度或高度: 结点的最大层次就是树的深度或高度。
有序树:如果将树中结点的各子树看成是从左到右有次序的,不能互换的,那么该树就是有序树,否则就是无序树。
第三部分: 树的存储结构
我们知道, 树中某个结点的孩子有多个,所以无论是使用顺序存储结构还是使用链式存储结构,都不能很好的表示树。但是我们可以利用顺序存储结构和链式犓结构的特点来实现之。
一. 双亲表示法
我们假设以一组连续的空间存储树的结构,同时在每个节点中,附加一个指示器指示其双亲结点在数组中的位置,也就是说,每个结点不仅仅知道自己是谁意外,还知道它的双亲在数组中的哪个位置。
即一个结点包含两个部分: 1. data(数据域) --- 用于存储结点的数据信息。 2. parent (指针域) --- 用于存储该结点的双亲在数组中的下标。
注意: 这样的好处是我们可以根据结点的parent指针很容易的找出它的双亲结点,所以复杂度是O(1), 但是如果我们要知道结点的孩子是谁,就得遍历整个结构才行。为了解决这个问题,我们可以增加一个结点最左边孩子的域,可以称之为长子域。 孩子可以找到了, 但是找兄弟呢 ? 我们还可以增加一个兄弟域,这样,可以发现:
存储结构的设计是一个非常灵活的过程,一个存储结构设计的是否合理,取决于该存储结构的运算是否合适、是否方便, 时间复杂度好不好等等。
二. 孩子表示法
如果一棵树中的每一个结点都有很多的孩子, 那么这时孩子为重, 我们就可以考虑每个结点有多个指针域, 每个指针域指向一颗子树的根节点, 这种方法就是多重链表表示法。 不难想象,多重链表表示法的最终结构和一颗树的结构是无异的。但是根据一个结点需要多少个域的不同,我们可以将多重链表表示法分为下面两种:
1. 每个结点的指针域的个数是数的度
优点 这样做的好处在于树的结构是稳定的、整齐的、维护起来较为方便。 缺点 坏处在于有可能很多指针域都是空着的,造成了资源的浪费。
2. 每个结点的指针域的个数就是其子节点的个数(即它的度)
优点 这样做的好处在于不会造成资源的浪费。 缺点 坏处在于这样在后期维护起来会比较麻烦,且结构会比较混乱。
ok! 那有没有一种方法可以回避上面的缺点呢? 当然有,这就是孩子表示法了, 孩子表示法,即以n个结点的单链表作为存储结构, 则n个结点就有n个孩子链表,每个结点又会引出孩子的节点(如果没有孩子,就不会引出,如果有,就一直引下去)。
这样的表示法缺点在于,如果知道某个结点的双亲是谁呢? 并不难,只要我们把双亲表示法和孩子表示法综合一下不就行了吗? 这种思路当然可行。
三、 孩子兄弟表示法
第四部分:二叉树
二叉树(Binary Tree)是n个结点的有限集合,该集合或者为空集,或者由一个根节点和两颗互不相交的、分别称为根节点的左子树和右子树的二叉树组成。
1. 二叉树的特点
- 每个结点最多有两个子树。
- 左子树和右子树的顺组是不能随意颠倒的。
- 即使某个结点只有一个子树,也要分清楚左子树还是右子树。
- 三个结点可以构成 5 种不同的二叉树。
2. 一些特殊的二叉树
- 斜树 如上图中的第二个图和最后一个图就是斜树 --- 每一层都只有一个结点,结点的个数和二叉树的深度相同。
- 满二叉树 在一颗二叉树中,如果所有分支结点都存在左子树和右子树,并且所有的叶子都在同一层上, 这样的二叉树就称为满二叉树。
- 完全二叉树 对于一颗具有n个结点的二叉树按层序编号,如果编号为i (1 <= i <= n)的结点与同样深度的满二叉树中编号为i的结点在树中的位置完全相同,则这棵树就是完全二叉树。 编号方式: 从上到下,从左到右。
- 注意: 满二叉树一定是完全二叉树, 完全二叉树不一定是满二叉树。
3. 二叉树的5条性质
性质一: 在二叉树的第i层上最多有2i-1 个结点。(i>=1). 如二叉树的第三层上最多有2 的 2 次方个结点。
性质二: 在深度(或高度)为k的二叉树上最多有 2k - 1 个结点。 如深度为3的二叉树的结点数最多为 2 3 - 1 = 7个结点。
性质三: 对于任意一颗二叉树T, 如果其终端的节点数为n0 ,度为2的节点数为n2, 则有n0= n2 + 1;
性质四: 具有n个结点的完全二叉树的深度为[log2n] + 1 (其中[x]表示不大于x的最大整数)。
性质五:
如果对一颗有n个结点的完全二叉树(其深度为[log2n] + 1)的结点按层序编号(从上到下,从左到右), 对任一结点i (1 <= i <= n)都有:
1. 如果 i = 1, 那么结点i是二叉树的根。 如果i > 1, 那么双亲是结点[i/2]。
2. 如果 2i>n, 则结点 i 无左孩子,否则其做孩子是2i。
3. 如果2i + 1 > n, 则结点i无右孩子, 否则其右孩子是结点 2i + 1;
第五部分:遍历二叉树
二叉树的遍历 ( traversing binary tree )是指从根节点出发, 按照某种次序依次访问二叉树中的所有结点,使得每个结点都被访问一次且仅被访问一次。
这里的两个关键词是 访问 和 次序。
显然,二叉树的访问次序不同于线性结构,对于线性结构而言,最多也就是从头到尾、循环、双向等简单的遍历方式。树的结点之间不存在唯一的前驱和后继,所以在访问了一个结点之后,下一个被访问的结点面临着不同的选择。因而,对于二叉树而言,遍历结点的方式就完全不同了。
说明:一般我们的习惯是从左到右的,这里同样也做出这样的限制。那么遍历时,前中后是指根节点被访问的时间上的前、中、后。
如前序遍历时先访问根节点、 中序历时中间的时候访问根节点、后续遍历是最后访问根节点。 不论是哪种遍历方式都是先左子树、后右子树。
一: 前序遍历
规则: 若二叉树为空, 则空操作返回,否则先访问根结点,然后前序遍历左子树,在前序遍历右子树。
在二叉树中,先根后左再右。巧记:根左右。
二: 中序遍历
规则: 若树为空, 则空操作返回, 否则先从根节点开始(注意并不是先访问根节点),中序遍历根节点的左子树,然后是访问根节点,最后中序遍历右子树。
在二叉树中,先左后根再右。巧记:左根右。
注意: 对称遍历就是中序遍历。
三: 后续遍历
规则: 若树为空,则空操作返回,否则从左到右先叶子结点的方式遍历访问左子树,最后是访问根节点。
在二叉树中,先左后右再根。巧记:左右根。
四:层序遍历
规则: 若树为空,则空操作返回,否则从树的第一层开始,也就是根节点开始访问,从上而下逐层遍历,在同一层中,按照从左到右的顺序对结点逐个访问。
这个是最为简单的,只要遵循从上到下、从左到右的规则即可。
规则: 1. 这里的空操作返回,就是指向上返回,且空操作返回是共同点。 2. 这里的左子树、右子树和根节点都是相对的,环境和事件稍有改变,这些名词所代表的实体就发生了彻底的变化。