【算法题集锦之二】--多个集合求交集

来源:互联网 发布:淘宝客户资料购买平台 编辑:程序博客网 时间:2024/06/15 09:36

这是我之前接的一个私活里遇到的一个问题:根据关键字搜索新浪微博,并获取发表这些微博的用户信息,然后再筛选出这些用户的共同关注对象(估计是想根据共同关注对象来投放广告吧),整个项目难度不是很大,利用网络爬虫去抓取微博网页的信息即可(微博提供的API有极大的限制,所以不采用),whatever,这不是重点,本文要讲的还是这个“筛选用户的共同关注用户”------换个说法就是本文的标题了:多个集合求交集。

这个问题说难不难,说容易也不容易,刚好适合我这样的菜鸟。实现的方法有多种,但是我们的目的是寻找出一种较好的算法。

首先,java中的集合框架就提供了相应的方法,即Collection的retainAll方法,我查看了一下它的源码:如下

public boolean retainAll(Collection<?> c) {        boolean modified = false;        Iterator<E> it = iterator();        while (it.hasNext()) {            if (!c.contains(it.next())) {                it.remove();                modified = true;            }        }        return modified;    }
原理很简单,遍历集合的元素,查看此元素是否在另一个集合里存在,如果不存在,则移除,遍历完后该集合就只剩下交集了。

好了,有了这个方法,求多个集合的交集就简单了,只需让一个集合依次和其他集合retainAll,这个集合剩余的数就是交集的数了。

代码如下:

int setNum = 20; // 集合总数int setSize = 1200000; // 每个集合包含的数字数量int randomRange = 2000000; // 生成随机数的范围// 用set保存Set<Integer>[] setArray = new HashSet[setNum];// 随机生成数据for(int i = 0; i < setNum; i ++ ){Set<Integer> set = new HashSet<Integer>();for(int j = 0; j < setSize; j ++){/** * 随机生成 randomRange 内的数 * 每个set存放 setSize 个数 */int randomNum = new Random().nextInt(randomRange);// 保证不重复while(set.contains(randomNum))randomNum = new Random().nextInt(randomRange);set.add(randomNum);}setArray[i] = set;}/** * 方案一:使用jdk自身提供的retainAll方法 */long beginTime = System.nanoTime();Set<Integer> referSet = new HashSet<Integer>();referSet.addAll(setArray[0]);for(int i = 1; i < setNum; i ++){referSet.retainAll(setArray[i]);}long endTime = System.nanoTime();System.out.println("使用retainAll方法,运行时间:" + (endTime - beginTime));Iterator<Integer> iter = referSet.iterator();System.out.println("交集里的数有" + referSet.size() + "个。如下:");while(iter.hasNext()){System.out.print(iter.next() + " ");}
运行结果如下:

运行时间差强人意,20个包含120万数据的集合求交集用retainAll方法总共0.37秒,如果数据再多一点,还有可能出现java.lang.outOfMemoryError....。(这段代码运行时间会比较长,主要是生成随机数花费时间多)。

循环调用retainAll并不是一个很好的选择,因为用于参照的那个集合(即代码中的referSet)每次调用retainAll都要从头遍历,注意到多个交集的集合必定是任一个集合的子集,所以只需遍历一个集合,查看这个集合里的数在剩余的集合存不存在,如果剩余的任一集合不包含这个数,那肯定不属于交集,这样就可以减少判断的次数。

所以上面的代码可以这样优化:

int setNum = 20; // 集合总数int setSize = 1200000; // 每个集合包含的数字数量int randomRange = 2000000; // 生成随机数的范围// 用set保存Set<Integer>[] setArray = new HashSet[setNum];// 随机生成数据for(int i = 0; i < setNum; i ++ ){Set<Integer> set = new HashSet<Integer>();for(int j = 0; j < setSize; j ++){/** * 随机生成 randomRange 内的数 * 每个set存放 setSize 个数 */int randomNum = new Random().nextInt(randomRange);// 保证不重复while(set.contains(randomNum))randomNum = new Random().nextInt(randomRange);set.add(randomNum);}setArray[i] = set;}/** * 方案二: 以第一个集合作为参照集合,遍历之, * 依次与剩余集合比较,如果剩余的set集合里中任意一个set都不包含这个数, * 那么可以断定这个数一定不属于交集 */long beginTime = System.nanoTime();Set<Integer> referSet = new HashSet<Integer>();referSet.addAll(setArray[0]);iter = referSet.iterator();// 遍历这个集合while(iter.hasNext()){Integer i = iter.next();boolean belongToSection = true;// 依次与剩余集合比较for(int index = 1; index < setNum; index ++){Set<Integer> set = setArray[index];// 如果剩余的set集合里中任意一个set都不包含这个数,// 那么可以断定这个数一定不属于交集if(!set.contains(i)){belongToSection = false;break;}}// 移除这个不属于交集的数if(!belongToSection)iter.remove();}// 当遍历完第一个集合时,里面剩余的数就是交集里的数long endTime = System.nanoTime();System.out.println();System.out.println("使用遍历筛选方法,运行时间:" + (endTime - beginTime));iter = referSet.iterator();System.out.println("交集里的数有" + referSet.size() + "个。如下:");while(iter.hasNext()){System.out.print(iter.next() + " ");}
运行结果如下:

唔····貌似有点进步了,花费时间提升了大概0.1秒··,但这个结果还是不能让人满意,如果换成占用内存较大的对象,那么妥妥的内存要爆。有没有更优的办法呢···当然了,不然我也不写这篇博客了。。。

这个想法也是从我的一个面试题中得到的灵感,即如何从40亿个数中判断一个数存不存在,40亿个整形数大概占用内存1G左右,一次性读取进内存是不可取得,所以要想办法压缩整形的占用空间------即用一个二进制位表示一个数。

如何用一个二进制位表示一个整数呢?举个栗子就明白了:

比如1001001这个二进制数所表示的数集就为{1,4,7}-------因为这个二进制数的第1位和第4位和第7位为1,所以压缩的思想就是,根据二进制数的某一位是否为1来表示这个数是否存在,第一位为1就说明1存在数集里,第2位为0就表示2不存在数集里,java也提供了相应的工具类--即BitSet。

这样一个数集就可以用一个二进制数来表示了,而且也不用怕出现内存不够用的情况了(相比整形最多压缩了31倍的空间)

用二进制数表示数集后,求交集简直就是太简单了,直接用两个表示数集的二进制数做AND操作就行了,得到的二进制数就表示了交集。

还是举个栗子:

例如求s1:{1, 2, 4, 8, 10, 11, 20}和s2:{3, 8, 10, 11, 15, 17, 20}的交集
这两个用二进制数表示即为
s1:011010001011000000001 
s2:000100001011000101001;
s1 AND s2 = 000000001011000000001;
这个集合表示数集{8, 10, 11, 20};

下面是代码:

int setNum = 20; // 集合总数int setSize = 1200000; // 每个集合包含的数字数量int randomRange = 2000000; // 生成随机数的范围// 用bitSet保存BitSet[] bitSetArray = new BitSet[setNum];// 随机生成数据for(int i = 0; i < setNum; i ++ ){BitSet bitSet = new BitSet(randomRange);for(int j = 0; j < setSize; j ++){/** * 随机生成 randomRange 内的数 * 每个set存放 setSize 个数 */int randomNum = new Random().nextInt(randomRange);// 保证不重复while(set.contains(randomNum))randomNum = new Random().nextInt(randomRange);// 把bitset对应的位设为truebitSet.set(randomNum, true);}bitSetArray[i] = bitSet;}/** * 方案三 * 使用bitSet查找,只需要用一个bitSet依次与剩余的做逻辑与(AND)操作即可。 * 例如求s1:{1, 2, 4, 8, 10, 11, 20}和s2:{3, 8, 10, 11, 15, 17, 20}的交集 * 这两个用bitSet表示即为 * s1:011010001011000000001  * s2:000100001011000101001; * s1 AND s2 = 000000001011000000001; * 这个集合表示数集{8, 10, 11, 20}; * 用bitSet来存储可以压缩存储空间,一位即可表示一个数字,且求交集简单,运算速度快, * 缺点是当集合的数比较散列时(即不是集中在某一个范围),则会占用比较多的空间。 * 计算后的bitSet即保存了交集的结果---即为true的位所对应的数。 */long beginTime = System.nanoTime();BitSet resultSet = bitSetArray[0];// 用第一个bitSet依次与剩下的bitSet做逻辑与操作for(int i = 1; i < setNum; i ++){resultSet.and(bitSetArray[i]);}long endTime = System.nanoTime();System.out.println();System.out.println("使用bitSet方法,运行时间:" + (endTime - beginTime));System.out.println("交集里的数有" + resultSet.cardinality() + "个。 如下:");for(int i = 0; i < resultSet.size(); i ++){if(resultSet.get(i))System.out.print(i + " ");}}
运行结果如下:


可以看出,运行时间直接提升了2个数量级···而且也不会出现java.lang.outOfMemoryError了。



0 1
原创粉丝点击