从字符串映射到整数 - 各种方法的性能

时间:2010-10-21 09:46:05

标签: java performance data-structures big-o

假设我需要从String到整数进行映射。整数是唯一的,从0开始形成连续范围。即:

Hello -> 0
World -> 1
Foo   -> 2
Bar   -> 3
Spam  -> 4
Eggs  -> 5
etc.

至少有两种直接的方法可以做到这一点。使用hashmap:

HashMap<String, Integer> map = ...
int integer = map.get(string); // Plus maybe null check to avoid NPE in unboxing.

或列表:

List<String> list = ...
int integer = list.indexOf(string); // Plus maybe check for -1.

我应该使用哪种方法,为什么?可以说相对性能取决于列表/地图的大小,因为List#indexOf()是使用String#equals()的线性搜索 - &gt; O(n)效率,而HashMap#get()使用哈希来缩小搜索范围 - &gt;当地图很大时肯定会更有效率,但是当只有很少的元素时可能会更低效(计算哈希值必须有一些开销,对吧?)。

由于正确地对Java代码进行基准测试非常困难,所以我希望得到一些有根据的猜测。我的推理上面是否正确(列表更适合小型,地图更适合大型)?阈值大小约为多少?各种ListHashMap实现有何不同?

6 个答案:

答案 0 :(得分:5)

第三个选项,可能我最喜欢的是使用trie

我敢打赌它在性能上胜过HashMap(没有冲突+无论如何计算哈希码都是O(length of string)的事实),在某些情况下可能还有List方法(例如,如果你的字符串有很长的公共前缀,因为indexOf会在equals方法中浪费大量时间。)

在列表和地图之间进行选择时,我会选择Map(例如HashMap)。这是我的理由:

  • <强>可读性

    Map界面只为这个用例提供了更直观的界面。

  • 在正确的地方进行优化

    我会说如果你使用的是List,那么无论如何你都会针对小案例进行优化。那可能不是瓶颈的位置。

第四个选项将使用LinkedHashMap,如果尺寸较小则迭代它;如果尺寸很大,则get关联的数字。

第五个选项是将决策封装在一个单独的类中。在这种情况下,您甚至可以实现它以在列表增长时在运行时更改策略。

答案 1 :(得分:4)

你是对的:一个列表是O(n),一个HashMap就是O(1),所以一个HashMap对n来说会更快,所以计算哈希值的时间不会淹没List线性搜索。

我不知道门槛大小;这是一个实验或更好的分析问题,而不是我现在可以考虑的事情。

答案 2 :(得分:4)

你的问题在所有方面都是完全正确的:

  • HashMap s更好(他们使用哈希)
  • 对Java代码进行基准测试很难

但是在一天结束时,您只需要对特定应用程序进行基准测试。我不明白为什么HashMaps对于小案例会更慢,但如果是,基准测试会给你答案。

另一个选项,TreeMap是另一个地图数据结构,它使用树而不是哈希来访问条目。如果您正在进行基准测试,那么您也可以进行基准测试。

关于基准测试,主要问题之一是垃圾收集器。但是,如果您执行的测试不分配任何对象,那应该不是问题。填写你的地图/列表,然后写一个循环来获得N个随机元素,然后计时,它应该是合理可重复的,因此提供信息。

答案 3 :(得分:2)

不幸的是,您将不得不自己进行基准测试,因为相对性能将主要取决于实际的String值,以及测试不在映射中的字符串的相对概率。当然,这取决于String.equals()String.hashCode()的实施方式,以及所使用的HashMapList类的详细信息。

HashMap的情况下,查找通常涉及计算键字符串的哈希值,然后将键字符串与一个或多个条目键字符串进行比较。哈希码计算查看String的所有字符,因此依赖于键字符串。 equals操作通常会在equals返回true时检查所有字符,而在返回false时则更少。为给定键字符串调用equals的实际次数取决于散列键字符串的分布方式。通常情况下,您希望平均有1或2个呼叫等于“命中”,而对于“未命中”则可能高达3。

List的情况下,在“点击”的情况下,查找将调用equals平均一半的条目键字符串,而在“点击”的情况下,所有这些字符串都是“小姐”。如果您知道要查找的键的相对分布,则可以通过对列表进行排序来提高“命中”案例的性能。但“未命中”案例无法优化。

除了@aioobe建议的trie替代方法之外,您还可以使用所谓的perfect hash function将专用String实现为整数hashmap。这会将每个实际的键字符串映射到一个小范围内的唯一哈希值。然后可以使用散列来索引键/值对的数组。这减少了对一个哈希函数调用和一个String.equals调用的查找。 (如果您可以假设提供的密钥将始终是映射字符串之一,则可以省去对equals的调用。)

完美哈希方法的难点在于找到适用于映射中的键集的函数,并且计算起来并不太昂贵。 AFAIK,这必须通过反复试验来完成。

但实际情况是,简单地使用HashMap是一种安全的选择,因为它使O(1)性能具有相对较小的比例常数(除非输入键是病态的)。

(FWIW,我的猜测HashMap.get()优于List.contains()的收支平衡点小于10条目,假设字符串平均长度为510。)

答案 4 :(得分:1)

从我记忆中,list方法将是O(n),但是可以快速添加项目,因为不会发生任何计算。如果您实现了b搜索或其他搜索算法,则可以获得较低的O(log n)。哈希值为O(1),但插入速度较慢,因为每次添加元素时都需要计算哈希值。

我在.net中知道,这是一个名为HybridDictionary的特殊集合,它正是这样做的。使用列表到点,然后是哈希。我认为交叉点大约是10,所以这可能是一个很好的线条。

我会说你在上面的陈述中是正确的,尽管我不是100%确定小集合的列表是否更快,以及交叉点是什么。

答案 5 :(得分:1)

我认为HashMap总会更好。如果您有n个字符串,每个字符串的长度最多为l,那么String#hashCodeString#equals都是O(l)(无论如何,在Java的默认实现中)。

执行List#indexOf时,它会遍历列表(O(n))并对每个元素(O(l))执行比较,以提供O(nl)表现。

Java的HashMap有(假设)r个桶,每个桶都包含一个链表。这些列表中的每一个都具有O(n/r)的长度(假设String的hashCode方法在桶之间均匀地分配字符串)。要查找字符串,您需要计算hashCodeO(l)),查找存储桶(O(1) - 一个,而不是l),然后遍历该存储桶链表(O(n/r)元素)对每个元素进行O(l)比较。这使得总查找时间为O(l + (nl)/r)

由于List实现是O(nl)而HashMap实现是O(nl/r)(我正在删除第一个l,因为它相对无关紧要),查找性能应该等于{{1对于r=1的所有更大值,HashMap会更快。

请注意,使用this构造函数构建r时,可以设置r(将HashMap设置为initialCapacityr对于您的loadFactorn/r选择n的参数。