繁体   English   中英

LinkedHashMap的复杂性

[英]LinkedHashMap complexity

我有一个简单的问题来找到数组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(n ^ 2)的第二种方法相比,它的复杂度为O(nLogn),因此第一种方法的执行速度是否应该更快? 我在这里想念什么? 测试代码下方:

    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。 因此,是的,填满地图是最昂贵的操作。

您如何建议作为解决此类问题的最有效方法?

我怀疑您没有选择为第二种情况创建“最坏情况”条件的输入。

例如,如果构造数组使所有百万个元素都重复(例如A[i] = 2 * i / A.length ),则第二种方法比第一种方法慢,因为它具有检查元素的10^12组合。

通过更改内部for循环的条件以仅从j = i + 1检查,可以使其速度更快(大约快一倍),但是10^12 / 2仍然是一个很大的数字。

如果您只是选择随机数来填充数组,则第一个元素很有可能是唯一的,而第一个和第二个元素中的一个很有可能是唯一的,依此类推。经过几个元素,您将达到几乎可以肯定该元素是唯一的,因此它将在几次迭代后停止。


第一种方法花费的2秒时间太长。 我只能认为您没有在基准测试之前正确地预热JIT。 但是,即使不尝试这样做,您的第一种方法对我来说也只需要40-50毫秒(经过几次迭代后降至10-15毫秒)。

大部分时间将归因于对象的创建-在键和值的自动装箱以及ArrayList实例的创建中。

时间复杂度忽略了效率系数,因为通常知道函数随着输入大小的增加如何增长会更有用。 尽管第一个函数的时间复杂度较低,但是在较小的输入大小下,它会运行得慢得多,因为您要制作许多ArrayList对象,这在计算上是昂贵的。 但是,第二种方法仅使用数组访问,这比实例化对象要便宜得多。

时间复杂度应从其渐近意义上理解(即,随着输入大小增长到googolplex),仅此而已。 如果算法具有线性时间复杂度,则仅意味着存在一些a,b,使得执行时间(大约!!)= a * inputsize + b。 它没有说出a和b的实际大小,并且两个线性算法仍可能存在巨大的性能差异,因为它们的a / b的大小差异很大。

(此外,您的示例是一个糟糕的示例,因为算法的时间复杂度应考虑所有基础操作(例如对象创建等)的复杂性。其他人的答案也暗示了这一点。)

考虑改用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;
}

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

  • 即使我们忽略了由于使用的方法,GC等引起的不准确性,但发现方法2在100万个条目上的速度更快,并不会告诉您它在10亿个条目上的表现如何
    • Big-O是一个理论概念,必须在理论上加以证明。 基准测试可以为您提供的最大帮助是,您可以估算复杂度,这不是通过比较一个输入上的两种方法,而是通过比较多个输入上的一种方法来完成的,每个输入方法比以前的输入大一个数量级(甚至那么几乎不可能得出任何有用的结论)
  • Big-O是最坏情况下的复杂性,但是对于第一种方法(映射),您的随机输入可能会在“中间”某处,而对于数组来说,它远未达到最坏情况-实际上,它有50%的机会第一次迭代的成功次数,而地图则必须经过全面处理,平均约有100万个条目
    • 对于“ map”方法,最坏的情况可能是所有元素都不同,但哈希码相等(因此,您需要在n次迭代中的每一个中读取已添加元素的整个列表)
    • “数组”方法的最坏情况是所有元素都相等(需要完成整个嵌套迭代)

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

  • LinkedHashMap操作putcontainsKey / get具有大的O复杂度O(n)(最坏的情况),使得整个算法为O(n ^ 2)
  • 但是, put摊销复杂度为O(1)(使所有插入的摊销复杂度为O(n)),而get 平均复杂度是恒定的(这取决于所使用的哈希函数对给定输入的工作程度); 唯一值查找为O(n)

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

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

接下来的事情是Array是简单得多的结构,其中GC不尝试执行任何操作。 但是当涉及到LinkedHashMap时,它比在Array从特定索引处简单获取元素要复杂得多,并且在某些情况下创建和操作的成本要复杂得多。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM