今天学习到的有关二叉树的知识
树是由节点和叶子组成的。其组织方式是从根向外层结点(被称作树的叶子)扩展。
二叉树:是一种特定的树,在这类树中,每个双亲的孩子数不超过两个(图11.6)。
这些“二叉树”具有统一的结构,有多种扫描算法可供使用并可提供对元素的高效访问。在二叉树中,每个节点有0个,1个或2个孩子。我们将左边的结点称作“左孩子”,右边的结点称为“右孩子”。标记左或右是我们对树形象化的表示。二叉树是一种递归结构。每个结点都是其子树的根并有孩子,这些孩子分别被称为结点的左子树和右子树的根。树访问历程很自然地就是一种递归过程。以下是二叉树的递归定义。
二叉树是满足下列条件的结点B的集合。
(a) 如果结点集为空,B是一棵树。(空树亦是树。)
(b) B可划分为3个独立的子集(图1)。
R |
L1,L2……,Ln |
R1,R2……,Rn |
根结点 |
R的左子树 |
R的右子树 |
图1 |
在任一层次n,二叉树可能包含1到2n个结点。每一层的结点数决定了树的密度。直观上说,密度是相对于树的深度而言对树的大小(结点数)的一种度量。图11.6中,树A包含了深度为3到8个结点。而树B包含了深度诶4的5个结点。后一种情况是特殊形式,其树被称为“退化”树,其中只有一个叶子结点(E),每个非叶子结点只有1个孩子。一个退化树等价于一个链表。
密度较大的树在数据结构上很重要,因为它们在接近树的顶部离根较近的地方集中了较大比例的元素数目。一棵致密的树使得我们可以存储大量数据并对这些项进行有效访问。快速访问是用树来存放数据的关键。
退化树是密度度量中的极端 情况。另一种极端情况是深度为N的“完全二叉树”,其中从1到N-1各层的结点都是满的,而第N层的所有叶子结点占据树的最左边的位置。第N层包含2n个结点的完全二叉树是一棵“满二叉树”,满二叉树是其每个非叶子结点都有两个孩子的二叉树。图11.7示意了完全二叉树和满二叉树。
深度为k的完全二叉树和满二叉树具有一些有趣的数学特性。这两种树在第0层(根)都只有1(20)个结点;在第1层有2(21)个结点;在第2层有4(22)个结点,依此类推。一直到k-1层,总共有2k-1个结点。
1+2+4+…2k-1=2k-1
在第k层,结点数在最小值1到最大值2k(满)之间。对于满树。结点数为:1+2+4+…2k-1=2k-1
完全二叉树中的结点数N满足不等式:
2k≤N≤2k+1-1<2k+1
对k求解,我们得到:
k≤log2(N)<k+1
例如,深度为3的满二叉树有24-1=15个结点。
递归树的遍历
二叉树的递归定义规定二叉树的结构是由根和左、右子树组成,其中左右子树分别由根的左右指针域所定义。递归的威力在遍历方法上可以清楚地体现出来。每种树遍历算法都对一个结点实施3个动作:访问结点(N)、递归遍历左子树(L)以及右子树(R)。转到子树以后,算法确定其结点并实施同样的3个动作。遇到空树(指针==NULL)时遍历终止。各种递归算法的不同之处在于它们对结点实施动作的次序。在我们编制的中序和后序方法中 先走左子树再走右子树。
中序遍历 中序扫描中对一个结点实施的第1个动作是转到左子树以便扫描该子树中的结点。在递归遍历完左子树后,中序遍历对结点实施第2个动作,对结点的数据值进行处理。遍历对结点完成的最后一个动作是递归扫描右子树。在递归扫描中,对每个新结点重复同样的动作。
中序遍历的操作次序如下:
1. 遍历左子树
2. 访问结点
3. 遍历右子树
这种遍历方法被称作LNR(左、结点、右)。对于函数MakeCharTree中的树Tree_0,假设“visit”输出结点中的数据域。
中序遍历Tree_0时要执行的操作如下:
动作 | 打印 | 观察 |
从A到B,转到左子树; |
| B的左孩子为NULL |
访问B; | B |
|
从B到D,转到右子树; |
| D是叶子结点 |
访问D; | D | A的左子树访问完毕 |
访问根A | A |
|
从A到C,转到右子树; |
| E是叶子结点 |
从C到E,转到左子树; |
|
|
访问E; | E |
|
访问C; | C | 完毕! |
结点的遍历次序是BDAEC。递归函数先递归访问结点的左子树[dàLeft],然后访问结点,最后再递归访问右子树[tàRight()]。
后续扫描 后序扫描将对结点的访问推迟到递归访问左子树和右子树以后。这种操作次序被称为LRN扫描(左、右、结点)。
1. 遍历左子树
2. 遍历右子树
3. 访问结点
后序扫描树Tree_0时,结点的访问次序是DBECA。
动作 | 打印 | 观察 |
从A到B,转到左子树; |
| B的左孩子为NULL |
从B到D,转到右子树; |
| D是叶子结点 |
访问D; | D | B的孩子访问完毕 |
访问B; | B | A的左子树访问完毕 |
从A到C,转到右子树; |
|
|
从C到E,转到左子树; |
| E是叶子结点 |
访问E; | E | C的左孩子访问完毕 |
访问C; | C | A的右孩子访问完毕 |
访问根A; | A | 完毕 |
前序扫描则规定先访问结点,然后再扫描左右分支(NLR)。
很显然,前缀词“前”、“中”、“后”分别表示对结点的访问发生在何时。但在上述3种情况下对左子树的访问总是先于右子树。实际上在这3种算法之外也有先访问右子树再访问左子树的算法。我们可以用RNL扫描打印一棵树。树遍历算法允许我们访问树中所有结点。它们提供与顺序扫描数组或链表等价的算法。