Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
think-dast-zh
提交
e3b8937a
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 搜索 >>
提交
e3b8937a
编写于
9月 20, 2017
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
ch10
上级
91d4384a
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
218 addition
and
0 deletion
+218
-0
10.md
10.md
+218
-0
未找到文件。
10.md
0 → 100644
浏览文件 @
e3b8937a
# 第十章 哈希
> 原文:[Chapter 10 Hashing](http://greenteapress.com/thinkdast/html/thinkdast011.html)
> 译者:[飞龙](https://github.com/wizardforcel)
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
在本章中,我定义了一个比
`MyLinearMap`
更好的
`Map`
接口实现,
`MyBetterMap`
,并引入哈希,这使得
`MyBetterMap`
效率更高。
## 10.1 哈希
为了提高
`MyLinearMap`
的性能,我们将编写一个新的类,它被称为
`MyBetterMap`
,它包含
`MyLinearMap`
对象的集合。它在内嵌的映射之间划分键,因此每个映射中的条目数量更小,这加快了
`findEntry`
,以及依赖于它的方法的速度。
这是类定义的开始:
```
java
public
class
MyBetterMap
<
K
,
V
>
implements
Map
<
K
,
V
>
{
protected
List
<
MyLinearMap
<
K
,
V
>>
maps
;
public
MyBetterMap
(
int
k
)
{
makeMaps
(
k
);
}
protected
void
makeMaps
(
int
k
)
{
maps
=
new
ArrayList
<
MyLinearMap
<
K
,
V
>>(
k
);
for
(
int
i
=
0
;
i
<
k
;
i
++)
{
maps
.
add
(
new
MyLinearMap
<
K
,
V
>());
}
}
}
```
实例变量
`maps`
是一组
`MyLinearMap`
对象。构造函数接受一个参数
`k`
,决定至少最开始,要使用多少个映射。然后
`makeMaps`
创建内嵌的映射并将其存储在一个
`ArrayList`
中。
现在,完成这项工作的关键是,我们需要一些方法来查看一个键,并决定应该进入哪个映射。当我们
`put`
一个新的键时,我们选择一个映射;当我们
`get`
同样的键时,我们必须记住我们把它放在哪里。
一种可能性是随机选择一个子映射,并跟踪我们把每个键放在哪里。但我们应该如何跟踪?看起来我们可以用一个
`Map`
来查找键,并找到正确的子映射,但是练习的整个一点是编写一个有效的实现
`Map`
。我们不能假设我们已经有了。
一个更好的方法是使用一个哈希函数,它 接受一个
`Object`
,一个任意的Object
`,并返回一个称为哈希码的整数。重要的是,如果它不止一次看到相同的`
Object
`,它总是返回相同的哈希码。这样,如果我们使用哈希码来存储键,当我们查找时,我们将得到相同的哈希码。
在Java中,每个`
Object
`都提供了`
hashCode
`,一种计算哈希函数的方法。这种方法的实现对于不同的对象是不同的;我们会很快看到一个例子。
这是一个辅助方法,为一个给定的键选择正确的子映射:
```java
protected MyLinearMap<K, V> chooseMap(Object key) {
int index = 0;
if (key != null) {
index = Math.abs(key.hashCode()) % maps.size();
}
return maps.get(index);
}
```
如果`
key
`是`
null
`,我们任意选择索引为`
0
`的子映射。否则,我们使用`
hashCode
`获取一个整数,调用`
Math.abs
`来确保它是非负数,然后使用余数运算符`
%
`,这保证结果在`
0
`和`
maps.size()-1
`之间。所以`
index
`总是一个有效的`
maps
`索引。然后`
chooseMap
`返回为其所选的映射的引用。
我们使用`
chooseMap
`的`
put
`和`
get
`,所以当我们查询键的时候,我们得到添加时所选的相同映射,我们选择了相同的映射。至少应该是 - 稍后我会解释为什么这可能不起作用。
这是我的`
put
`和`
get
`的实现:
```java
public V put(K key, V value) {
MyLinearMap<K, V> map = chooseMap(key);
return map.put(key, value);
}
public V get(Object key) {
MyLinearMap<K, V> map = chooseMap(key);
return map.get(key);
}
```
很简单,对吧?在这两种方法中,我们使用`
chooseMap
`来找到正确的子映射,然后在子映射上调用一个方法。这就是它的工作原理。现在让我们考虑一下性能。
如果在`
k
`个子映射中分配了`
n
`个条目,则平均每个映射将有`
n/k
`个条目。当我们查找一个键时,我们必须计算其哈希码,这需要一些时间,然后我们搜索相应的子映射。
因为`
MyBetterMap
`中的条目列表,比`
MyLinearMap
`中的短`
k
`倍,我们的预期是`
ķ
`倍的搜索速度。但运行时间仍然与`
n
`成正比,所以`
MyBetterMap
`仍然是线性的。在下一个练习中,你将看到如何解决这个问题。
## 10.2 哈希如何工作?
哈希函数的基本要求是,每次相同的对象应该产生相同的哈希码。对于不变的对象,这是比较容易的。对于具有可变状态的对象,我们必须花费更多精力。
作为一个不可变对象的例子,我将定义一个`
SillyString
`类,它包含一个`
String
`:
```java
public class SillyString {
private final String innerString;
public SillyString(String innerString) {
this.innerString = innerString;
}
public String toString() {
return innerString;
}
```
这个类不是很有用,所以它叫做`
SillyString
`。但是我会使用它来展示,一个类如何定义它自己的哈希函数:
```java
@Override
public boolean equals(Object other) {
return this.toString().equals(other.toString());
}
@Override
public int hashCode() {
int total = 0;
for (int i=0; i<innerString.length(); i++) {
total += innerString.charAt(i);
}
return total;
}
```
注意`
SillyString
`重写了`
equals
`和`
hashCode
`。这个很重要。为了正常工作,`
equals
`必须和`
hashCode
`,这意味着如果两个对象被认为是相等的 - 也就是说,`
equals
`返回`
true
` - 它们应该有相同的哈希码。但这个要求只是单向的;如果两个对象具有相同的哈希码,则它们不一定必须相等。
`
equals
`通过调用`
toString
`来工作,返回`
innerString
`。因此,如果两个`
SillyString
`对象的`
innerString
`实例变量相等,它们就相等。
`
hashCode
`的原理是,迭代`
String
`中的字符并将它们相加。当你向`
int
`添加一个字符时,Java 将使用其 Unicode 代码点,将字符转换为整数。你不需要了解 Unicode 的任何信息来弄清此示例,但如果你好奇,可以在 <http://thinkdast.com/codepoint> 上阅读更多内容。
该哈希函数满足要求:如果两个`
SillyString
`对象包含相等的内嵌字符串,则它们将获得相同的哈希码。
这可以正常工作,但它可能不会产生良好的性能,因为它为许多不同的字符串返回相同的哈希码。如果两个字符串以任何顺序包含相同的字母,它们将具有相同的哈希码。即使它们不包含相同的字母,它们可能会产生相同的总量,例如`
"ac"
`和`
"bb"
`。
如果许多对象具有相同的哈希码,它们将在同一个子映射中。如果一些子映射比其他映射有更多的条目,那么当我们有`
k
`个映射时,加速比可能远远小于`
k
`。所以哈希函数的目的之一是统一;也就是说,以相等的可能性,在这个范围内产生任何值。你可以在 <http://thinkdast.com/hash> 上阅读更多设计完成的,散列函数的信息。
## 10.3 哈希和可变性
`
String
`是不可变的,`
SillyString
`也是不可变的,因为`
innerString
`定义为`
final
`。一旦你创建了一个`
SillyString
`,你不能使`
innerString
`引用不同的`
String
`,你不能修改所指向的`
String
`。因此,它将始终具有相同的哈希码。
但是让我们看看一个可变对象会发生什么。这是一个`
SillyArray
`定义,它与`
SillyString
`类似,除了它使用一个字符数组而不是一个`
String
`:
```java
public class SillyArray {
private final char[] array;
public SillyArray(char[] array) {
this.array = array;
}
public String toString() {
return Arrays.toString(array);
}
@Override
public boolean equals(Object other) {
return this.toString().equals(other.toString());
}
@Override
public int hashCode() {
int total = 0;
for (int i=0; i<array.length; i++) {
total += array[i];
}
System.out.println(total);
return total;
}
```
`
SillyArray
`也提供`
setChar
`,它能够修改修改数组内的字符。
```java
public void setChar(int i, char c) {
this.array[i] = c;
}
```
现在假设我们创建了一个`
SillyArray
`,并将其添加到`
map
`。
```java
SillyArray array1 = new SillyArray("Word1".toCharArray());
map.put(array1, 1);
```
这个数组的哈希码是`
461
`。现在如果我们修改了数组内容,之后尝试查询它,像这样:
```java
array1.setChar(0, 'C');
Integer value = map.get(array1);
```
修改之后的哈希码是`
441
`。使用不同的哈希码,我们就很可能进入了错误的子映射。这就很糟糕了。
一般来说,使用可变对象作为散列数据结构中的键是很危险的,这包括`
MyBetterMap
`和`
HashMap
`。如果你可以保证映射中的键不被修改,或者任何更改都不会影响哈希码,那么这可能是正确的。但是避免这样做可能是一个好主意。
## 10.4 练习 8
在这个练习中,你将完成`
MyBetterMap
`的实现。在本书的仓库中,你将找到此练习的源文件:
+ `
MyLinearMap.java
`包含我们在以前的练习中的解决方案,我们将在此练习中加以利用。
+ `
MyBetterMap.java
`包含上一章的代码,你将填充一些方法。
+ `
MyHashMap.java
`包含按需增长的哈希表的概要,你将完成它。
+ `
MyLinearMapTest.java
`包含`
MyLinearMap
`的单元测试。
+ `
MyBetterMapTest.java
`包含`
MyBetterMap
`的单元测试。
+ `
MyHashMapTest.java
`包含`
MyHashMap
`的单元测试。
+ `
Profiler.java
`包含用于测量和绘制运行时间与问题大小的代码。
+ `
ProfileMapPut.java
`包含配置该`
Map.put
`方法的代码 。
像往常一样,你应该运行`
ant build
`来编译源文件。然后运行`
ant MyBetterMapTest
`。几个测试应该失败,因为你有一些工作要做!
从以前的章节回顾`
put
`和`
get
`的实现。然后填充`
containsKey
`的主体。提示:使用`
chooseMap
`。再次运行`
ant MyBetterMapTest
`并确认通过了`
testContainsKey
`。
填充`
containsValue
`的主体。提示:不要使用`
chooseMap
`。`
ant MyBetterMapTest
`再次运行并确认通过了`
testContainsValue
`。请注意,比起找到一个键,我们必须做更多的操作才能找到一个值。
类似`
put
`和`
get
`,这个实现的`
containsKey
`
是线性的,因为它搜索了内嵌子映射之一。在下一章中,我们将看到如何进一步改进此实现。
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录