我有一个简单的问题来找到数组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。所以,填写地图是最昂贵的操作。
您认为这种问题最有效的方法是什么?
答案 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)
首先,为什么基准不相关:
至于找到一个好的算法 - 您可以使用Map<Integer, Boolean>
而不是Map<Integer, List<Integer>
,因为您只需要存储唯一标志而不是值列表 - 当True
时添加False
你第一次看到元素,当你遇到一个双重性时切换到put
containsKey
,get
/ 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
的特定索引处简单获取元素要复杂得多。