# 第六章 搜索树  Search Trees

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

有关树的一个重要应用就是搜索。这项任务是确定数据结构中是否存在目标值，这些目标值表示一组数据，搜索结果可能返回与该值相关的一些辅助信息。在所有这些搜索中，我们都会执行多个步骤，直到我们找到了要查询的值，或者尝试了所有可能性。在每一步中，我们都会为下一步考虑从而消除其中的一些部分。在线性搜索(linear search)的情况下（见§1.3.1），我们在每个步骤中删除一个数据单元。在二分法搜索(binary search)的情况下（见第1.3.4节），在每个步骤中删除剩余数据的一半。

二分法搜索的问题是，待搜索项集不易更改；除非新添加数据项大于所有现有数据，否则添加新数据项需要移动原待搜索集的某些部分，以便为新项腾出合适的空间。最坏情况下，此操作的成本会随着集合的大小成比例地增加。这将转化为一个插入数据的问题，但是找到一组数据的中间值这一二分法的关键操作的花销将会变大。

对于树结构。假设我们有一组数据值，我们可以从每个数据值中提取一个键(key)，并且键集可能是完全有序的，也就是说，我们总是可以说一个键小于、大于或等于另一个键。键值的意义完全取决于数据的种类，但表达方式应做好标注。我们可以通过让这些数据值作为二叉搜索树（BST）的标签来近似进行二分搜索，二叉搜索树具有如下性质：

	BST性质：对于树的每个节点x，x的左子树中的所有节点的键都小于或等于x的键，x的右子树中的所有节点的键都大于或等于x的键。

图6.1a是典型BST的示例。在该示例中，标签是整数，键与标签相同，关于“小于”、“大于”和“等于”的表达为其通常的含义。  

![6.1](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/6.1.png)

键不必是整数。通常，我们可以对key使用任何一种全序关系(total order)来把一组数据添加到BST中，我们用符号\leq表述全序关系，全序关系具有以下属性：

* 完备性： 对于任何 x 和 y 要么满足 x ≤ y 要么满足 y ≤ x 或者二者同时都满足
* 传递性： 如果 x ≤ y 而且 y ≤ z 那么 x ≤ z
* 非对称性 如果 x ≤ y 并且 y ≤ x 那么 x = y  

例如，键可以是整数，而且不仅可以是整数，可以有它们通常的含义。或者数据和键可以是字符串，其顺序是字典排序。或者，数据可以是（pairs）一对（A，B），键可以是对的第一项。字典排序就像是按定义的单词排序，而不考虑它们的含义。最后一个排序是一个例子，在这个例子中，人们可能期望在搜索树中有几个具有相同键的不同项。

从定义看起BST的一个重要特性是，通过BST可以按标签的升序访问其节点，这就产生了一种简单的排序算法，称为树排序“tree sort”。

```java
/** Permute the elements of A into non-decreasing order. Assumes
* the elements of A have an order on them. */
static void sort(SomeType[] A) {
	int i;
	BST T;
	T = null;
	for (i = 0; i < A.length; i += 1) {
		insert A[i] into search tree T.
	}

	i = 0;
	traverse T in inorder, where visiting a node, Q, means
	A[i] = Q.label(); i += 1;
}
```

*someType类型的数据，根据BST定义的要求，用来表示具有小于和等于运算性质的数据。*

## 6.1 BST的应用   Operations on a BST

由于BST是二叉树，我们可以使用5.2节中的表示来给图6.2这个类。现在我将使用int类型作为标签，并假定标签与键相同。

由于在这个特殊的BST中一个标签可能对应多个实例，因此我必须仔细指定删除该标签或查找包含该标签的节点的含义。我在这里选择了包含标签的“最高”节点--最接近根的节点。
为什么这总是独一无二的？也就是说，为什么不能有两个包含标签的最高节点，同样接近于根节点？

这个特定的BST定义的一个问题特征是，数据结构相对来说是不受保护的。
正如insert上的注释所表明的那样，当t.label（）为20时，可以通过向其中一个子元素中插入错误的内容来“破坏”bst（如bst.insert（t.left（），42）。
当我们把这种表示应用于6.2节的SortedSet中时，我们会避免此类错误引用。

```java
/** A binary search tree. */
class BST {

	protected int label;
	protected BST left, right;

	/** A leaf node with given LABEL */
	public BST(int label) {
	 this(label, null, null);
	  }
	  /** Fetch the label of this node. */
	  public int label();
	  /** Fetch the left (right) child of this. */
	  public BST left() ...public BST right() ...
	  /** The highest node in T that contains the
	  * label L, or null if there is none. */
	  public static BST find(BST T, int L) ...
	  /** True iff label L is in T. */
	  public static boolean isIn(BST T, int L){
	   return find (T, L) != null; 
	}
	/** Insert the label L into T, returning the modified tree.
	* The nodes of the original tree may be modified. If
	* T is a subtree of a larger BST, T’, then insertion into
	* T will render T’ invalid due to violation of the binary-
	* search-tree property if L > T’.label() and T is in
	* T’.left() or L < T’.label() and T is in T’.right(). 
	*/
	public static BST insert(BST T, int L) ...
	/** Delete the instance of label L from T that is closest to
	* to the root and return the modified tree. The nodes of
	* the original tree may be modified. */
	public static BST remove(BST T, int L) ...
	/* This constructor is private to force all BST creation
	* to be done by the insert method. */
	private BST(int label, BST left, BST right) {
		this.label = label; this.left = left; this.right = right;
	}
}
```
*6.2: A BST representation*


### 6.1.1 搜索 BST   Searching a BST

搜索BST与数组中进行二分搜索非常相似，树的根对应于数组的中间值。

```java
/** The highest node in T that contains the
* label L, or null if there is none. */
public static BST find(BST T, int L){
	if (T == null || L == T.label)return T;
	else if (L < T.label)
		return find(T.left, L);
	else return find(T.right, L);
}
```

### 6.1.2 插入 BST  Inserting into a BST

正如之前所说，使用一棵树的好处是向它添加数据时花销相对较小，如下面的例行程序。



```java
/** Insert the label L into T, returning the modified tree.
* The nodes of the original tree may be modified.... */
static BST insert(BST T, int L){
	if (T == null)
		return new BST (L, null, null);
	if (L < T.label)
		T.left = insert(T.left, L);
	else
		T.right = insert(T.right, L);
	return T;}
```

*因为我写这个的特殊方式，当我在树中插入同一个值的多个副本时，它们总是在所有现有副本的右边。我将在删除操作中保留此属性。*

### 6.1.3 删除 BST 中的数据单元  Deleting items from a BST

删除要复杂得多，因为当你删除一个内部节点时，你不能让它的子节点失效，而是必须在树的某个地方重新连接它们。
*显然，删除一个外部节点很容易；只需用空树替换它(见图6.3(a))。
*删除一个只有一个叶子节点的内部节点也很容易，只需要让子节点继承父节点并且向上移动即可。（图6.3(b)）
*当两个叶子节点都不为空时，我们要先序遍历他的右子树，使第一个节点也就是key最小的节点来替代要删除的节点
  此外，由于它是第一个按顺序排列的节点，其左子节点将为空。因此，我们可以用其正确的子节点替换该节点，并将
  其key移动到要删除的节点，如图6.3（c）所示


![6.3](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/6.3.png)

三个可能的删除，每个从图6.1中的树开始

下面为从BST中删除数据的一个例子，辅助函数swapsmallest()是一个附加的私有方法，定义如下：

```java
/** Delete the instance of label L from T that is closest to
* to the root and return the modified tree. The nodes of
* the original tree may be modified. */
public static BST remove(BST T, int L) {
	if (T == null)
		return null;
	if (L < T.label)
		T.left = remove(T.left, L);
	else if (L > T.label)
		T.right = remove(T.right, L);// Otherwise, we’ve found L
	else if (T.left == null)
		return T.right;
	else if (T.right == null)
		return T.left;
	else
		T.right = swapSmallest(T.right, T);
		return T;
	}

	/** Move the label from the first node in T (in an inorder
	* traversal) to node R (over-writing the current label of R),
	* remove the first node of T from T, and return the resulting tree.*/
	private static BST swapSmallest(BST T, BST R) {
		if (T.left == null) {
			R.label = T.label;
			return T.right;
		} 
		else {
			T.left = swapSmallest(T.left, R);
			return T;
		}
	}
```
*Removing items from a BST without parent pointers  从没有父指针的BST中删除项*

### 6.1.4 带有父指针的操作  Operations with parent pointers

如果我们修改BST类以提供父操作，并在表示中添加相应的父参数，那么操作将变得更复杂，但提供了更多的灵活性。
但不为bst提供setParent操作可能是明智的，因为使用此操作很容易破坏二进制搜索树属性。
而且因为存在插入和删除的操作，使用BST时或许根本不需要他。


因为查询操作忽略父节点所以不会受到影响，另一方面，由于在BST插入时必须设置任何一个插入节点的父节点，因此
情况变得复杂，下面展示了一种方法。当然，和之前一样，从带有父指针的BST中删除数据是最棘手的。

```java
static BST insert(BST T, int L) {
	BST newNode;
	if (T == null)
		return new BST (L, null, null);
	if (L < T.label)
		T.left = newNode = insert(T.left, L);
		elseT.right = newNode = insert(T.right, L);
		newNode.parent = T;
		return T;
	}
```
*Insertion into a BST that has parent pointers (带有父指针BST的插入操作)*


```java
/** Delete the instance of label L from T that is closest to
* to the root and return the modified tree. The nodes of
* the original tree may be modified. */
public static BST remove(BST T, int L) {
	if (T == null)
		return null;
		BST newChild;
		newChild = null;
		 result = T;
	if (L < T.label)
		T.left = newChild = remove(T.left, L);
	else if (L > T.label)
		T.right = newChild = remove(T.right, L);
	// Otherwise, we’ve found L
   else if (T.left == null)
   		return T.right;
   else if (T.right == null)
		return T.left;
   else
   	T.right = newChild = swapSmallest(T.right, T);
   if (newChild != null)
    	newChild.parent = T;
        return T;
    }
    private static BST swapSmallest(BST T, BST R) {
    	if (T.left == null) {
    		R.label = T.label;
    		return T.right;
    	} else {
    		T.left = swapSmallest(T.left, R);
    		if (T.left != null)
    			T.left.parent = T;
    		return T;
    	}
    }
```
*Removing items from a BST with parent pointers  带有父指针的删除操作*


### 6.1.5  简并性问题 Degeneracy strikes

不幸的是，事情不可能一帆风顺。
图6.1（b）中的树是按升序将节点插入到树中的结果（显然，同样的树可以从更大的树中进行适当的删除操作得到）
您可以看到在树上执行搜索或插入就像在链接列表上执行搜索或插入。它像一个链接列表，但在每个元素中都有多余的指针，这些指针始终为空
这棵树是不平衡的：它包含子树，在其中，左、右子树有不同的高度。在进行进一步学习后，我们将在第9章中回到这个问题

![6.1](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/6.1.png)


## 6.2 实现SortedSet接口   Implementing the SortedSet interface

标准Java库接口SoretSeT（参见第2.2.4）提供了一种支持范围查询的集合。也就是说，根据某种排序关系，程序可以使用接口查找集合中某个值范围内的所有项。搜索单个特定值只是一种特殊情况，其中范围只包含一个值。使用二进制搜索树作为表示来实现这个接口是相当容易的；我们将把结果称为BSTSet

让我们提前计划一下，在这些操作中，我们必须支持headSet()、 tailSet()和 subSet()，
它们返回与该集合的子范围相关的一些底层集合的视图。
返回的值本身就是完整的排序集，也应该修改底层集。由于一个完整的集合也可以被看作是一个范围的视图，在这个范围内，边界是“非常小”到“非常大”，我们可能会寻找一个支持两个集合的表示，这两个集合都是由一个构造器创建的，而那些是其他集合的视图。这表示我们集合的一个表示，其中包含指向BST根的指针，两个边界表示集合中最大和最小的成员，空值表示缺少的边界。

因为一个重要的原因，我们将BST的根作为一个（永久的）哨兵节点。
我们将对集合的所有视图使用相同的树。如果我们表示意味着指向包含数据的树的根，那么每当删除树的节点时，该指针就必须更改。
但是，我们还必须确保更新集合中所有其他视图中的根指针，因为它们也应该反映集合中的更改。
通过引入由所有视图共享且从未删除的哨兵节点，我们便解决了保持所有视图都是最新的问题。
这是旧计算机科学格言的一个典型例子：大多数技术问题都可以通过引入另一个间接层次来解决。
*Most technical problems can be solved by introducing another level of indirection*

假设我们使用父指针，一个迭代器通过一个集合可以包含一个指针，指向下一个要返回标签的节点，一个指向最后一个节点的指针，
它的标签被返回（用于IMP删除），并且一个指向BSTSET的指针被迭代（在Java中通过使迭代器成为一个内部类方便地提供）。
迭代器将按顺序进行，跳过树中超出集合边界的部分。另请参见练习5.2关于使用父指针迭代的内容

下例说明了一个BSTSet，它显示了表示的主要元素：原始集合、包含其数据的BST、同一集合的视图以及该视图上的迭代器。这些集合都包含用于比较器的空间（见§2.2.4），以允许集合的用户指定一个顺序；在下例中，我们使用自然顺序(natural ordering)，在字符串上，自然顺序给出了词典编纂顺序。图6.7包含表示的相应Java声明的草图。

```java
public class BSTSet<T> extends AbstractSet<T> {
	/** The empty set, using COMP as the ordering. */
	public BSTSet (Comparator<T> comp) {
		comparator = comp;
		low = high = null;
		sent = new BST ();
	}/** The empty set, using natural ordering. */
	public BSTSet () {
	 this (null); 
	}
	/** The set initialized to the contents of C, with natural order. */
	public BSTSet (Collection<? extends T> c) {
	 addAll (c); 
	}
	/** The set initialized to the contents of S, same ordering. */
	public BSTSet (SortedSet<? extends T> s) {
		this (s.comparator()); addAll (c);
	}···
	/** Value of comparator(); null if naturally ordered. */
	private Comparator<T> comp;
	/** Bounds on elements in this class, null if no bounds. */private T low, high;
	/** Sentinel of BST containing data. */
	private final BST<T> sent;
```

*Java representation for BSTSet class, showing only constructors and instance variables*

```java
/** Used internally to form views. */
private BSTSet (BSTSet<T> set, T low, T high) {
	comparator = set.comparator ();
	this.low = low;
	 this.high = high;
	 this.sent = set.sent;
	}/** An iterator over BSTSet. */
	private class BSTIter<T> implements Iterator<T> {
		/** Next node in iteration to yield. Equals the sentinel node* when done. */
		BST<T> next;
		/** Node last returned by next(), or null if none, or if remove()
		* has intervened. */
		BST<T> last;BSTIter () {
			last = null;
			next = ﬁrst node that is in bounds, or sent if none;
		}···
	}
	/** A node in the BST */
	private static class BST<T> {
		T label;BST<T> left, right, parent;
		/** A sentinel node */
		BST () {
		 label = null; parent = null;
		  }
		  BST (T label, BST<T> left, BST<T> right) {
		  	this.label = label;
		  	 this.left = left;
		  	  this.right = right;
		  	}
		  }

	
```

*continued: Private nested classes used in implementation*

![6.8](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/6.8.png)

*表示的BST部分在动物群和子集之间共享。三角形表示整个子树，圆形矩形表示单个节点。每个集合包含一个指向列表根的指针（一个sentinel节点，其标签被认为大于树中的任何值）、值的上下限（空表示未绑定）和比较器（在本例中为空，表示自然顺序）。迭代器包含一个指向子集的指针（它正在迭代），一个指向按顺序包含next label的节点的指针（next）（“duck”），另一个指向按i.next（）最后传递的序列包含标签的节点的指针（last）。BST的虚线区域完全由迭代器进行sk ipped。迭代器不返回“hartebeest”节点，但迭代器必须通过它才能到达它返回的节点。*



## 6.3  正交范围查询  Orthogonal Range Queries

在理想情况下，BST将使用线性排序的数据分成两部分。
然而分而治之的思想并不停留在以上应用，假设我们正在处理具有更多结构的key。
例如划分二维空间中的一些点。在某些情况下，我们可能希望根据它们的位置查找此集合中的单元；它们的key是它们的位置。
虽然有可能对这些键施加线性排序，但它并不是非常有用。
例如，我们可以使用词典排序，并定义（x0，y0）>（x1，y1）if x0 > x1 或 x0 = x1 和 y0 > y1
然而，对于水平位置位于A和B之间的物体，其垂直位置是任意的。这就造成一般的数据是没有用的

四叉树是指一种更好地利用二维位置数据的搜索树结构。
搜索的每一步都将主数据分为四组，每组都包含一个矩形区域内部的点。（建议联想成以一个点为原点画一个二位坐标系）。
如果是**PR四叉树**(PR quadtree )，那么分为四组的分界点可以是一个矩形的中心（这样的话将会划分成四块相同大小的矩形）。
如果是**点四叉树**(point quatree)，那么分界点可以是存储在树中的一个点。

图6.9说明了这两种四叉树背后的思想。树的每个节点对应一个矩形划分区域。
任何一个区域都可以根据若干内部分界点划分为西北、东北、东南和西南的矩形分区，这些分区由与分界点对应的树节点的子节点表示。
对于pr四叉树，这些分界点是矩形的中心，而对于点四叉树，它们是从数据点中选择的，就像二进制搜索树中的分界值是从存储在树中的数据中选择的一样。

![6.9](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/6.8.png)

*同一组数据的两种四叉树的图解*
对于pr四叉树，树的每个级别都包含表示具有相同边大小的正方形的节点（如右图所示）。
对于点四叉树，每个点都是将矩形区域划分为四个区域（通常不相等）的子树的根。


## 6.4 优先级队列和堆 Priority queues and heaps

假设我们考虑着一个不同的问题。我们不能通过快速搜索集合中是否存在某个元素，而是限制自己只能搜索最大的元素（我们也可以搜索最小的元素。
在BST中找到最大值是相当容易的，但是我们需要处理树的不平衡问题。通过限制自己插入元素的操作，以及查找和删除最大元素。
我们可以避免产生平衡问题。仅支持这些操作的数据结构称为优先级队列，因为我们按值的顺序从中删除项目，而不管到达顺序如何。

在Java中，我们可以简单地创建一个实现SoReTeSET的类，并且当X恰好是集合中的第一个元素时，在操作删除（x）时特别快。
但是，在使用这样一个类时，你会惊讶地发现迭代整个集合的速度有多慢。
因此，我们可能会改造他，如下所示。


```java
interface PriorityQueue<T extends Comparable<T>> {
	/** Insert item L into this queue. */
	public void insert(T L);
	/** True iff this queue is empty. */
	public boolean isEmpty();
	/** The largest element in this queue. Assumes !isEmpty(). */
	public T first();
	/** Remove and return an instance of the largest element (there may
	* be more than one; removes only one). Assumes !isEmpty(). */
	public T removeFirst();
}
```

用于表示优先级队列的数据结构是堆（由于命名冲突，注意不要与新分配内存的大容量存储区混淆）。 
堆只是一个满足以下属性的位置树（通常是二叉树）。

* 堆属性：树中任何节点上的标签大于或等于该节点的任何后代的标签。


由于叶子节点的顺序无关紧要，所以在如何排列堆中的值方面有更多的操作空间，这使得保持堆的茂密变得容易。
因此，当我们在此上下文中使用术语“heap”时，我们将表示具有heap属性的完整树。
这加快了对堆的所有操作，因为插入和删除所需的时间与堆的高度成正比。图6.11显示了一个典型的堆

![6.11](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/6.11.png)

*序列（b）–（d）显示删除最大项的步骤。最后一个（最底部，最右侧）标签首先向上移动以覆盖根的标签。
它被“筛选”直到堆属性被存储。阴影节点显示在进程中违反堆属性的位置。*


实现确定最大值的操作显然很容易。要删除最大的元素，同时保留堆属性和树的茂密性，
我们首先将堆底部的“最后一个元素”项移到树的根（替换和删除最大的元素），然后依据堆的性质重新构建。
图6.11 b-d说明了该过程。
这通常是用一个数组表示的二叉树来完成的，如5.3节的binarytree2类。下面给出了一种实现方式


```java
class Heap<T extends Comparable<T>>extends BinaryTree2<T> implements PriorityQueue<T> {
	/** A heap containing up to N > 0 elements. */
	public Heap (int N) {
	 super(N); }
	 /** The minimum label value (written −∞). */
	 static final int MIN = Integer.MIN_VALUE;
	 /** Insert item L into this queue. */
	 public void insert(T L) {
	 	extend(L);reHeapifyUp(currentSize()-1);
	 }
	 /** True iff this queue is empty. */
	 public boolean isEmpty() {
	  return currentSize() == 0; 
	}
	/** The largest element in this queue. Assumes !isEmpty(). */
	public int first() {
	 return label(0); 
	}
	/** Remove and return an instance of the largest element (there may
	* be more than one; removes only one). Assumes !isEmpty(). 
	*/
	public T removeFirst() {
		int result = label(0);
		setLabel(0, label(currentSize()-1));
		size -= 1;
		reHeapifyDown(0);
		return result;
	}
```

*Implementation of a common kind of priority queue: the heap.*

当然，通过反复确定最大的元素，我们可以对一组对象进行排序:
```java
/** Sort the elements of A in ascending order. */
static void heapSort(Integer[] A) {
	if (A.length <= 1)return;
	Heap<Integer> H = new Heap<Integer>(A.length);
	H.setHeap(A, 0, A.length);
	for (int i = A.length-1; i >= 0; i -= 1)
		A[i] = H.removeFirst();
}
```

```java
/** Restore the heap property in this tree, assuming that only
* NODE may have a label larger than that of its parent. */
protected void reHeapifyUp(int node) {
	if (node <= 0)return;T x = label(node);
	while (node != 0 && label(parent(node)).compareTo (x) < 0) {
		setLabel(node, label(parent(node)));
		node = parent(node);
	}setLabel(node, x);
}
/** Restore the heap property in this tree, assuming that only
* NODE may have a label smaller than those of its children. */
protected void reHeapifyDown(int node) {
	T x = label(node);
	while (true) {
		if (left(node) >= currentSize())
			break;

		int largerChild =
		(right(node) >= currentSize()|| label(right(node)).compareTo (label(left(node))) <= 0)?
		 left(node) : right(node);
		 if (x >= label(largerChild))
		 	break;
		 setLabel(node, label(largerChild));
		 node = largerChild;
		}
		setLabel(node, x);
		}
```

```java
/** Set the labels in this Heap to A[off], A[off+1], ...
* A[off+len-1]. Assumes that LEN <= maxSize(). */
public void setHeap(T[] A, int off, int len) {
	for (int i = 0; i < len; i += 1)
		setLabel(i, A[off+i]);
	size = len;
	heapify();
}
/** Turn label(0)..label(size-1) into a proper heap. */
protected void heapify() { ... }
```
*continued*

过程图解：

![6.13](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/6.13.png)

*堆排序示例。原始数组在（a）中；（b）是setheap的结果；（c）–（h）是连续迭代的结果。
每个都显示堆数组的活动部分和输出数组的已排序部分，由间隙分隔*

我们可以这样实现生成堆方法：
```java
protected void heapify(){
	for (int i = 1; i < size; i += 1)
		reHeapifyUp(i);
}
```

然而，有趣的是，这个实现并没有它所能达到的速度快，而且通过不同的方法执行操作更快，在这种方法中，我们从叶开始工作。也就是说，按照还原级别的顺序，我们将每个节点与其父节点交换，如果它更大，然后，至于重新组合，继续将父节点的值向下移动，直到恢复堆。这似乎与重复插入不同，但稍后我们将看到这一点。


```java
protected void heapify(){
	for (int i = size/2-1; i >= 0; i -= 1)
		reHeapifyUp(i);
}
```

### 6.4.1 生成堆时间

 如果我们研究**heapSort**排序n个项目所需的时间，我们会发现这是n个元素生成堆的成本加上提取n个项目所需的时间。
 从堆中提取n个项的最坏情况是，从堆的顶部节点开始，Ce（n）的开销主要由reHapifyDown的开销控制。
 如果我们对父标签和子标签进行计数比较，您会发现最坏情况下的成本与堆的当前高度成比例。
 假设堆的初始高度为**k**（$N = \sqrt[2]{k+1}-1$）。
 它一直保持这种状态，直到提取出$\sqrt[2]{k}$个元素（删除整个底行），然后变为K-1。
 接下来的$\sqrt[2]{k-1}$个元素保持 **K-1**，以此类推。
 因此，花费在提取项目上的总时间是：

![heap1](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/heap1.png)

现在让我们考虑一下堆积n元素的成本。
如果我们通过逐个插入n个元素并执行reHapifyUp来完成此操作，
那么我们将获得与提取n个元素类似的成本：
*对于第一次插入，我们将进行0个标签比较；
*对于第二次插入的2个数据，我们将进行1次比较；
*对于第二次插入的4个数据，我们将进行2次比较；
*等等


![heap2](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/heap2.png)

上面公式结果最坏的情况是，N个数据通过重复执行reHapifyDown来完成生成堆的操作。
这和刚刚给出的公式一样
![heap3](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/heap3.png)

但假设我们在第6.4节结尾处执行第二个算法，
对数组中所有项（从n/2−1开始一直到0）进行重新计算，从而进行堆积。
reHapifyDown的开销取决于到最深处的距离。
对于堆中的最后$\sqrt[2]{k}$个数据，这个成本是0（这就是为什么要跳过它们）。对于前面的$\sqrt[2]{k-1}$，成本为1

![heap4](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/heap4.png)

使用和之前一样的方法计算：

![heap5](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/heap5.png)


因此，第二种生成堆的方法比明显的重复插入方法运行得更快（渐进）。
当然，由于在最坏的情况下提取n个元素的成本仍然是Θ（N lg N），
堆排序的总体最坏的成本仍然是Θ（N lg N）。
但是，对于足够大的n，使用第二种形式的堆配置会有一些不变的因素优势。

##6.5 博弈树 Game Trees

如何在具有完备信息的双人博弈下做出最优决策。
简单地说，你可以通过列举所有可能的移动来做到这一点：
玩家可以从当前位置开始，然后给每个移动指定一个分数，然后选择得分最高的移动。
例如，你可以通过比较你的棋子数量和对手的棋子数量来计算得分。但这样的分数会误导人。
一个动作可能会给你更多的棋子，但是下一步你对手的回应可能会让你损失更多。
所以，对于每一个动作，你也应该考虑你对手的所有可以接受的动作，
假设他为他选择了最好的一个，并以此作为正反馈。
但是如果你有更好的应对方法呢？
我们如何合理地组织这次搜索

一种典型的方法是把博弈的所有可能性延展成一个空间，我们称之为博弈树。树中的每个节点都是博弈中的一个位置；
每个边都是一个决策。图6.14说明了我们的意思。每个节点都是一个位置；
节点的子节点是可能的下一个位置。每个节点上的数字是您猜测的收益（其中较大的值更适合您）。
问题是如何得到这些数字。

让我们递归地考虑这个问题。假设是你在某个位置上的移动，用P节点表示，你大概会选择给你最好分数的移动；
也就是说，你会选择得分最高的P节点的子节点。因此，合理地将该子节点的分数指定为P节点本身的分数。
反之，如果Q节点代表的是一个位置，在这个位置上，它会给你最好的分数。
在对手的回合中，对手可能会选择得分最低的Q的孩子（因为对你来说最小意味着对对手来说最好）。
因此，分配给q的适当值是最小子节点的值。
图6.14中的说明性博弈树上的数字符合这个分配分数的规则，这个规则被称为minimax算法。
图中的星型节点表示你和你的对手在得到这些分数后会认为最好的节点。


然而，对于大多数有趣的博弈，博弈树太大，无法存储或计算，除非在游戏的最后。
所以我们在某一点上切断了计算，即使结果叶位置不是最终位置。
通常，我们选择一个最大深度，并尝试计算基于位置本身的叶的值（称为静态评估）。
一个微小的变化是使用迭代深化：
在不断增加的深度重复搜索，直到达到某个时间限制，并采用迄今为止发现的最佳结果


![6.14](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/6.14.png)
图6.14：来计算得分。
节点是位置(positions)，线是决策(moves)，数字是为您估计每个位置的“优势”的分数。
星星表示根据优势分析应该选择的子节点。


### 6.5.1  Alpha-beta修剪算法  Alpha-beta pruning


与任何树搜索一样，博弈树搜索在树的深度上是指数级的。
此外，博弈树可以有相当大的分支数量。
很容易看出，如果一个人每一步都有16个选择，那么他就不能向前看很多步。
我们可以通过在搜索时修剪游戏树来缓解这个问题。

一种叫做α-β剪枝的技术是基于一个简单的观察：
如果我已经计算了移动到某个位置，p，将得到至少α的分数，
并且我已经部分地评估了其他可能的位置，q，
我得知它的值将小于α，那么我可以停止任何关于q的进一步计算（修剪掉未经观测的分支）。
同样，当计算对手的值时，如果我确定某个位置的值不超过β（分数越大，对我越好，对对手越差），
那么我可以停止计算已知值大于β的对手的任何其他位置

例如，考虑图6.15。在≥5位置，我知道对手不会选择移动到这里（因为他已经有一个-5移动）。
在≤−20位置，我的对手知道我永远不会选择移动到这里（因为我已经有了−5的移动）。

alpha-beta修剪绝不是加速搜索博弈树的唯一方法。
更复杂的搜索策略可以去参考AI相关算法。

![6.15](https://github.com/yongfengyan/cs61b-textbook-zh/blob/master/zh/img/6.15.png)

### 6.5.2 博弈树搜索算法   A game-tree search algorithm

下面的伪代码总结了本节中的讨论。如果你研究一下这节，你会发现我们在本节中讨论过的博弈树并没有真正实现。
相反，我们生成一个我们需要的节点的叶子，当不再需要的时候把他们丢弃。
实际上，根本没有树型数据结构；图6.14和6.15中所示的树是概念性的，或者如果您愿意，它们描述的是计算而不是数据结构。

```java 
/** A legal move for WHO that either has an estimated value >= CUTOFF
* or that has the best estimated value for player WHO, starting from
* position START, and looking up to DEPTH moves ahead. */
Move findBestMove (Player who, Position start, int depth, double cutoff){
	if (start is a won position for who) 
		return WON_GAME;
		 /* Value=∞ */
		 else if (start is a lost position for who) 
		 	return LOST_GAME;
		 	 /* Value=−∞ */
		 	 else if (depth == 0) 
		 	 	return guessBestMove (who, start, cutoff);
		 	 Move bestSoFar = Move.REALLY_BAD_MOVE;
		 	 for (each legal move, M, for who from position start) {
		 	 	Position next = start.makeMove (M);
		 	 	/* Negate here and below because best for opponent = worst for WHO */
		 	 	Move response = findBestMove (who.opponent (), next,depth-1, -bestSoFar.value ());
		 	 	if (-response.value () > bestSoFar.value ()) {
		 	 		Set M’s value to -response.value ();
		 	 		bestSoFar = M;if (M.value () >= cutoff) break;
		 	 	}
		 	 }return bestSoFar;
		 	}/** Static evaluation function. */
		 	Move guessBestMove (Player who, Position start, double cutoff){
		 		Move bestSoFar;bestSoFar = Move.REALLY_BAD_MOVE;
		 		for (each legal move, M, for who from position start) {
		 			Position next = start.makeMove (M);
		 			Set M’s value to heuristic guess of value to who of next;
		 			if (M.value () > bestSoFar.value ()) {bestSoFar = M;
		 				if (M.value () >= cutoff)break;
		 			}
		 		}return bestSoFar;
		 	}
```

*Game-tree search w ith alpha-beta p runing*








*原文附带相关练习题，如有需求点击查看*
[原文及练习题](https://apachecn.org/cs61b-textbook-zh/en/#pf77)





















