本节课我们继续二叉树的话题。
二叉树的性质
这节课,先介绍一下二叉树的几个相关的性质,这些性质很简单,但却是我们进行数据结构的性能分析的基础。
1. 在二叉树的第 i 层上至多有
这个性质是显然的,第一层有1个,第二层最多只可能是第1层的两倍,第三层是第二层的两倍。
2. 深度为 k 的二叉树,最多有
这条性质由 1 可以直接得出:将每一层的最大结点数相加。这是一个公比为 2 的等比数列,其和为
再介绍一个定义,如果一个深度为 k 的二叉树,正好有
例如,下图就是一棵满二叉树:
而下面这个二叉树就不是满二叉树,却是一个完全二叉树:
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倍。好的数据结构和算法设计的威力可见一斑。
另外,完全二叉树的定义是很重要的,因为我们后面会学习一种威力十分强大的数据结构:堆,它就是一个完全二叉树。所以请记住完全二叉树。
二叉树的遍历
在上一节课的课外阅读里,我们详细地讨论了递归程序。并且使用递归实现了二叉树的前序遍历:
public void preOrder(Node<T> n) {
System.out.println(n.data);
if (n.left != null)
preOrder(n.left);
if (n.right != null)
preOrder(n.right);
}
这里,先介绍一下三种常用的遍历方式:
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 变量,来标记当前结点已经访问到哪一步。每次从子结点回溯回来的时候,都可以直接从当前结点里取出,这样就可以更加简化这个程序。接下来,给出非递归的中序遍历的代码。
public void midOrderWithoutRecurs() {
if (root == null)
return;
Stack<Node<T>> s = new Stack<>(64);
Node<T> current;
s.push(root);
while (! s.isEmpty()) {
current = s.getTop();
if (current.state == 0) {
if (current.left != null)
s.push(current.left);
current.state = 1;
}
else if (current.state == 1) {
System.out.println(current.data);
current.state = 2;
}
else if (current.state == 2) {
if (current.right != null)
s.push(current.right);
current.state = 3;
}
else if (current.state == 3) {
s.pop();
current.state = 0;
}
}
}
今天的课程就到这里了。今天的作业:
1. 补全BinarySearchTree的中序遍历, 前序遍历和后序遍历的实现。
2. 补全非递归的前序遍历的实现。
3. 树的遍历,还有一种办法,那就是按层遍历,例如,对于下面这棵树
按层遍历的结果,就是1, 2, 3, 4, 5, 6。请实现一下按层遍历。
上一节课:二叉树
下一节课:迭代器模式
目录:课程目录