【问题标题】:Acceptable types to use as keys in a HashTable在 HashTable 中用作键的可接受类型
【发布时间】:2009-11-03 20:39:51
【问题描述】:

我必须承认,我对 HashTables 的工作原理只有初步的了解,尽管据我所知,这似乎相当简单。我的问题是这样的:似乎传统智慧是使用简单的基本值类型,例如整数作为 HashTable 中的键。然而,字符串也经常被使用,尽管在许多语言中它们被实现为引用类型。我觉得一般鼓励的是使用复杂的引用类型;我猜这是因为这样做需要更慢的哈希函数?但是为什么字符串如此常用呢?毕竟,字符串内部不是 char[] 数组吗(同样,在大多数语言中)?

最后,什么值类型通常被认为是“最好的”(甚至只是“可接受的”)选择作为 HashTable 中的键?是否有任何常用的选择实际上被认为是“坏的”(可能像字符串)?

【问题讨论】:

    标签: language-agnostic types hashtable key


    【解决方案1】:

    这不是 字符串整数 或值与引用的问题,而是 可变键与不可变键的问题。 只要键是不可变的(因此它们的散列值永远不会改变)它们可以索引散列表。例如,Java 中的字符串是不可变的,因此非常适合用作哈希表键。

    顺便说一句,如果一种数据类型足够简单,可以始终按值传递(如标量),那当然没问题。

    但是现在假设你使用了一个可变类型;如果您给我一个对这些对象之一的引用作为键,我将计算其哈希值,然后将其放入我的哈希表存储桶之一。但是当你以后修改对象时,我将没有办法得到通知;并且该对象现在可能驻留在错误的存储桶中(如果其哈希值不同)。

    希望这会有所帮助。

    【讨论】:

    • 这是一个非常有帮助的答案;但我仍然想知道是否有某些类型比其他类型“更好”用作键。例如,假设我已经定义了一个实际上是不可变的类,并且将在其整个存在期间保持相同的哈希码。那么用作键是否完全可以,还是使用整数之类的东西会更好(出于性能原因)?在我看来,完整、全面的答案可能是您的(键必须是不可变类型)和 spa(用作键的类型应该具有有效的哈希函数)的组合......
    • @Dan:一个特定的哈希表需要存储它需要存储的东西。如果您要维护 Web 缓存,则您正在存储 URL 的内容。键必须是 URL,不能是整数,因为您不是在查找整数。显然更快是“更好”,但“慢慢做我想做的事”总是比“做一些非常快但完全没用的事情”“更好”:-)
    • 同样重要的是要注意,如果键的目的是识别特定对象,那么使用可变类类型作为哈希表键没有任何问题。例如,在 .net 中,System.Windows.Forms.Form 是一种非常可变的类型(具有可以随时更改的位置等属性),但可以使用哈希表将表单与其他内容相关联。请注意,这样的表将始终认为对不同形式的两个引用是不相等的,即使它们的所有属性(除了它们的标识)碰巧都匹配。
    【解决方案2】:

    大多数字符串实现,虽然它们可能在托管环境中显示为引用类型,但它们的实现通常是不可变类型。

    哈希函数的作用是将大量的状态映射到较少的状态。

    这就是为什么字符串散列有利于测试字符串相等性的原因。您可以将值映射到数组的索引,并非常快速地查找有关该值的一些信息。您不需要将每个字符与每个其他字符串中的每个其他字符进行比较。你可以对任何事情说同样的话。这一切都是关于以某种有用的方式减少或识别任意数量的字节。

    这是关于您在哈希表中使用的键类型的讨论变得无效的地方,因为它是将该值映射到较小的状态空间以及如何在内部使用它使其有用。整数通常是硬件友好的,但 32 位并不是一个真正的大空间,并且对于任意输入,在该空间内可能会发生冲突。

    最后,当您确实使用哈希表时,计算哈希值的成本与将每个值与每个其他可能位置的其他值进行比较所需的时间无关(假设您的哈希表包含数百个项目)。

    【讨论】:

    • 我确实理解哈希函数通过将(可能)较大的值映射到较小的空间来工作,但是哈希函数的速度是否也取决于其输入的大小?这就是为什么我认为通常不鼓励使用大型引用类型作为键的原因。但是,如果不是这样,那么我想知道为什么会不鼓励这样做(也许是因为开发人员需要实现他/她自己的高效哈希函数?)。
    • 正如我所说,许多托管环境将字符串实现为不可变类型。当你有一个不可变类型时,不需要每次都计算哈希码,因为值是常量(一旦创建)。通常,您只需为每个唯一字符串支付一次生成哈希码的成本。例如.NET 运行时维护一个内部字符串池来完成此操作。但是,从未知字符串生成哈希码的成本是存在的,但成本与用作键的字符串的长度有关,而不是与集合(或哈希表)的大小有关。
    • 这对我来说很违反直觉。你是说,如果我将一个项目添加到 HashTable,然后通过键检索该项目,运行时将神奇地知道该键的散列码,而无需执行散列函数?这怎么可能?
    • 是的,好吧,显然不是这样的。这不是魔术,而是运行时如何处理字符串的副作用。这个想法是所有相同的字符串值在内存中只表示一次。运行时仍在支付维护字符串池的成本,但您永远不会最终支付多次散列字符串的成本。
    • 所以你是说运行时在内部维护自己的类似字符串的对象,每个对象都有一个哈希码的缓存值(对吗?)......这就是我的still 不明白:如果我实例化一个新字符串——或者更确切地说,即使运行时确实将它视为相同的字符串,在我看来什么是新字符串——运行时如何知道它已经拥有它内存中的字符串,不使用哈希表?我知道确定我是否有缓存的哈希码的唯一方法是在 HashTable 中查找键...这需要计算哈希码...我说得通吗?
    【解决方案3】:

    只要提供了合适的哈希函数,所有类型都可以作为键。请记住,哈希表毕竟只是一个线性数组。哈希函数采用某种类型的键并计算哈希表数组(称为存储桶)中存储值的索引(但存在一些冲突问题)。

    所以真正棘手的部分是找到一个哈希函数。当然它应该具有某些属性,比如计算简单、混乱(几乎相同的键应该映射到完全不同的哈希表桶)、确定性(相同的键意味着相同的哈希表桶)、统一性(所有可能的键被均匀地映射到桶),或满射性(应使用哈希表的所有桶)。

    似乎为整数等简单类型定义这样的函数更容易。

    【讨论】:

    • 确实如此。但是,它是一个定义,哪些键被认为是相等的,哪些不是。
    【解决方案4】:

    最好的hash keys 是那些

    1. 具有良好的(如在低 collisions 中)散列(对于 .NET,请参阅 Object.GetHashCode,对于 Java,请参阅 Object.hashcode
    2. 进行快速比较(当存在哈希冲突时)。

    综上所述,我认为字符串在大多数情况下都是很好的哈希键,因为字符串有很多出色的哈希实现。

    【讨论】:

      【解决方案5】:

      如果您要使用复杂类型作为键,那么:

      • 哈希表实现很难将项目分组到桶中以进行快速检索;它如何决定如何将一系列哈希分组到一个桶中?
      • 哈希表可能需要对类型有深入的了解才能选择存储桶。
      • 存在对象属性更改的风险,导致项目最终进入错误的存储桶。哈希值必须是不可变的。

      之所以常用整数,是因为它们很容易拆分为与桶对应的范围,它们是值类型,因此是不可变的,而且它们相当容易生成。

      【讨论】:

        猜你喜欢
        • 2014-07-04
        • 2021-10-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-05-13
        • 1970-01-01
        • 2022-06-11
        • 1970-01-01
        相关资源
        最近更新 更多