提交 0413ba70 编写于 作者: W wizardforcel

ch13

上级 75af26f7
# 第十三章 二叉搜索树
> 原文:[Chapter 13 Binary search tree](http://greenteapress.com/thinkdast/html/thinkdast014.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
本章介绍了上一个练习的解决方案,然后测试树形映射的性能。我展示了一个实现的问题,并解释了 Java 的`TreeMap`如何解决它。
## 13.1 简单的`MyTreeMap`
......@@ -177,3 +185,99 @@ private void addInOrder(Node node, Set<K> set) {
递归地应用相同的参数,我们知道左子树中的元素是有序的,右子树中的元素也一样。并且边界情况是正确的:如果子树为空,则不添加任何键。所以我们可以认为,该方法以正确的顺序添加所有键。
因为`containsValue`方法访问树中的每个节点,所以所需时间与`n`成正比。
## 13.5 对数时间的方法
`MyTreeMap`中,`get``put`方法所需时间与树的高度`h`成正比。在上一个练习中,我们展示了如果树是满的 - 如果树的每一层都包含最大数量的节点 - 树的高度与`log n`成横臂。
我也说了,`get``put`是对数时间的;也就是说,他们的所需时间与`logn`成正比。但是对于大多数应用程序,不能保证树是满的。一般来说,树的形状取决于键和添加顺序。
为了看看这在实践中是怎么回事,我们将用两个样本数据集来测试我们的实现:随机字符串的列表和升序的时间戳列表。
这是生成随机字符串的代码:
```java
Map<String, Integer> map = new MyTreeMap<String, Integer>();
for (int i=0; i<n; i++) {
String uuid = UUID.randomUUID().toString();
map.put(uuid, 0);
}
```
`UUID``java.util`中的类,可以生成随机的“通用唯一标识符”。UUID 对于各种应用是有用的,但在这个例子中,我们利用一种简单的方法来生成随机字符串。
我使用`n=16384`来运行这个代码,并测量了最后的树的运行时间和高度。以下是输出:
```
Time in milliseconds = 151
Final size of MyTreeMap = 16384
log base 2 of size of MyTreeMap = 14.0
Final height of MyTreeMap = 33
```
我包含了“`MyTreeMap`大小的`2`为底的对数”,看看如果它已满,树将是多高。结果表明,高度为`14`的完整树包含`16384`个节点。
随机字符串的树高度实际为33,这远大于理论上的最小值,但不是太差。要查找`16,384`个键中的一个,我们只需要进行`33`次比较。与线性搜索相比,速度快了近`500`倍。
这种性能通常是随机字符串,或其他不按照特定顺序添加的键。树的最终高度可能是理论最小值的`2~3`倍,但它仍然与`log n`成正比,这远小于`n`。事实上,随着`n`的增加,`logn`会慢慢增加,在实践中,可能很难将对数时间与常数时间区分开。
然而,二叉搜索树并不总是表现良好。让我们看看,当我们以升序添加键时会发生什么。下面是一个示例,以微秒为单位测量时间戳,并将其用作键:
```java
MyTreeMap<String, Integer> map = new MyTreeMap<String, Integer>();
for (int i=0; i<n; i++) {
String timestamp = Long.toString(System.nanoTime());
map.put(timestamp, 0);
}
```
`System.nanoTime`返回一个`long`类型的整数,表示以微秒为单位的启动时间。每次我们调用它时,我们得到一个更大的数字。当我们将这些时间戳转换为字符串时,它们按字典序增加。
让我们看看当我们运行它时会发生什么:
```java
Time in milliseconds = 1158
Final size of MyTreeMap = 16384
log base 2 of size of MyTreeMap = 14.0
Final height of MyTreeMap = 16384
```
运行时间是以前的时间的七倍多。时间更长。如果你想知道为什么,看看树的最后的高度:`16384`
![](img/13-1.jpg)
图 13.1:二叉搜索树,平衡(左边)和不平衡(右边)
如果你思考`put`如何工作,你可以弄清楚发生了什么。每次添加一个新的键时,它都大于树中的所有键,所以我们总是选择右子树,并且总是将新节点添加为,最右边的节点的右子节点。结果是一个“不平衡”的树,只包含右子节点。
这种树的高度正比于`n`,不是`logn`,所以`get``put`的性能是线性的,不是对数的。
图 13.1 显示了平衡和不平衡树的示例。在平衡树中,高度为`4`,节点总数为`2^4 - 1 = 15`。在节点数相同的不平衡树中,高度为`15`
## 13.6 自平衡树
这个问题有两种可能的解决方案:
你可以避免向`Map`按顺序添加键。但这并不总是可能的。
你可以制作一棵树,如果碰巧按顺序处理键,那么它会更好地处理键。
第二个解决方案是更好的,有几种方法可以做到。最常见的是修改`put`,以便它检测树何时开始变得不平衡,如果是,则重新排列节点。具有这种能力的树被称为“自平衡树”。普通的自平衡树包括 AVL 树(“AVL”是发明者的缩写),以及红黑树,这是 Java`TreeMap`所使用的。
在我们的示例代码中,如果我们用 Java 的`MyTreeMap`替换,随机字符串和时间戳的运行时间大致相同。实际上,时间戳运行速度更快,即使它们有序,可能是因为它们花费的时间较少。
总而言之,二叉搜索树可以以对数时间实现`get``put`,但是只能按照使得树足够平衡的顺序添加键。自平衡树通过每次添加新键时,进行一些额外的工作来避免这个问题。
你可以在 <http://thinkdast.com/balancing> 上阅读自平衡树的更多信息。
## 13.7 更多练习
在上一个练习中,你不必实现`remove`,但你可能需要尝试。如果从树中央删除节点,则必须重新排列剩余的节点,来恢复 BST 的特性。你可以自己弄清楚如何实现,或者你可以阅读 <http://thinkdast.com/bstdel> 上的说明。
删除一个节点并重新平衡一个树是类似的操作:如果你做这个练习,你将更好地了解自平衡树如何工作。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册