HashMap - 包含和获取方法不应该一起使用

时间:2015-05-29 05:16:25

标签: java algorithm dictionary hash hashmap

我在接受采访时得到了以下问题。

我得到了一个像这样的字符数组:

<?php

// YOUR CODE

$output = shell_exec('php myCustomPHPInstallScript.php');
// myCustomPHPInstallScript.php is name of your PHP file.
Mage::log($output, null, 'myCustomLogFile.log'); 
//creating log file

//YOUR CODE

?>

我需要获得每个角色的不同角色和数量:

char[] characters = {'u', 'a', 'u', 'i', 'o', 'f', 'u'};

所以我用Java回答了以下代码:

u = 3
a = 1
i = 1
o = 1
f = 1

面试官是一名解决方案架构师。他问我为什么在这里使用HashMap<Character, Integer> map = new HashMap<Character, Integer>(); int i = 1; for (char c : characters) { if (map.containsKey(c)) { int val = map.get(c); map.put(c, ++val); } else map.put(c, i); } containsKey()方法,并指出使用这两种方法都是多余的。他的观点是什么?我在这做错了什么?我的代码会导致性能问题等吗?

11 个答案:

答案 0 :(得分:25)

架构师意味着getcontainsKey具有相同的费用,可能会累积到一张支票中:

Integer val = map.get(c);
if (val != null) {
  ...
} else {
  ...
}

但我想知道为什么建筑师只关心这一点,因为还有更多的事情需要改进:

  • 通过接口( Effective Java 2nd Edition,Item 52
  • 引用对象
  • 从Java 1.7开始,您可以使用菱形运算符&lt;&gt;
  • 累积角色的自动装箱操作
  • 如果你使用AtomicInteger(或任何其他可修改的数字类)而不是Integer,你甚至可以将get与其中一个puts合并

因此,从我的角度来看,使用HashMap时的最佳性能将提供:

Map<Character, AtomicInteger> map = new HashMap<>();
for (Character c : characters) {
    AtomicInteger val = map.get(c);
    if (val != null) {
        val.incrementAndGet();
    } else {
        map.put(c, new AtomicInteger(1));
    }
}

如果字符的范围很小(并且事先已知),则可以使用int数组进行计数。这将是所有可能解决方案中最快的:

char firstCharacter = 'a';
char lastCharacter = 'z';
int[] frequency = new int[lastCharacter - firstCharacter + 1];
for (char c : characters) {
  frequency[c - firstCharacter]++;
}

答案 1 :(得分:18)

你的代码是多余的,因为get和containsKey做了几乎相同的工作。您可以检查get是否返回null值,而不是调用containsKey。

代码可以简化为:

HashMap<Character, Integer> map = new HashMap<Character, Integer>();
for (char c : characters) {   
    Integer val = map.get(c);          
    if (val == null)
        val = 0;
    map.put(c,++val);
}

答案 2 :(得分:8)

您可以像这样编写for循环 -

for (char c : characters) {             

   Integer val = map.get(c);
   if (null != val){
      map.put(c, ++val);
   } else {
      map.put(c, 1);
   }
}  

注意:我已将int修改为Integer,以便我可以针对null进行检查。如果地图已包含值,则返回值它将与您声明的Integer变量val一起分配。否则val将为null。所以我认为你不需要使用Map.containsKey()方法。

答案 3 :(得分:7)

让我们从您的代码开始,然后开始减少它。

HashMap<Character, Integer> map = new HashMap<Character, Integer>();
int i = 1;

for (char c : characters)
{             
    if (map.containsKey(c))
    {
        int val = map.get(c);
        map.put(c, ++val);
    }
    else map.put(c, i);
}

我要做的第一件事是使用Java 7钻石运算符,并删除变量i

Map<Character, Integer> map = new HashMap<>();

for (char c : characters)
{
    if (map.containsKey(c))
        map.put(c, ++map.get(c));
    else
        map.put(c, 1);
}

这是我的第一步,我们删除了变量i,因为它始终为常量1并且在执行期间不会发生变化。我还简要说明了这句话,并map.get拨打map.put电话。现在,在看到时,我们有三次调用map方法。

Map<Character, Integer> map = new HashMap<>();

for (char c : characters)
{
    Integer i = map.get(c);

    if (i == null) i = 0;

    map.put(c, ++i);
}

这是最好的方式,也是@Eran在上面的回答中所说的。希望这种细分有所帮助。

答案 4 :(得分:6)

for (char c : characters) {   
     Integer val = map.get(c);
     if(val != null){
        map.put(c, ++val); 
     }else{
        map.put(c, 1);
     }
 }

这可能是最好的方式

函数get和contains都做同样的工作......

而不是通过使用get函数

同时使用它的好处

使用get函数时检查空值。 通过避免这两个调用,它将改善性能。

注意:在这种情况下,可能看起来性能没有任何改善,但在另一种情况下会有大量的数据。

答案 5 :(得分:6)

从Java 8开始,你甚至可以这样做:

final Map<Character, Integer> map = new HashMap<>();
for (char c : characters)
    map.merge(c, 1, Integer::sum);

请注意,您使用此解决方案进行了大量的装箱和拆箱。这应该不是问题,但要注意它是很好的。

上面的代码实际上做了什么(即手动装箱和拆箱):

for (char c : characters)
    map.merge(
        Character.valueOf(c),
        Integer.valueOf(1),
        (a, b) -> Integer.valueOf(Integer.sum(a.intValue(), b.intValue())));

答案 6 :(得分:4)

如果您想将字符计数放在Map中,我通常会这样做。

onunload

Map.containsKey(key)将检查地图中与Map.get(key)非常相似的指定键。在你的代码中,你同时调用&#34; containsKey&#34;并且&#34;得到&#34;方法意味着您将经历两次条目,这可能会导致性能问题。

答案 7 :(得分:2)

好吧,我也是一名系统架构师,我认为你的代码没有任何问题,除非没有大括号 - 你通常应该总是使用它们。在我看来,这很好:

for (char c : characters) {             
    if (map.containsKey(c)) {
        int val = map.get(c);
        map.put(c, ++val);
    } else {
        map.put(c, 1);
    }
}

就个人而言,我会这样写,这与你自己的版本非常相似:

for (char c : characters) {
    int val = map.containsKey(c) ? map.get(c) : 0;
    map.put(c, ++val);
}

为什么同时使用containsKey()get()?好吧,如果你只想使用get(),那么你需要以某种方式进行空检查。阅读代码的其他人if (map.containsKey(c))if (val != null)哪个更清楚?实际差异很小。

散列查找为O(log N),因此调用get() containsKey()会导致两次查找而不是1.如果您已经继续讨论性能这个问题的含义以及它如何与一个非常大的数据集一起运行,那将是相关的。

最后,如果没有containtsKey()检查,int val = map.get(c);会在第一时间投出,那么您需要使用Integer val = map.get(c);。哪个更清晰,更安全 - int valInteger val?我认为让自动装箱做事并使用int val并没有错,我通常尽可能使用原始类型而不是对象,尽管int vs Integer可能有很多不同的意见。

答案 8 :(得分:2)

我还没见过另一个Java 8解决方案:

Character[] characters = {'u', 'a', 'u', 'i', 'o', 'f', 'u'};
Map<Character, Integer> result = Arrays.asList(characters)
        .stream()
        .collect(Collectors.groupingBy(Function.identity(), Collectors.summingInt(c -> 1)));

它确实需要使用盒装类型字符,但是 - Arrays.asList不能与char[]一起使用,而Arrays.stream()也没有{{1}的重载}。

答案 9 :(得分:1)

问题是containskey必须遍历Map的整个条目以获取密钥(迭代1)。以下包含密钥的代码。

public boolean containsKey(Object key) {
    return getEntry(key) != null;
}
final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

现在get(&#39;&#39;)必须再次迭代才能获得密钥映射的值(迭代2)。 get的代码也调用getEntry,如下所示。

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}

当不需要时,您不必要地在条目集中迭代2次,从而导致性能问题。 @Eran在答案中给出了最好的方法。

答案 10 :(得分:1)

答案很简单,的确如此。包含方法检查每次循环中元素是否存在于集合中。因此,越大的集合,它将对每个下一个元素执行检查的时间越长。包含对散列集合非常有用,其中不可能通过索引获取元素。但是对于这样的意图需要覆盖hashCode并且等于正确。在这种情况下,包含将采用O(1)。