在一种情况下,使用哈希码作为相等比较的捷径是有意义的。
考虑您正在构建哈希表或哈希集的情况。事实上,让我们只考虑哈希集(哈希表通过保存一个值来扩展它,但这不相关)。
可以采用多种不同的方法,但在所有这些方法中,您都有少量可以放置散列值的槽,我们采用开放式或封闭式方法(只是为了好玩,有些人使用相反的术语给别人);如果我们为两个不同的对象在同一个槽上发生碰撞,我们可以将它们存储在同一个槽中(但有一个链表等对象实际存储的位置)或重新探测以选择不同的槽(有各种策略)。
现在,无论采用哪种方法,我们都在从我们想要的哈希表的 O(1) 复杂度转向 O(n) 复杂度。这样做的风险与可用槽的数量成反比,因此在一定大小后,我们调整哈希表的大小(即使一切都很理想,如果存储的项目数大于插槽)。
在调整大小时重新插入项目显然取决于哈希码。正因为如此,虽然在一个对象中记忆很少有意义GetHashCode()
(它只是在大多数对象上调用得不够频繁),但在哈希表本身中记忆它肯定是有意义的(或者也许是记忆产生的结果,例如,如果您使用 Wang/Jenkins 散列重新散列以减少由错误GetHashCode()
实现造成的损害)。
现在,当我们开始插入时,我们的逻辑将类似于:
- 获取对象的哈希码。
- 获取对象的插槽。
- 如果插槽为空,则将对象放入其中并返回。
- 如果 slot 包含相等的对象,我们就完成了一个哈希集,并且可以替换哈希表的值。这样做,然后返回。
- 根据碰撞策略尝试下一个插槽,然后返回第 3 项(如果我们循环太频繁,可能会调整大小)。
因此,在这种情况下,我们必须在比较是否相等之前获取哈希码。我们还有已经预先计算的现有对象的哈希码以允许调整大小。这两个事实的结合意味着将我们对第 4 项的比较实现为:
private bool IsMatch(KeyType newItem, KeyType storedItem, int newHash, int oldHash)
{
return ReferenceEquals(newItem, storedItem) // fast, false negatives, no false positives (only applicable to reference types)
||
(
newHash == oldHash // fast, false positives, no fast negatives
&&
_cmp.Equals(newItem, storedItem) // slow for some types, but always correct result.
);
}
显然,这样做的优势取决于 的复杂性_cmp.Equals
。如果我们的密钥类型是,int
那么这将是一种完全的浪费。如果我们的 key type where string 并且我们使用不区分大小写的 Unicode 标准化相等比较(因此它甚至不能缩短长度),那么节省可能是值得的。
通常记忆散列码是没有意义的,因为它们的使用频率不足以赢得性能,但是将它们存储在散列集或散列表本身中是有意义的。