【问题标题】:How expensive is a call to java.util.HashMap.keySet()?调用 java.util.HashMap.keySet() 有多贵?
【发布时间】:2010-04-08 13:53:13
【问题描述】:

我将稀疏矩阵实现为List<Map<Integer,Double>>
要获取第 i 行的所有条目,我调用 list.get(i).keySet()。这个电话有多贵?

我还使用了 trove 库作为 List<TIntDoubleHashMap> 的替代实现。
在这里拨打list.get(i).keys() 的费用是多少?

您对如何实现高效稀疏矩阵有任何进一步的想法吗?
或者你能提供一个java中现有实现的列表吗?

【问题讨论】:

    标签: java


    【解决方案1】:

    取决于实现 List 和 Map 的类。如果您使用的是实现 java.util.RandomAccess 的 List 类(即 ArrayList),那么对 get(i) 的调用是 O(1)。如果是 LinkedList,则为 O(n)。

    -- 编辑后显示以下代码 sn-p(因为下面的 verdy_p 读起来不太好,而且喜欢离题):--

    // In HashMap.java, line 867, JDK 1.6.0.24, how much more
    // constant time do we want?
    
    public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }
    

    -- 编辑结束--

    在大多数 Map 实现中对 keySet() 的调用将是常数时间。

    关于遍历 keySet() 如果您使用数组支持的 Map 实现(如 HashMap),keySet() 依赖于 entrySet(),它返回一个由数组支持的内部迭代器。所以keySet()的迭代是O(n)。

    我还假设大多数(如果不是全部)由数组支持的 Map 实现都是这种情况。

    对于 SortedMap 实现(如 TreeMap),对其键进行迭代类似于在树上从最低键到最大键进行迭代。这相当于一个失败的二分查找 O(n)。

    这两种情况似乎都是 O(n)。如果您使用 Eclipse,您实际上可以查看实现 java 类的代码并更好地了解它们的复杂性。

    对于 java.util.concurrent 下的类(如 ConcurrentHashMap),您必须考虑其他因素来确定它们的成本。


    再扩展一点,如果你使用链表,list.get(i).keyset() 将是 O(n)。使用 ArrayList,它将是 O(1)。遍历键集将取决于您使用的是数组支持的 Map (HashMap) 还是 SortedMap (TreeMap)。在这两种情况下,遍历都将是 O(n),前者比后者快 显着,因为数组遍历总是比遍历指针(或此 Java 特定情况下的引用)更快。

    现在,如果您考虑 both list.get(i).keySet() 和集合的迭代,使用链表实现,将为 O(n^2)。因此,您应该使用迭代器而不是执行 list.get(i).keySet()(请参阅下面的 pseudocode,为了清楚起见,它避免了通用语法)

    对于未实现 java.util.RandomAccess(如 LinkedList)的列表,这是 O(n^2):

    for( int i = 0; i < list.size(); i++ )
    {
       Set keySet = list.get(i).keySet();
       for( Integer key : keySet.iterator() )
       {
          ... stuff (assuming constant time) ...
       }
    }
    

    对于相同类型的 List 实现,这是 O(n):

    for( Map m : list.iterator() )
    {
       for( Integer key : m.keySet() )
       {
          ... stuff (assuming constant time) ...
       }
    }
    

    【讨论】:

    • 您写道“在大多数 Map 实现中对 keySet() 的调用将是常数时间。”这是完全错误的,只是基于 错误假设 Hashmap 在大多数情况下避免冲突。事实上,Hashmap 只能避免少量的冲突,具体取决于它的填充因子(Hashmap 填充的越多,冲突就越多)。但是默认的 Hashmap 使用大约 85% 的填充因子,这意味着您将在大约一半的情况下(对于随机访问)发生 1 次冲突。平均而言,您将获得大约 1.5 个值来遍历以获得好的值。
    • 这还取决于值在其计算的 hash() 上的分布情况:如果您的 hash() 函数未正确编写以正确随机化他们想要的所有可能源值的位散列,结果会更差,你会得到更多的碰撞。所以你应该写“”在大多数 Map 实现上对 keySet() 的调用将在 O(1) 时间内执行。”但不保证这个 O(1) 时间会很短(甚至可能是最坏的情况O(n) with a bad hash() function !)
    • 请注意,本机类型(byte、short、int、long、float、double)的内置 hash() 函数非常基本,并且 not 创建真正的随机分布32 位模式中的位返回为 hash !这意味着 HashMap 根本不保证恒定的时间,对于许多容易重现的最坏情况甚至不保证 O(1) 访问时间(在这种情况下,HashMap 的行为类似于具有 O( n) 访问时间!,仅通过 HashMap 填充因子缓解,在我看来这太大了,应该减少到 50% 而不是默认的 85%....)
    • @verdy_p,伙计,请在回复之前慢慢阅读一两次。查看 HashMap.keySet() 的 JDK 源代码(HashMap.java,第 867 行,JDK 1.6.0.24)并告诉我它不是在恒定时间内执行的。 在您回复之前不要回复。更好的是,看看 polygenelubricants 的回复(2010 年 4 月 8 日)。调用 keySet() 不涉及任何时间的散列、搜索或冲突检测。我知道总是有一种基本的冲动来发布一些东西并展示我们所知道的,但是来吧,至少阅读你批评的句子。
    • 主要原因是在一个典型的情况下,使用盒装类型来管理大量数字条目确实很慢,在这种情况下,您将处理多个稀疏矩阵中不断重新分配的大量值。垃圾收集器不是很快,即使对于在单独的池中分配了小的常量大小的盒装原生类型也是如此。
    【解决方案2】:

    它很便宜,因为它是一个视图。

    来自jdk7 source line 884

    public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }
    

    Trove 可能更快,因为与 Java 集合框架不同,它可以直接使用原语而无需昂贵的装箱/拆箱。

    【讨论】:

      【解决方案3】:

      根据Sparse matrices / arrays in Java,Colt 库包含此功能;潜入他们的Javadoc API,这似乎是真的,并且包括时间。

      此外,您的实现似乎没有使用列稀疏性(您只有行上的哈希图)。他们这样做了,并且针对整数和双精度进行了优化,就像在 Trove 中的情况一样(但在标准 Java 案例中不是这样,它使用具有相当大开销的对象)。我推荐柯尔特。

      【讨论】:

        猜你喜欢
        • 2011-09-23
        • 2011-01-06
        • 2016-06-07
        • 1970-01-01
        • 2010-12-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多