此题我的思路是,先形成一个 num与其频率的 treeMap, 然后每次选定一个左边的num,这样其他两数必在此key的右边,而且其和也被决定了,可用左右两指针互相逼近的方法获得。遍历左边num的时间为O(n), 找右边两数也是O(n),所以时间复杂为O(n2)。应该说这已经是优化过的算法了,然后在实现中https://leetcode.com/submissions/detail/245681661/,却因为我对TreeMap的不熟悉,导致速度并不理想。问题在哪里呢?我一看源代码,以TreeMap的ceilingKey方法为例,我用它来推动最小数和左指针的右移,脑子里想的是有序数组,每移一次仅O(1),但是实际上呢,人家并没有用到中序遍历的后序指针,每次都是从root往下进行二叉搜索,这时间就不是O(1)而是O(log n)了。如此总体时间变成n2 (log n)2.
在https://leetcode.com/submissions/detail/245697101/中我做了改进,不再直接用treeMap来遍历,而是用new ArrayList(entrySet)将它的所有键值对包成一个有序数组,再对这个数组进行遍历。如此时间应该真的是O(n2)了
此前的相关文章:如何对map的entry进行排序
此时我心里还是有疑问,难道TreeMap的遍历也会不是O(n)的吗。于是我在本地做了个实验:
TreeMap<Integer,Integer> map = new TreeMap<Integer,Integer>();
int t = 20000000;
while(--t>=0) {
map.put(t, 1);
}
long t0 = System.currentTimeMillis();
int sum = 0;
List<Integer> sortedKeys = new ArrayList<Integer>(map.keySet());
long t11 = System.currentTimeMillis();
System.out.println(t11-t0);
for (int i = 0; i < sortedKeys.size() ; ++i) {
sum += sortedKeys.get(i);
}
long t1 = System.currentTimeMillis();
System.out.println(t1-t11);
int sum2 = 0;
Set<Integer> keySet = map.keySet();
for(int num : keySet){
sum2 += num;
}
long t2 = System.currentTimeMillis();
System.out.println(t2-t1);
Iterator<Integer> it = map.keySet().iterator();
while(it.hasNext()) {
sum2 += it.next();
}
long t3 = System.currentTimeMillis();
System.out.println(t3-t2);
事实证明直接对keyset进行遍历是线性的,因为如果它是O(n logn), 那么随着数据规模的变大,它对线性O(n)的倍数会增大。而事实上我反复实验,它们基本上成正比。
于是我准备做如下权衡,对treemap进行遍历时,如果不会改动treemap,又是简单的从左到右,则直接遍历。如果会改动treemap,或者要进行更灵活的遍历,比如此题,则用数组包装下对数组遍历吧。。
另外,当对TreeMap既要进行key的遍历,也要进行v的遍历时,应该用entrySet()而不是keySet()。因为TreeMap.get方法的时间复杂度是O(logn),而非O(1)