1. 在二叉树的第 i 层上至多有 [公式] 个结点
第一层有1个,第二层最多只可能是第1层的两倍,第三层是第二层的两倍。
2. 深度为 k 的二叉树,最多有 [公式] 个结点。
这条性质由 1 可以直接得出:将每一层的最大结点数相加。这是一个公比为 2 的等比数列,其和为 [公式]。
另一个定义,如果一个深度为 k 的二叉树,正好有 [公式] 个结点,那么这棵树就被称为满二叉树。如果一个二叉树,只有当第 k 层已经达到了 [公式] 的最大值以后,才能往第 k + 1 层自左向右地添加结点,这棵树就被称为完全二叉树。
3. 具有 n 个结点的完全二叉树的深度为 [公式]
这个性质就是性质二的直接推论。这个性质向我们揭示了一个问题:受二叉树的树形的影响,同样有n个结点的一个二叉树,它的高度可能差别很大。比如,我们上节课的作业第二题,如果二叉搜索树是以(1, 2, 3, 4, 5, 6)或者(6, 5, 4, 3, 2, 1)的顺序插入的话,二叉树的高度就是6,退化为链表。上节课的第一题,我们看到了,二叉树中的搜索效率与树的高度成正比。二叉树越矮,我们就能越快地找到目标,二叉树越高,要经过的结点就会越多。
当树形为完全二叉树时,在树中进行查找的时间复杂度是 O(log n),而当树形退化为链表时,查找的时间复杂度是 O(n),这个时间复杂度的差别是十分巨大的,大家可以算一下,如果有1000个结点,O(n)的时间复杂度,意味着1000次比较,而O(log n)的时间复杂度,只有10次比较,性能可以提升100倍。好的数据结构和算法设计的威力可见一斑。
另外,完全二叉树的定义是很重要的,因为我们后面会学习一种威力十分强大的数据结构:堆,它就是一个完全二叉树。所以请记住完全二叉树。
二叉树的遍历
并且使用递归实现了二叉树的前序遍历
这里,先介绍一下三种常用的遍历方式:
1. 前序遍历。先访问根结点,再前序遍历左子树,最后前序遍历右子树。可见,这个操作的定义就是递归的。
2. 中序遍历。先中序遍历左子树,再访问根结点,最后中序遍历右子树。由于左子树上的值都比根结点小,右子树上的值都比根结点大,所以,中序遍历一棵树所得到的结果,是从小到大有序的,可以根据这个特点,来检验你的中序遍历是否正确实现了。
3. 后序遍历。先后序遍历左子树,再后序遍历右子树,最后访问根结点。
定义非常简单,前,中,后,无非就是说的根结点在什么时机被访问而已。还有其他的遍历方式,但这三种是我们最常用的,希望大家彻底理解它。我这里再给出中序遍历的代码,大家自己完成后序的:
public void midOrder(Node n) {
if (n.left != null)
midOrder(n.left);
System.out.println(n.data);
if (n.right != null)
midOrder(n.right);
}
非递归遍历二叉树
在很多笔试题中,二叉树的非递归遍历是一个很常见的考题。非递归遍历有很多种实现方式,掌握起来要耗费很大的精力,而且还容易忘记。这一节将会介绍一种模拟递归函数的函数栈,从而实现非递归遍历。
如果将未访问完的结点入栈,每次只对栈顶元素进行操作,一旦栈顶元素被全部处理完,就将其出栈。继续取栈顶元素进行处理,这就相当于回溯到了父结点。但是每次子结点全部访问完回溯到父结点时,都需要知道父结点已经执行到哪一步了,在递归程序中,这个值是放在子结点所对应栈的 old EIP 中的,因此,这是我们要模拟的一个值。由于不需要像真正的递归程序那样在回溯时恢复调用者的函数栈,所以 old EBP 和 old ESP 这两个值是,经过分析,我们可以给结点的定义加上一个变量state来模拟old EIP:
class Node<T> {
public T data;
public Node left;
public Node right;
public int state;
public Node(T d) {
this.data = d;
}
}
为结点增加 state 变量,来标记当前结点已经访问到哪一步。每次从子结点回溯回来的时候,都可以直接从当前结点里取出,这样就可以更加简化这个程序。给出非递归的中序遍历的代码