有一种情况下,使用哈希码作为相等比较的快捷方式是有意义的。
考虑构建哈希表或哈希集合的情况。实际上,让我们只考虑哈希集合(哈希表通过还保存值来扩展,但这并不相关)。
有各种不同的方法可以采取,但在所有方法中,您都有一小部分插槽可用于放置哈希值,并且我们采用开放或封闭的方法(仅仅是为了好玩,有些人使用相反的术语)。如果我们在两个不同对象的相同插槽上发生碰撞,我们可以将它们存储在同一个插槽中(但是需要链表或类似的东西来存储实际存储的对象),或者重新探测以选择另一个插槽(有各种策略可供选择)。
现在,无论采用哪种方法,我们都会远离哈希表所需的O(1)复杂度,而向O(n)复杂度靠近。这种风险与可用插槽数量成反比,因此在一定大小后,我们会调整哈希表的大小(即使一切理想,如果存储的项目数大于插槽数,我们最终也必须这样做)。
重新调整大小时重新插入项目显然取决于哈希码。因此,虽然在对象中很少有必要记忆GetHashCode()
(大多数对象上不经常调用它),但在哈希表本身内部记忆它肯定是有意义的(或者,记忆一个生成的结果,例如如果您使用Wang/Jenkins哈希重新哈希以减少由糟糕的GetHashCode()
实现引起的损坏)。
现在,当我们来插入时,我们的逻辑将是:
- 获取对象的哈希码。
- 获取对象的插槽。
- 如果插槽为空,请将对象放入其中并返回。
- 如果插槽包含相等的对象,则对于哈希集合我们已完成,并且具有替换哈希表值的位置。执行此操作,并返回。
- 根据碰撞策略尝试下一个插槽,并返回到第3项(如果我们循环太多次,则可能调整大小)。
因此,在这种情况下,我们必须在比较相等性之前获取哈希码。我们还预先计算了现有对象的哈希码以允许调整大小。这两个事实的组合意味着,我们可以将第4项的比较实现为:
private bool IsMatch(KeyType newItem, KeyType storedItem, int newHash, int oldHash)
{
return ReferenceEquals(newItem, storedItem)
||
(
newHash == oldHash
&&
_cmp.Equals(newItem, storedItem)
);
}
显然,这取决于_cmp.Equals
的复杂性。 如果我们的键类型是int
,那么这将是完全浪费。 如果我们的键类型是字符串,并且我们使用不区分大小写的Unicode规范化等式比较(因此甚至不能在长度上进行快捷方式),那么节省的时间可能是值得的。
通常情况下,备忘哈希码没有意义,因为它们不经常使用,无法提高性能,但将它们存储在哈希集或哈希表本身中是有意义的。