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 个元素的数组,第一个方法执行时间约为 2000 毫秒,而第二个方法执行时间约为 10 毫秒。我的问题是:与复杂度为 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。所以,是的,填充地图是最昂贵的操作。
对于此类问题,您认为最有效的方法是什么?
我的观察:
第二种方法要快得多,因为它使用 Array
并声明了宽度。在第一个示例中,发生了大小变化。
请尝试定义更准确的大小 LinkedHashMap
以将初始容量设置为 1000000。
接下来的事情是 Array 是一个更简单的结构,GC 不会尝试做任何事情。但是,当涉及到 LinkedHashMap
时,它的创建和操作成本在某些情况下比简单地从 Array
.
获取特定索引处的元素要复杂得多。
时间复杂度忽略了系数,因为了解函数如何随着输入大小的增加而增长通常更有用。尽管您的第一个函数的时间复杂度较低,但在输入较小的情况下,它会 运行 慢得多,因为您正在制作许多 ArrayList
对象,这在计算上是昂贵的。然而,你的第二种方法只使用数组访问,这比实例化一个对象要便宜得多。
时间复杂度应该从渐近的意义上来理解(即随着输入大小增长到 googolplex),仅此而已。如果一个算法具有线性时间复杂度,那仅意味着存在一些 a,b,使得执行时间(大致 !!!)= a * inputsize + b。它没有说明 a 和 b 的实际大小,并且两个线性算法仍然可能具有巨大的性能差异,因为它们的大小 a/b 差异很大。
(此外,您的示例很糟糕,因为算法的时间复杂度应考虑所有基础操作(例如对象创建等)的复杂度。其他人也在他们的答案中暗示了这一点。)
我怀疑您没有选择为第二种情况创造 "worst case" 条件的输入。
例如,如果您构造的数组使得所有百万个元素都有重复项(例如 A[i] = 2 * i / A.length
),那么第二种方法比第一种方法慢得多,因为它必须检查10^12
个元素组合。
您可以通过将内部 for 循环的条件更改为仅从 j = i + 1
开始检查来使其更快(大约快两倍),但 10^12 / 2
仍然是一个相当大的数字。
如果您只是简单地选择随机数来填充数组,那么第一个元素很可能是唯一的,并且第一个和第二个元素中的一个是唯一的可能性更大,等等。在几个元素之后,您几乎可以确定该元素是唯一的,因此它会在几次迭代后停止。
第一种方法花费的 2 秒太长了。我只能认为您在基准测试之前没有正确预热 JIT。但即使不尝试这样做,你的第一种方法对我来说也只需要 40-50 毫秒(经过几次迭代后下降到 10-15 毫秒)。
大部分时间将归因于对象创建 - 在键和值的自动装箱以及 ArrayList
实例的创建中。
考虑改用 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 在一百万个条目上更快也不会告诉您它在十亿个条目上的表现如何
- Big-O是一个理论概念,必须从理论上证明。大多数基准可以在这里为您做的是让您估计复杂性,这不是通过在一个输入上比较两种方法来完成的,而是通过在多个输入上比较一种方法,每个输入都比前一个大一个数量级(甚至那么几乎不可能得出任何有用的结论)
- Big-O 是最坏情况 的复杂性,但对于第一种方法(映射),您的随机输入可能在某个地方 "in the middle",而它会远非数组的最坏情况——实际上它有 50% 的机会在第一次迭代中成功,而映射必须被完全处理并且平均有大约 50 万个条目
- "map" 方法的最坏情况可能是所有元素都不同但具有相同的哈希码(因此您需要在 n 次迭代的每一次中读取添加元素的整个列表)
- "array" 方法的最坏情况是所有元素都相等(需要完成整个嵌套迭代)
至于找到一个好的算法 - 你可以使用 Map<Integer, Boolean>
而不是 Map<Integer, List<Integer>
因为你只需要存储唯一标志而不是值列表 - 添加 True
第一次看到元素的时候,遇到口是心非的时候切换到False
- LinkedHashMap 操作
put
、containsKey
/get
具有大 O 复杂度 O(n)(最坏情况)使得整个算法 O(n^2)
- 但是,
put
的 分摊复杂度 是 O(1)(使得所有插入的 分摊复杂度 O( n)) 并且 get
的 平均 复杂度是恒定的(这取决于所使用的哈希函数对给定输入的工作情况);唯一值查找则为 O(n)
我有一个简单的问题要找到数组 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 个元素的数组,第一个方法执行时间约为 2000 毫秒,而第二个方法执行时间约为 10 毫秒。我的问题是:与复杂度为 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。所以,是的,填充地图是最昂贵的操作。
对于此类问题,您认为最有效的方法是什么?
我的观察:
第二种方法要快得多,因为它使用 Array
并声明了宽度。在第一个示例中,发生了大小变化。
请尝试定义更准确的大小 LinkedHashMap
以将初始容量设置为 1000000。
接下来的事情是 Array 是一个更简单的结构,GC 不会尝试做任何事情。但是,当涉及到 LinkedHashMap
时,它的创建和操作成本在某些情况下比简单地从 Array
.
时间复杂度忽略了系数,因为了解函数如何随着输入大小的增加而增长通常更有用。尽管您的第一个函数的时间复杂度较低,但在输入较小的情况下,它会 运行 慢得多,因为您正在制作许多 ArrayList
对象,这在计算上是昂贵的。然而,你的第二种方法只使用数组访问,这比实例化一个对象要便宜得多。
时间复杂度应该从渐近的意义上来理解(即随着输入大小增长到 googolplex),仅此而已。如果一个算法具有线性时间复杂度,那仅意味着存在一些 a,b,使得执行时间(大致 !!!)= a * inputsize + b。它没有说明 a 和 b 的实际大小,并且两个线性算法仍然可能具有巨大的性能差异,因为它们的大小 a/b 差异很大。
(此外,您的示例很糟糕,因为算法的时间复杂度应考虑所有基础操作(例如对象创建等)的复杂度。其他人也在他们的答案中暗示了这一点。)
我怀疑您没有选择为第二种情况创造 "worst case" 条件的输入。
例如,如果您构造的数组使得所有百万个元素都有重复项(例如 A[i] = 2 * i / A.length
),那么第二种方法比第一种方法慢得多,因为它必须检查10^12
个元素组合。
您可以通过将内部 for 循环的条件更改为仅从 j = i + 1
开始检查来使其更快(大约快两倍),但 10^12 / 2
仍然是一个相当大的数字。
如果您只是简单地选择随机数来填充数组,那么第一个元素很可能是唯一的,并且第一个和第二个元素中的一个是唯一的可能性更大,等等。在几个元素之后,您几乎可以确定该元素是唯一的,因此它会在几次迭代后停止。
第一种方法花费的 2 秒太长了。我只能认为您在基准测试之前没有正确预热 JIT。但即使不尝试这样做,你的第一种方法对我来说也只需要 40-50 毫秒(经过几次迭代后下降到 10-15 毫秒)。
大部分时间将归因于对象创建 - 在键和值的自动装箱以及 ArrayList
实例的创建中。
考虑改用 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 在一百万个条目上更快也不会告诉您它在十亿个条目上的表现如何
- Big-O是一个理论概念,必须从理论上证明。大多数基准可以在这里为您做的是让您估计复杂性,这不是通过在一个输入上比较两种方法来完成的,而是通过在多个输入上比较一种方法,每个输入都比前一个大一个数量级(甚至那么几乎不可能得出任何有用的结论)
- Big-O 是最坏情况 的复杂性,但对于第一种方法(映射),您的随机输入可能在某个地方 "in the middle",而它会远非数组的最坏情况——实际上它有 50% 的机会在第一次迭代中成功,而映射必须被完全处理并且平均有大约 50 万个条目
- "map" 方法的最坏情况可能是所有元素都不同但具有相同的哈希码(因此您需要在 n 次迭代的每一次中读取添加元素的整个列表)
- "array" 方法的最坏情况是所有元素都相等(需要完成整个嵌套迭代)
至于找到一个好的算法 - 你可以使用 Map<Integer, Boolean>
而不是 Map<Integer, List<Integer>
因为你只需要存储唯一标志而不是值列表 - 添加 True
第一次看到元素的时候,遇到口是心非的时候切换到False
- LinkedHashMap 操作
put
、containsKey
/get
具有大 O 复杂度 O(n)(最坏情况)使得整个算法 O(n^2) - 但是,
put
的 分摊复杂度 是 O(1)(使得所有插入的 分摊复杂度 O( n)) 并且get
的 平均 复杂度是恒定的(这取决于所使用的哈希函数对给定输入的工作情况);唯一值查找则为 O(n)