# 第五章 树

> 译者：[biubiubiuboomboomboom](https://github.com/biubiubiuboomboomboom)

在本章中，我们将从接口到库的定义中休息一下，开始研究用于表示对象、表达式和其他层次结构的可搜索集合的基本数据结构工具之一——树。
术语的树指的是几种不同的变体，我们稍后将称之为连通的、非循环的、无向图。
不过，不过，现在我们先不这么叫它，而是集中研究两种有根的树。

定义： 一颗有序的树有如下特点：

	A、节点，它可能包含一段称为标签的数据。在应用程序上解除挂起状态时，节点可以代表任意数量的事物，并且它的数据标记可以任意详细说明。树的节点部分称为根节点或根。
	B、0棵或更多树的序列，其根节点称为根的子节点。树中的每个节点最多是一个节点的子节点——它是发散的。任何节点的子节点都是彼此的兄弟节点。

	一个节点的子节点数称为该节点的度。没有子节点的节点称为叶子(节点)、外部节点或终端节点;所有其他节点称为内部节点或非终端节点。

我们通常认为在每个节点及其子节点之间存在着称为边的连接，我们通常将沿着边从父节点到子节点或从子节点返回父节点的过程称之为遍历或者回溯。
从任意节点r开始，在以r为根的树中，有一个从r到任意节点n的唯一的、不重复的路径或边缘序列。
这条路径上的所有节点，包括r和n，都被称为r的后代和n的祖先。如果r的后代不是r本身，那么它就是正确的后代;类似地定义了正确的祖先。树中的任何节点都是该树的子树的根。
同样，树的正确子树是不等于(因此小于)树的子树。任何一组不相交的树(如扎根于所有结点上的树)都称为“森林”。

从节点n到树的根r的距离，即从n到r必须经过的边的数量，被称为是树中该节点的级别(或深度)。
树中所有节点的最大级别称为树的高度。
树中所有节点级别的和是树的路径长度。
我们还将内部(外部)路径长度定义为所有内部(外部)节点的级别之和。
图5.1说明了这些定义。
图中显示的所有级别都相对于节点0。“节点7在以节点1为根的树中的级别”也有意义，即2。
如果您仔细研究有序树的定义，您会发现它必须至少有一个节点，这样就不存在空的有序树。因此，具有k个>个子节点的子节点j的子节点j总是非空的。
更改定义以允许空树是很容易的:
	定义：位置树分为： 
	
		A、空的
		B、一个节点(通常标记)，对于每个非负整数j，一个位置树的第j个子节点
	
节点的度是非空子节点的个数。如果树中的所有节点都只在小于 k的位置上有子节点，我们说它是k-ary树。叶节点是那些没有非空子节点的节点;所有其他的都是内部节点。
也许最重要的位置树是二叉树，其中k= 2。对于二叉树，我们通常将子树0和子树1分别称为左子树和右子树。
一个完整的k-ary树是一个所有的内部节点，除了可能最右底部的节点都有递减。树是完整的，如果它是满的，并且它的所有叶子节点都是在从上到下、从左到右读取时最后出现的，如图5.2c所示。
完整的二叉树之所以有趣，是因为在某种意义上，它们最大程度上是“浓密的”;对于任意给定数量的内部节点，它们都最小化了树的内部路径长度，这很有趣，因为它与每次从树的内部节点移动到另一内部节点所需的总时间成正比。



## 5.1 表达式树

当试图表示递归定义的类型时，树通常很有趣。
一个熟悉的例子是表达式树,我们可以递归地将一个表达式定义为:
	1、有一个标识符或常量
	2、一个运算符(代表k个参数的某个函数)和k个表达式(它的操作数)
根据这个定义，表达式可以方便地由树表示，树的内部节点包含操作符，外部节点包含标识符或瞬间。图5.3展示了表达式x*(y + 3) - z
上图说明了如何处理树，以及表达式的求值。通常情况下，表达式所表示的值的定义与表达式的结构密切相关:
  1、常数的值就是它表示为数字的值。变量的值是其当前定义的值。
  2、由运算符和操作数表达式组成的表达式的值是将运算符应用于操作数表达式的值的结果
我们可以立即根据定义得到一个程序：

``` 
    /** The value currently denoted by the expression E (given
      *  current values of any variables).  Assumes E represents
      *  a valid expression tree, and that all variables
      *  contained in it have values. */
    static int eval(Tree E){
    if (E.isConstant ())
        return E.valueOf ();
    else if (E.isVar ())
        return currentValueOf (E.variableName ());
    else
        return perform (E.operator (),eval (E.left ()), eval (E.right ()));
        }
```

在这里，我们假设存在一个定义树，它提供了操作符，用于检测E是表示常量还是变量的叶子和提取存储在E的数据值、变量名或操作符名，以及用于查找E的左子节点和右子节点(用于内部节点)。我们还假设执行接受一个操作符名称(例如，“+”)和两个整数值，并对这些整数执行指定的计算。通过对树的结构进行归纳(在节点的子节点上扎根的树总是节点树的子树)，并观察表达式值的定义与程序之间的匹配，可以立即得出该程序的正确性。

## 5.2 基本树原语
可以在树上定义许多可能的操作集，就像序列一样。图5.4显示了一个可能的类(假设是整数标签)。通常，在给定的应用程序中只会实际提供一些显示的操作。对于二叉树，我们可以更加专门化，如图5.5所示。在实践中，我们通常不将binarytree定义为Tree的扩展，我在这里这样做只是为了说明。

到目前为止，所有操作都假定对树进行“根向下”处理，在这种处理中，有必要从父树继续到子树。当采用另一种方法更合适时，以下操作可以作为树的构造函数和子方法的补充(或替代)。
```
/** The parent of T, if any (otherwise null). */
public Tree<T> parent() ...
  /** Sets parent() to P. */
  public void setParent(Tree<T> P);
  /** A leaf node with label L and parent P */
  public Tree(T L, Tree<T> P);
  
  ```
## 5.3  表示树
 通常，我们对树的表示在很大程度上取决于对树的使用。
 
### 5.3.1  基于指针的二叉树
 对于在下面(§5.4)中描述的二叉树上执行遍历，递归定义的直接抄写通常是合适的，因此有：
 ```
    T L;  /* Data stored at node */
    BinaryTree<T> left,right; /* Left and right children */
  ```
  正如我所说的关于BinaryTree的示例定义，这种特殊的表示在实践中比简单地重用Tree的实现更为常见。当然，如果要支持父节点操作，我们可以添加一个额外的指针:
    `
    BinaryTree<T> parent; // or Tree , as appopriate
  `
  ### 5.3.2 基于指针的有序树
  用于BinaryTree的字段对于某些非二叉树也很有用，这得益于最左子树和右兄弟树的表示。假设我们表示一个有序树，其中每个内部节点可以有任意数量的子节点。我们可以将任何节点指向节点的子#0，并将右节点指向节点的下一个同级节点(如果有的话)，如图5.6所示
  举一个小例子。考虑计算节点包含整数的树中所有节点值的和的问题(我们将对其使用library类Integer，因为我们的标签必须是对象)。树中所有节点的和是根节点的值加上子节点的值之和。我们可以这样写:
  ```
  /** The sum of the values of all nodes of T, assuming T is anordered tree with no missing children. */
  static int treeSum(Tree<Integer> T){
  int S;
  S = T.label()；
  for (int i = 0; i < T.degree(); i += 1)
      S += treeSum(T.child(i));
  return S;
  }
  ```
  (java在这里默认地将Interger类型转换成int)
  一个有趣的发现是，这个程序的归纳证明没有包含明显的基本情况。上面的程序几乎是直接抄写“根值的和加上所有子节点值的和”。
  
  ### 5.3.3 叶子结点的表示
  对于父节点操作很重要而叶子节点操作不重要的应用程序，我们可以使用不同的表示形式。
  ```
  	T label;
	Tree<T> parent; /* Parent of current node */
  ```
  这样，就不能再对子节点有任何操作了
  这种表示法有一个相当有趣的优点:它占用更少的空间;每个节点少一个指针。仔细想想，这乍一看可能有点奇怪，因为每条边都需要一个指针。不同之处在于，“父”表示不需要所有外部节点中的空指针，只需要根节点中的空指针。我们将在后面的章节中看到这种表示的应用。
  ### 5.3.4 一颗完整的树的数组表示
  当树完成时，使用数组会有一个特别紧凑的表示。考虑图5.2c中的完整树。父节点的编号k > 0,图节点数量⌊(k−1) / 2⌋(或Java (k - 1) / 2或(k - 1) > > 1);左子节点k是2 k + 1和2 k + 2。我们从1节点编号而不是0,这些公式就更简单:⌊k / 2⌋父母,2 k的左子节点和2 k + 1的。因此，我们可以将这些完整的树表示为只包含标签信息的数组，并使用数组中的索引作为指针。父节点操作和子节点操作都变得简单。当然，必须小心维护完整性属性，否则数组中会出现间隙(实际上，对于某些不完整的树，可能需要一个包含2h - 1个元素的数组来表示具有h节点的树)。
	不幸的是，实现这种表示所需的头与上面的头稍有不同，因为以这种方式访问由数组表示的树的元素需要三个信息—数组、上界和索引—而不仅仅是一个指针。此外，我们可能还需要一些路由来为新树分配空间，预先指定树的大小。下面是一个示例，其中还提供了一些主体。
```
	/** A BinaryTree2<T> is an entire binary tree with labels of type T.
	The nodes in it are denoted by their depth-first number ina complete tree. */
	class BinaryTree2<T> {
	protected T[] label;
	protected int size;
	/** A new BinaryTree2 with room for N labels. */
	public BinaryTree2(int N) {
	label = (T[]) new Object[N]; 
	size = 0;
	}
	public int currentSize() { return size; }
	public int maxSize() { return label.length; }
	/** The label of node K in breadth-first order.
	*  Assumes 0 <= k < size. */
	public T label(int k) { return label[k]; }
	/** Cause label(K) to be VAL. */
	public void setLabel(int k, T val) { label[k] = val; }
	public int left(int k) { return 2*k+1; }
	public int right(int k) { return 2*k+2; }
	public int parent(int k) { return (k-1)/2; 
	/** Add one more node to the tree, the next in breadth-first
	*  order. Assumes currentSize() < maxSize(). */
	public void extend(T label) {
		 this.label[size] = label; 
		 size += 1;
		}
	}
```
稍后处理堆数据结构时，我们将看到这个数组表示。图5.2c中的二叉树表示如图5.7所示。
	
  ### 5.3.5 空树的替代表示
  
   在我们的表示中，空树往往具有特殊的状态。例如，我们可以构造一个用于访问树T的左节点的方法T.left()，但是如果T是一各空树，我们就不能编写一个方法，使T. isempty()为真。相反，我们必须写T == null。当然，原因是我们用null指针表示空树，并且没有在null指针的动态类型上定义实例方法(更具体地说，如果我们尝试，我们会得到一个NullPointer exception)。如果空树由普通对象指针表示，它就不需要特殊的状态 。
	例如，我们可以从§5.2开始扩展树的定义，如下所示：
```
		class Tree<T> {
			...
			public final Tree<T> EMPTY = new EmptyTree<T> ();
			/** True iff THIS is the empty tree. */
			public boolean isEmpty () { return false; }
			private static class EmptyTree<T> extends Tree<T> {
			/** The empty tree */
			private EmptyTree () { }
			public boolean isEmpty () { return true; }
			public int degree() { return 0; }
			public int numChildren() { return 0; }
			/** The kth child (always an error). */
			public Tree<T> child(int k) {
 				throw new IndexOutOfBoundsException ();
				}
			/** The label of THIS (always an error). */
			public T label () {throw new IllegalStateException ();}
			}
			}
```
  这里只有一个空树(因为EmptyTree类对tree类是私有的，这是单例设计模式的一个例子，所以可以保证它是空树)，但是这棵树是一个成熟的对象，我们不需要对null进行特殊测试来避免异常。我们将在树遍历的讨论中进一步扩展这种表示(参见§5.4.2)。
## 5.4  树的遍历
  §5.1中的函数eval遍历(或遍历)它的参数——也就是说，它处理树中的每个节点。遍历按处理树节点的顺序进行分类。在程序eval中，我们首先遍历(即然后对这些遍历的结果和节点中的其他数据执行一些处理。后一种处理通常称为访问节点。因此，eval的模式是“遍历节点的子节点，然后访问节点”，这是一种称为postorder的顺序。可以通过postorder遍历以反向波兰格式打印表达式树，其中访问节点意味着打印其内容(图5.3中的树将输出为“x y 3 + * z -”)。如果对每个节点的主处理(“visitation”)发生在子节点的主处理之前，给出“访问节点，然后遍历其子节点”的模式，我们就得到了所谓的preorder遍历。最后，图5.1和图5.2中的节点都按级别顺序或宽度优先顺序编号，其中在树的给定级别上的节点在访问下一个节点之前访问。
	到目前为止，所有遍历顺序对于我们考虑过的任何类型的树都是有意义的。还有一种标准遍历顺序只适用于二进制树:顺序遍历(或对称遍历)。这里的模式是“遍历节点的左子节点，访问节点，然后遍历右子节点”。例如，在表达式树的情况下，将按照中缀顺序再现所表示的表达式。事实上,这并不是很准确,因为正确表达式括号,精确操作必须像“编写一个左括号,然后遍历左子,然后写操作符,然后遍历右子,然后写一个右括号"这样。
	然而，尽管这样的例子至少导致了一次为遍历引入更通用表示法的尝试，但我们通常只是将它们近似地归类到上面描述的类别之一中，并就此打住。
	图5.8演示了几个按preorder、inorder和postorder顺序遍历二叉树的情况。
	
### 5.4.1 广度遍历
  我故意对“访问”的含义含糊其辞，因为树遍历是一个通用的概念，并不特定于树节点上的任何特定操作。事实上，我们可以编写一个遍历的通用定义，并将树在每个节点上“访问”的操作作为参数。在与Scheme类似的语言有类似的函数闭包，我们只需将访问参数设为函数参数即可。如：
```
	;; Visit the nodes of TREE, applying VISIT to each in inorder
	(define inorder-walk (tree visit)
	 (if (not (null? tree))
	  (begin (inorder-walk (left tree) visit)
		(visit tree)
		(inorder-walk (right tree) visit))))
```
例如，打印树的所有节点:
	(inorder-walk myTree (lambda (x) (display (label x)) (newline)))
	在Java中，我们使用的则是对象而不是函数(就像Java .util.Comparator 接口 §2.2.4)。例如，我们可以定义如下接口：
```
		public interface TreeVisitor<T> {void visit (Tree<T> node);}
		public interface BinaryTreeVisitor<T> {void visit (BinaryTree<T> node);}
		将上面的改写为
		static <T> BinaryTreeVisitor<T> inorderWalk (BinaryTree<T> tree,BinaryTreeVisitor<T> visitor){
		if (tree != null) {
		inorderWalk (tree.left (), visitor);
		visitor.visit (tree);
		inorderWalk (tree.right (), visitor);}
		return visitor;
		}
```
这里调用的方法是：
	`inorderWalk (myTree, new PrintNode ());`

我们可以再定义一个 BinaryTree<String>
```
	class PrintNode implements BinaryTreeVisitor<String> {
		public void visit (BinaryTree<String> node) {
			System.out.println (node.label ());}}
```
显然，PrintNode类也可以与其他类型的traverals一起使用。最后，我们可以让访问者匿名，就像在最初的Scheme程序中一样:
```	
	inorderWalk (myTree,new BinaryTreeVisitor<String> () {
		public void visit (BinaryTree<String> node) {
			System.out.println (node.label ());}});
```
像这样封装一个操作并将其放在集合中的其他方法中的做法，有一个简单的名字，叫Visitor模式。
通过向访问者添加状态，我们可以得到：
```
	/** A TreeVisitor that concatenates the labels of all nodes it*  visits. */
	public class ConcatNode implements BinaryTreeVisitor<String> {
	private StringBuffer result = new StringBuffer ();
	public void visit (BinaryTree<String> node) {
		if (result.length () > 0)result.append (", ");
		result.append (node.label ());}
	public String toString () { return result.toString (); }}
```
根据这个方法，我们可以按顺序打印树中以逗号分隔的项：
 ` System.out.println (inorderWalk (myTree, new ConcatNode()));`
(这个例子说明了为什么我让inorderWalk返回它的访问者参数。我建议你把这个例子的所有细节都练一遍。)


5.4.2  访问空树
我们将§5.4.1中的inorderWalk方法定义为一个静态(类)方法，而不是实例方法，这在一定程度上是为了清除对空树的处理。另一方面，如果我们使用§5.3.5的另一种空树表示，我们可以避免对空树进行特殊的大小写处理，并使遍历方法成为tree类的一部分。例如，有这么一个preorder-walk方法：
```
	class Tree<T> {
	···
	public TreeVisitor<T> preorderWalk (TreeVisitor<T> visitor) {
		visitor.visit (this);
		for (int i = 0; i < numChildren (); i += 1)
			child(i).preorderWalk (visitor);
			return visitor;}
		···
		private static class EmptyTree<T> extends Tree<T> {
		···
		public TreeVisitor<T> preorderWalk (TreeVisitor<T> visitor) {return visitor;}}}
```
在这里，您可以看到根本没有针对空树的显式调用;在调用preorderWalk的两个版本中，所有内容都是隐式的。
一种可能的方法是使用堆栈并简单地转换遍历的递归结构，就像我们在§4.4.1中展示的findExit过程那样。我们可能会得到如图5.9所示的BinaryTrees的迭代器。
另一种方法是使用带有父链接的树数据结构，如图5.10所示。如您所见，这个实现跟踪fieldnext中要访问的下一个节点(按postorder)。它通过查看父节点，并根据next是其父节点的左子节点还是右子节点来决定下一步要访问哪个节点。因为这个迭代器执行postorder遍历，如果next是一个右子节点，那么next之后的节点就是next的父节点，否则它就是父节点右子节点最深、最左的后代。

练习
5.1 实现一个迭代器，它按顺序枚举树节点的标签，使用一个堆栈，如图5.9所示
5.2 实现一个迭代器，使用父链接按顺序枚举树节点的标签，如图5.10所示
5.3 实现一个对一般类型树(而不是binarytree)进行操作的预置迭代器。

		
	
