diff --git a/10.md b/10.md new file mode 100644 index 0000000000000000000000000000000000000000..2774d4c80f5b4ef3b58ee360404422d1d260a2ed --- /dev/null +++ b/10.md @@ -0,0 +1,218 @@ +# 第十章 哈希 + +> 原文:[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 implements Map { + + protected List> maps; + + public MyBetterMap(int k) { + makeMaps(k); + } + + protected void makeMaps(int k) { + maps = new ArrayList>(k); + for (int i=0; i()); + } + } +} +``` + +实例变量`maps`是一组`MyLinearMap`对象。构造函数接受一个参数`k`,决定至少最开始,要使用多少个映射。然后`makeMaps`创建内嵌的映射并将其存储在一个`ArrayList`中。 + +现在,完成这项工作的关键是,我们需要一些方法来查看一个键,并决定应该进入哪个映射。当我们`put`一个新的键时,我们选择一个映射;当我们`get`同样的键时,我们必须记住我们把它放在哪里。 + + +一种可能性是随机选择一个子映射,并跟踪我们把每个键放在哪里。但我们应该如何跟踪?看起来我们可以用一个`Map`来查找键,并找到正确的子映射,但是练习的整个一点是编写一个有效的实现`Map`。我们不能假设我们已经有了。 + +一个更好的方法是使用一个哈希函数,它 接受一个`Object`,一个任意的Object`,并返回一个称为哈希码的整数。重要的是,如果它不止一次看到相同的`Object`,它总是返回相同的哈希码。这样,如果我们使用哈希码来存储键,当我们查找时,我们将得到相同的哈希码。 + + +在Java中,每个`Object`都提供了`hashCode`,一种计算哈希函数的方法。这种方法的实现对于不同的对象是不同的;我们会很快看到一个例子。 + +这是一个辅助方法,为一个给定的键选择正确的子映射: + +```java +protected MyLinearMap 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 map = chooseMap(key); + return map.put(key, value); +} + +public V get(Object key) { + MyLinearMap 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 上阅读更多内容。 + + +该哈希函数满足要求:如果两个`SillyString`对象包含相等的内嵌字符串,则它们将获得相同的哈希码。 + +这可以正常工作,但它可能不会产生良好的性能,因为它为许多不同的字符串返回相同的哈希码。如果两个字符串以任何顺序包含相同的字母,它们将具有相同的哈希码。即使它们不包含相同的字母,它们可能会产生相同的总量,例如`"ac"`和`"bb"`。 + +如果许多对象具有相同的哈希码,它们将在同一个子映射中。如果一些子映射比其他映射有更多的条目,那么当我们有`k`个映射时,加速比可能远远小于`k`。所以哈希函数的目的之一是统一;也就是说,以相等的可能性,在这个范围内产生任何值。你可以在 上阅读更多设计完成的,散列函数的信息。 + +## 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