在采访中有人问我这个问题,采访者想讨论我能想到的所有方法之间的权衡:
设计并实现TwoSum类。它应该支持以下内容 操作:添加和查找。
add-将数字添加到内部数据结构中。
find-查找是否存在总和等于该值的数字对。
我首先想出了以下解决方案,这很简单。
设计1:
public class TwoSumDesign1 {
private final Map<Integer, Integer> map = new HashMap<Integer, Integer>();
public void add(int number) {
map.put(number, map.getOrDefault(number, 0) + 1);
}
public boolean find(int value) {
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
int i = entry.getKey();
int j = value - i;
if ((i == j && entry.getValue() > 1) || (i != j && map.containsKey(j))) {
return true;
}
}
return false;
}
}
但是经过一些研究,我发现我们可以使用List存储所有数字,并且迭代列表比迭代keySet
更快,但是我仍然不明白为什么?
引用自:https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html
在集合视图上进行迭代需要的时间与HashMap实例的“容量”(存储桶数)及其大小(键值映射数)成正比。因此,如果迭代性能很重要,那么不要将初始容量设置得太高(或负载因子太低)是非常重要的。
Design2:
public class TwoSumDesign2 {
private final List<Integer> list = new ArrayList<Integer>();
private final Map<Integer, Integer> map = new HashMap<Integer, Integer>();
// Add the number to an internal data structure.
public void add(int number) {
if (map.containsKey(number))
map.put(number, map.get(number) + 1);
else {
map.put(number, 1);
list.add(number);
}
}
// Find if there exists any pair of numbers whose sum is equal to the value.
public boolean find(int value) {
for (int i = 0; i < list.size(); i++) {
int num1 = list.get(i), num2 = value - num1;
if ((num1 == num2 && map.get(num1) > 1) || (num1 != num2 && map.containsKey(num2)))
return true;
}
return false;
}
}
有人可以解释在这个问题上我们应该考虑的所有折衷方案,以及为什么第二个解决方案比迭代地图的keySet
更快吗?
答案 0 :(得分:4)
首先,我要说的是,我们所讨论的性能差异几乎不值得考虑。短语“因此,如果迭代性能很重要,则不要将初始容量设置得太高(或将负载系数设置得太低)非常重要”。这不是很重要。我宁愿说“因此,您可能不想设置初始容量...”
现在我们已经涵盖了这一点,让我们继续进行实际的回答。
与列表的简单组织相比,它与哈希映射的内部数据结构的组织方式有关。
散列图的标准实现采用“存储桶”列表,其中每个存储桶是节点的链接列表。键和值存储在这些节点中。存储桶列表中没有密集填充,这意味着许多条目是null
。
因此,为了遍历地图的所有键,您必须遍历存储桶列表,并且对于每个存储桶,遍历存储桶中的节点。
由于节点的数量和键的数量相同,因此节点的遍历与整个ArrayList
的遍历时间复杂度相同,但是在哈希映射的情况下,必须计算步行清单清单的开销。哈希图的“初始大小”越大或填充因子越小,null
存储桶将越多,这意味着您将在其中访问的存储桶列表中将有更多条目徒然,只是发现他们是null
并继续进行下一个输入。
因此,遍历HashMap
的费用比遍历ArrayList
的费用稍高。
但是请相信我,两者之间的差异是如此之小,以至于不值得考虑。没人会注意到。最好根据您的目的使用正确的数据结构,而不用担心性能的微小提高。正确的数据结构始终是产生最优雅解决方案的数据结构。最优雅的解决方案是最容易阅读和理解其功能以及如何执行的解决方案。
答案 1 :(得分:2)
在迭代Map
时,通常的陷阱是在使用keySet
来检索与键关联的值的同时,在get(key)
上迭代。通过在设计1中迭代entrySet
可以避免这种情况。
实际上,由于数据局部性,在HashMap
上进行迭代很可能会更加昂贵。遍历数组时,编译器可以引入许多优化。当您有Node
个支持HashMap
的对象列表时,这些对象将不存在,请参阅Bjarne Stroustrup: Why you should avoid Linked Lists。
但是,设计1更易于阅读和理解。这非常重要,过早的优化是万恶之源。在决定优化代码之前,应先测量性能的实际差异。很有可能是设计2中引入的新List
实际上会由于内存访问的更多间接性(两个数据结构与一个数据结构)而降低性能。
答案 2 :(得分:-1)
在第二种设计的情况下,引入了两种数据结构(HashMap和List)。 根据我的理解,当我们谈论代码的性能时,请同时检查两种情况:有效数据结构和内存利用率。
在第二种情况下,我们需要额外的内存。
设计1st易于阅读和理解,很可能是设计2中引入的新列表实际上会由于内存访问的更多间接性而降低性能。