假设我需要从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代码进行基准测试非常困难,所以我希望得到一些有根据的猜测。我的推理上面是否正确(列表更适合小型,地图更适合大型)?阈值大小约为多少?各种List
和HashMap
实现有何不同?
答案 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更好(他们使用哈希)但是在一天结束时,您只需要对特定应用程序进行基准测试。我不明白为什么HashMaps对于小案例会更慢,但如果是,基准测试会给你答案。
另一个选项,TreeMap
是另一个地图数据结构,它使用树而不是哈希来访问条目。如果您正在进行基准测试,那么您也可以进行基准测试。
关于基准测试,主要问题之一是垃圾收集器。但是,如果您执行的测试不分配任何对象,那应该不是问题。填写你的地图/列表,然后写一个循环来获得N个随机元素,然后计时,它应该是合理可重复的,因此提供信息。
答案 3 :(得分:2)
不幸的是,您将不得不自己进行基准测试,因为相对性能将主要取决于实际的String值,以及测试不在映射中的字符串的相对概率。当然,这取决于String.equals()
和String.hashCode()
的实施方式,以及所使用的HashMap
和List
类的详细信息。
在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
条目,假设字符串平均长度为5
到10
。)
答案 4 :(得分:1)
从我记忆中,list方法将是O(n),但是可以快速添加项目,因为不会发生任何计算。如果您实现了b搜索或其他搜索算法,则可以获得较低的O(log n)。哈希值为O(1),但插入速度较慢,因为每次添加元素时都需要计算哈希值。
我在.net中知道,这是一个名为HybridDictionary的特殊集合,它正是这样做的。使用列表到点,然后是哈希。我认为交叉点大约是10,所以这可能是一个很好的线条。
我会说你在上面的陈述中是正确的,尽管我不是100%确定小集合的列表是否更快,以及交叉点是什么。
答案 5 :(得分:1)
我认为HashMap
总会更好。如果您有n
个字符串,每个字符串的长度最多为l
,那么String#hashCode
和String#equals
都是O(l)
(无论如何,在Java的默认实现中)。
执行List#indexOf
时,它会遍历列表(O(n)
)并对每个元素(O(l)
)执行比较,以提供O(nl)
表现。
Java的HashMap
有(假设)r
个桶,每个桶都包含一个链表。这些列表中的每一个都具有O(n/r)
的长度(假设String的hashCode
方法在桶之间均匀地分配字符串)。要查找字符串,您需要计算hashCode
(O(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
设置为initialCapacity
和r
对于您的loadFactor
和n/r
选择n
的参数。