Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
think-dast-zh
提交
0413ba70
T
think-dast-zh
项目概览
OpenDocCN
/
think-dast-zh
8 个月 前同步成功
通知
0
Star
26
Fork
13
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
T
think-dast-zh
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
0413ba70
编写于
9月 22, 2017
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
ch13
上级
75af26f7
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
104 addition
and
0 deletion
+104
-0
13.md
13.md
+104
-0
未找到文件。
13.md
浏览文件 @
0413ba70
# 第十三章 二叉搜索树
> 原文:[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.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录