Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
think-dast-zh
提交
75af26f7
T
think-dast-zh
项目概览
OpenDocCN
/
think-dast-zh
9 个月 前同步成功
通知
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 搜索 >>
提交
75af26f7
编写于
9月 22, 2017
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
ch13.
上级
daa25695
变更
2
显示空白变更内容
内联
并排
Showing
2 changed file
with
179 addition
and
3 deletion
+179
-3
12.md
12.md
+0
-3
13.md
13.md
+179
-0
未找到文件。
12.md
浏览文件 @
75af26f7
...
...
@@ -207,11 +207,8 @@ public class MyTreeMap<K, V> implements Map<K, V> {
你的
`put`
实现的是时间应该与树的高度
`h`
成正比,而不是元素的数量
`n`
。理想情况下,你只需搜索一次树,但如果你发现两次更容易搜索,可以这样做:它会慢一些,但不会改变增长级别。
> 译者注:如果你同时跟踪和返回父节点,可以只搜索一次。
最后,你应该填充
`keySet`
。根据
<http://thinkdast.com/mapkeyset>
的文档,该方法应该返回一个
`Set`
,可以按顺序迭代键;也就是说,按照
`compareTo`
方法,升序迭代。我们在 8.3 节中使用的
`HashSet`
实现不会维护键的顺序,但
`LinkedHashSet`
实现可以。你可以阅读
<http://thinkdast.com/linkedhashset>
。
> 译者注:你也可以使用`TreeSet`,它和你正在实现的东西原理差不多,只不过把 BST 换成了红黑树。
我提供了一个
`keySet`
的大纲,创建并返回
`LinkedHashSet`
:
...
...
13.md
0 → 100644
浏览文件 @
75af26f7
# 第十三章 二叉搜索树
本章介绍了上一个练习的解决方案,然后测试树形映射的性能。我展示了一个实现的问题,并解释了 Java 的
`TreeMap`
如何解决它。
## 13.1 简单的`MyTreeMap`
上一个练习中,我给了你
`MyTreeMap`
的大纲,并让你填充缺失的方法。现在我会展示结果,从
`findNode`
开始:
```
java
private
Node
findNode
(
Object
target
)
{
// some implementations can handle null as a key, but not this one
if
(
target
==
null
)
{
throw
new
IllegalArgumentException
();
}
// something to make the compiler happy
@SuppressWarnings
(
"unchecked"
)
Comparable
<?
super
K
>
k
=
(
Comparable
<?
super
K
>)
target
;
// the actual search
Node
node
=
root
;
while
(
node
!=
null
)
{
int
cmp
=
k
.
compareTo
(
node
.
key
);
if
(
cmp
<
0
)
node
=
node
.
left
;
else
if
(
cmp
>
0
)
node
=
node
.
right
;
else
return
node
;
}
return
null
;
}
```
`findNode`
是
`containsKey`
和
`get`
所使用的一个私有方法;它不是
`Map`
接口的一部分。参数
`target`
是我们要查找的键。我在上一个练习中解释了这种方法的第一部分:
+
在这个实现中,
`null`
不是键的合法值。
+
在我们可以在
`target`
上调用
`compareTo`
之前,我们必须把它强制转换为某种形式的
`Comparable`
。这里使用的“类型通配符”会尽可能允许;也就是说,它适用于任何实现
`Comparable`
类型,并且它的
`compareTo`
接受
`K`
或者任和
`K`
的超类。
之后,实际搜索比较简单。我们初始化一个循环变量
`node`
来引用根节点。每次循环中,我们将目标与
`node.key`
比较。如果目标小于当前键,我们移动到左子树。如果它更大,我们移动到右子树。如果相等,我们返回当前节点。
如果在没有找到目标的情况下,我们到达树的底部,我就认为,它不在树中并返回
`null`
。
## 13.2 搜索值
我在前面的练习中解释了,
`findNode`
运行时间与树的高度成正比,而不是节点的数量,因为我们不必搜索整个树。但是对于
`containsValue`
,我们必须搜索值,而不是键;BST 的特性不适用于值,因此我们必须搜索整个树。
我的解法是递归的:
```
java
public
boolean
containsValue
(
Object
target
)
{
return
containsValueHelper
(
root
,
target
);
}
private
boolean
containsValueHelper
(
Node
node
,
Object
target
)
{
if
(
node
==
null
)
{
return
false
;
}
if
(
equals
(
target
,
node
.
value
))
{
return
true
;
}
if
(
containsValueHelper
(
node
.
left
,
target
))
{
return
true
;
}
if
(
containsValueHelper
(
node
.
right
,
target
))
{
return
true
;
}
return
false
;
}
```
`containsValue`
将目标值作为参数,并立即调用
`containsValueHelper`
,传递树的根节点作为附加参数。
这是
`containsValueHelper`
的工作原理:
+
第一个
`if`
语句检查递归的边界情况。如果
`node`
是
`null`
,那意味着我们已经递归到树的底部,没有找到
`target`
,所以我们应该返回
`false`
。请注意,这只意味着目标没有出现在树的一条路径上;它仍然可能会在另一条路径上被发现。
+
第二种情况检查我们是否找到了我们正在寻找的东西。如果是这样,我们返回
`true`
。否则,我们必须继续。
+
第三种情况是执行递归调用,在左子树中搜索
`target`
。如果我们找到它,我们可以立即返回
`true`
,而不搜索右子树。否则我们继续。
+
第四种情况是搜索右子树。同样,如果我们找到我们正在寻找的东西,我们返回
`true`
。否则,我们搜索完了整棵树,返回
`false`
。
该方法“访问”了树中的每个节点,所以它的所需时间与节点数成正比。
## 13.3 实现`put`
`put`
方法比起
`get`
要复杂一些,因为要处理两种情况:(1)如果给定的键已经在树中,则替换并返回旧值;(2)否则必须在树中添加一个新的节点,在正确的地方。
在上一个练习中,我提供了这个起始代码:
```
java
public
V
put
(
K
key
,
V
value
)
{
if
(
key
==
null
)
{
throw
new
IllegalArgumentException
();
}
if
(
root
==
null
)
{
root
=
new
Node
(
key
,
value
);
size
++;
return
null
;
}
return
putHelper
(
root
,
key
,
value
);
}
```
并且让你填充
`putHelper`
。这里是我的答案:
```
java
private
V
putHelper
(
Node
node
,
K
key
,
V
value
)
{
Comparable
<?
super
K
>
k
=
(
Comparable
<?
super
K
>)
key
;
int
cmp
=
k
.
compareTo
(
node
.
key
);
if
(
cmp
<
0
)
{
if
(
node
.
left
==
null
)
{
node
.
left
=
new
Node
(
key
,
value
);
size
++;
return
null
;
}
else
{
return
putHelper
(
node
.
left
,
key
,
value
);
}
}
if
(
cmp
>
0
)
{
if
(
node
.
right
==
null
)
{
node
.
right
=
new
Node
(
key
,
value
);
size
++;
return
null
;
}
else
{
return
putHelper
(
node
.
right
,
key
,
value
);
}
}
V
oldValue
=
node
.
value
;
node
.
value
=
value
;
return
oldValue
;
}
```
第一个参数
`node`
最初是树的根,但是每次我们执行递归调用,它指向了不同的子树。就像
`get`
一样,我们用
`compareTo`
方法来弄清楚,跟随哪一条树的路径。如果
`cmp < 0`
,我们添加的键小于
`node.key`
,那么我们要走左子树。有两种情况:
+
如果左子树为空,那就是,如果
`node.left`
是
`null`
,我们已经到达树的底部而没有找到
`key`
。这个时候,我们知道
`key`
不在树上,我们知道它应该放在哪里。所以我们创建一个新节点,并将它添加为
`node`
的左子树。
+
否则我们进行递归调用来搜索左子树。
如果
`cmp > 0`
,我们添加的键大于
`node.key`
,那么我们要走右子树。我们处理的两个案例与上一个分支相同。最后,如果
`cmp == 0`
,我们在树中找到了键,那么我们更改它并返回旧的值。
我使用递归编写了这个方法,使它更易于阅读,但它可以直接用迭代重写一遍,你可能想留作练习。
## 13.4 中序遍历
我要求你编写的最后一个方法是
`keySet`
,它返回一个
`Set`
,按升序包含树中的键。在其他
`Map`
实现中,
`keySet`
返回的键没有特定的顺序,但是树形实现的一个功能是,对键进行简单而有效的排序。所以我们应该利用它。
这是我的答案:
```
java
public
Set
<
K
>
keySet
()
{
Set
<
K
>
set
=
new
LinkedHashSet
<
K
>();
addInOrder
(
root
,
set
);
return
set
;
}
private
void
addInOrder
(
Node
node
,
Set
<
K
>
set
)
{
if
(
node
==
null
)
return
;
addInOrder
(
node
.
left
,
set
);
set
.
add
(
node
.
key
);
addInOrder
(
node
.
right
,
set
);
}
```
在
`keySet`
中,我们创建一个
`LinkedHashSet`
,这是一个
`Set`
实现,使元素保持有序(与大多数其他
`Set`
实现不同)。然后我们调用
`addInOrder`
来遍历树。
第一个参数
`node`
最初是树的根,但正如你的期望,我们用它来递归地遍历树。
`addInOrder`
对树执行经典的“中序遍历”。
如果
`node`
是
`null`
,这意味着子树是空的,所以我们返回,而不向
`set`
添加任何东西。否则我们:
1.
按顺序遍历左子树。
1.
添加
`node.key`
。
1.
按顺序遍历右子树。
请记住,BST 的特性保证左子树中的所有节点都小于
`node.key`
,并且右子树中的所有节点都更大。所以我们知道,
`node.key`
已按正确的顺序添加。
递归地应用相同的参数,我们知道左子树中的元素是有序的,右子树中的元素也一样。并且边界情况是正确的:如果子树为空,则不添加任何键。所以我们可以认为,该方法以正确的顺序添加所有键。
因为
`containsValue`
方法访问树中的每个节点,所以所需时间与
`n`
成正比。
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录