发现水贴(算法入门经典题目003)
题目
我们有一个ID列表,其中存着某论坛所有帖子的作者ID。
另外,我们已经得到准确情报,在这个长长的列表中,有3个ID,每个出现次数都超过了总数的1/4。这毫无疑问是职业水军干的。
你的任务是快速地找到这3个ID。
可以先考虑列表较短的时候,比如:
1,4,1,1,2,3,3,2,2,3
则所求ID为:1,2,3
分析
最容易想到的方案是:弄一个Map,记录每一个ID的出现次数。
最后再扫描一次,把次数大于总数的1/4的 ID 输出出来。
这个方案的问题在于:当列表较大,并且ID分布比较散的时候,我们这个Map可能很大,甚至是内存装不下。
有没有更节省的方案呢?
如果从实用性的角度讲,我们可以随机抽取若干个位置的ID,比如抽取10000个,从统计学理论(或直觉)可知,这个采样,应该保留了原列表的特征。所以对这个采样进行扫描处理就可以了。
但是不管怎么说,总有人说这个方法不严谨,并且有偷机取巧的意思。。。。
好吧,看看另一个思路。
我们注意到,3个ID发帖总数超过了3/4,也就是说,其他人发的帖子加起来也没有水军中一个人发的多!
于是乎,有人设计一个擂台赛:
擂台上可以同时容纳 3 个人,且开始是空的。
现在,队列中每个选手依次出场。
如果台上有自已人, 就把那个人的生命值加 1
如果台上有空位子,就站上台,且开始的生命数是 1
否则,就把台上 3 个人的生命值都减 1
生命值减到 0 的人死亡,把台上的位置空出来。
…
到最后,剩在台上的 ID,就是所求的 ID
代码
先来个笨办法的。
这在绝大多数场合是足够用的。
//problem003
import java.util.*;
public class A
{
static List<Integer> f(int[] data){
Map<Integer,Integer> map = new HashMap<Integer, Integer>();
for(int x: data){
Integer num = map.get(x);
if(num==null)
map.put(x,1);
else
map.put(x, num+1);
}
List<Integer> res = new ArrayList<Integer>();
for(int x: map.keySet()){
if(map.get(x) > data.length / 4.0) res.add(x);
}
return res;
}
public static void main(String[] args){
int[] data = {
2,3,7,3,2,1,1,2,2,4,
3,1,3,2,3,6,1,1,1,3,
8,3,1,9,2,1,2,5,3,2,
};
System.out.println(f(data));
}
}
下面是高效的算法。
//problem003
import java.util.*;
public class B
{
static Set<Integer> f(int[] data){
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for(int x: data){
if(map.get(x) != null){
map.put(x, map.get(x)+1);
continue;
}
if(map.size()<3){
map.put(x, 1);
continue;
}
for(Iterator<Integer> it=map.keySet().iterator(); it.hasNext();){
int key = it.next();
map.put(key, map.get(key)-1);
if(map.get(key)==0) it.remove();
}
}
return map.keySet();
}
public static void main(String[] args){
int[] data = {
2,3,7,3,2,1,1,2,2,4,
3,1,3,2,3,6,1,1,1,3,
8,3,1,9,2,1,2,5,3,2,
};
System.out.println(f(data));
}
}
这里需要初学java童鞋注意的是:
不能直接在Map的迭代中进行删除键的动作。
必须使用 Iterator,并且只对当前 Iterator 才能remove
详解
如果对为什么这样设计可行还是心里没底,可以在“千聊”上搜这个标题。
或者,手机扫下图,添加关注。
(最好是自已先仔细想想,挺有趣的)