LinkedHashMap的复杂性

时间:2016-05-16 20:13:01

标签: java time-complexity big-o nested-loops linkedhashmap

我有一个简单的问题来找到数组A中的第一个独特元素。但是,困扰我的是使用不同方法的时间复杂度。到目前为止,我已尝试过这两种方法。

第一种方法:

LinkedHashMap<Integer, List<Integer>> map = new LinkedHashMap<Integer, List<Integer>>();
for (int i = 0; i < A.length; i++)
{
    if (!map.containsKey(A[i]))
        map.put(A[i], new ArrayList<>());
    map.get(A[i]).add(i);
}

for (Map.Entry<Integer, List<Integer>> m : map.entrySet())
    if (m.getValue().size() == 1)
        return m.getKey();
return -1;

第二种方法:

    for(int i=0; i< A.length; i++){
        boolean unique = true;
        nestedFor:for(int j=0; j< A.length; j++){
            if(i != j && A[i] == A[j]){
                unique = false;
                break nestedFor;
            }
        }
        if(unique)
            return A[i];
    }
    return -1;

使用1000000个元素的数组进行测试,第一个方法在~2000ms处执行,而第二个方法在~10ms处执行。我的问题是:第一种方法执行得更快,因为它的复杂度是O(nLogn),而第二种方法的复杂度是O(n ^ 2)?我在这里失踪了什么?测试代码下方:

    int[] n = new int[1000000];
    for (int i = 0; i < n.length; i++)
        n[i] = new Random().nextInt(2000000);

    long start = System.currentTimeMillis();
    firstUnique(n);
    System.err.println("Finished at: " + (System.currentTimeMillis() - start ) + "ms");

编辑:

for (int i = 0; i < A.length; i++)
{
    if (!map.containsKey(A[i]))
        map.put(A[i], new ArrayList<>());
    map.get(A[i]).add(i);
}

消耗99%的执行时间,而

for (Map.Entry<Integer, List<Integer>> m : map.entrySet())
    if (m.getValue().size() == 1)
        return m.getKey();

始终是1-3ms。所以,填写地图是最昂贵的操作。

您认为这种问题最有效的方法是什么?

6 个答案:

答案 0 :(得分:2)

我怀疑你没有选择创造了最坏情况的投入&#34;第二种情况的条件。

例如,如果你构造数组使得所有百万个元素都有重复(例如A[i] = 2 * i / A.length),那么第二种方法比第一种方法慢,因为它必须检查{{ 1}}元素的组合。

通过将内部for循环的条件更改为仅10^12进行检查,您可以使其快一点(大约快两倍),但j = i + 1仍然是一个非常大的数字。

如果你只是选择随机数来填充数组,那么第一个元素是唯一的,并且第一个和第二个元素之一更有可能是唯一的,等等。在几个元素之后,你和#39;将近乎确定该元素是唯一的,因此它将在几次迭代后停止。

第一种方法花费的时间太长了。我只能认为你在基准测试之前没有正确地升温你的JIT。但即使没有尝试这样做,你的第一种方法对我来说只需要40-50ms(经过几次迭代后下降到10-15ms)。

大部分时间都是由于对象的创建 - 包括密钥和值的自动装箱以及10^12 / 2实例的创建。

答案 1 :(得分:1)

时间复杂性忽略了系数,因为通常知道函数如何随着输入大小的增加而增长更有用。虽然您的第一个函数具有较低的时间复杂度,但在较小的输入大小下,它会运行得慢得多,因为您正在制作许多ArrayList个对象,而这些对象的计算成本很高。然而,您的第二种方法仅使用数组访问,这比实例化对象便宜得多。

答案 2 :(得分:1)

时间复杂性意味着在其渐近意义上被理解(即,当输入大小增长为googolplex时),而不是别的。如果算法具有线性时间复杂度,那只意味着存在一些a,b使得执行时间(大致!!!)= a * inputsize + b。它没有说明a和b的实际大小,两个线性算法仍然会有很大的性能差异,因为它们的a / b大小差异很大。

(另外,你的例子很差,因为算法的时间复杂性应该考虑所有底层操作的复杂性,例如对象创建等。其他人也在他们的答案中暗示了这一点。)

答案 3 :(得分:1)

考虑使用2套:

public int returnFirstUnqiue(int[] a)
{
  final LinkedHashSet<Integer> uniqueValues = new LinkedHashSet<Integer>(a.length);
  final HashSet<Integer> dupValues = new HashSet<Integer>(a.length);

  for (int i : a)
  {
    final Integer obj = i;
    if (!dupValues.contains(obj))
    {
      if (!uniqueValues.add(obj))
      {
        uniqueValues.remove(obj);
        dupValues.add(obj);
      }
    }
  }

  if (!uniqueValues.isEmpty())
  {
    return uniqueValues.iterator().next();
  }
  return -1;
}

答案 4 :(得分:1)

首先,为什么基准不相关:

  • 即使我们忽略了使用过的方法,GC等引起的不准确性,发现方法2对百万条目的速度更快也不会告诉你它将如何在十亿条目上表现出来
    • Big-O是一个理论概念,必须在理论上得到证明。大多数基准可以为你做的是让你估计复杂性,而不是通过比较一个输入上的两个方法,而是通过比较多个输入上的一个方法,每个方法比前一个大一个数量级(甚至更大)那么几乎不可能得出任何有用的结论)
  • Big-O是一个最坏情况的复杂性,但你的随机输入可能在某个地方&#34;在中间&#34;对于第一种方法(map),虽然它与数组的最坏情况相差甚远 - 实际上它有50%的机会在第一次迭代时成功,而地图必须完全处理,平均有大约五十万个条目
    • &#34; map&#34;最糟糕的情况方法可能是所有元素不同但具有相同的哈希码(因此您需要在n次迭代中的每一次中读取添加元素的完整列表)
    • &#34;阵列的最坏情况&#34;方法是所有元素相等(需要完成整个嵌套迭代)

至于找到一个好的算法 - 您可以使用Map<Integer, Boolean>而不是Map<Integer, List<Integer>,因为您只需要存储唯一标志而不是值列表 - 当True时添加False你第一次看到元素,当你遇到一个双重性时切换到put

  • LinkedHashMap操作containsKeyget / put具有大O复杂度O(n)(最坏情况)使得整个算法O(n ^ 2)
  • 但是,get摊销复杂度为O(1)(使所有插入O(n)的摊销复杂度)和平均值 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(2*diameter, 2*diameter); } 的复杂性是常量(这取决于所使用的哈希函数对给定输入的效果如何);然后,唯一值查找是O(n)

答案 5 :(得分:0)

我的观察: 第二种方法更快,因为它使用Array声明宽度。在第一个例子中,发生了大小的变化。

请尝试定义更准确的LinkedHashMap大小,以将初始容量设置为1000000。

接下来的事情是,Array是一个更简单的结构,GC不会尝试做任何事情。但是当谈到LinkedHashMap时,创建它和操纵的更复杂和成本在某些情况下要比在Array的特定索引处简单获取元素要复杂得多。