zoukankan      html  css  js  c++  java
  • 原创:搜索算法之两个数组取交集的算法

    在垂直搜索中,有很多方法可以控制返回结果的数量。比如用户输入"上海世博会",要求只显示跟上海世博会相关的内容。有三种方法可以参考:①BooleanQuery,AND逻辑②phraseQuery,精读最高,只出现"上海世博会"连续的短语的文档③solr的模糊匹配查询。如果采用第一种方案,在垂直搜索中(比如Lucene),如果用户的查询向量(经由queryParser处理,调用中文分词,并且形成查询语法树)Term t = {xx,xx,……},BooleanQuery为AND时,向量中每一个维度的元素得到对应的倒排列表,倒排列表由许多的倒排索引项构成,然后取其中有交集的文档编号,然后进行排序。其核心思想类似于如下问题:

    现有两个数组:int []data1 = {12,45,65,2,5} int []data2 = {12,5,-8,9},取其中的交集。

    关于这个算法,最主要的精力是放在如何降低时间复杂度上。采取先排序再找交集的算法或者以空间换时间的算法,都不可取,要么时间复杂度高,要么就是空间复杂度高。Lucene源代码里,采用的是先排序,然后定义两个指针开始搜索相同的数字。当容量非常大时,这个算法的性能其实是不太好的。如果采用quickSort,最坏的复杂度是n^2,平均复杂度是nlgn。如果容量超过1千万,mergeSort会好一点,最坏复杂度为nlgn。大量的时间将浪费在排序上。换个思路,对于数字的处理,既然取交集,不妨从整体着手,避免一开始就陷入局部讨论。急于算法的实施,效果反而不好。对于不同的问题,要善于从整体考虑,分析内部的规律。那就要学会观察,类比和迁移,学会演绎推理。对于一个问题的解决,可以从一个类似的比较简单的事物入手,找出规律,然后进行迁移,改进,做近一部的研究。很多程序员都会各种排序算法,比如mergeSort,quickSort,HeapSort and son on。一开始进行排序,可能是程序员的惯性思维。对于这个算法,如果你的第一想法是它的话,说明你在算法上,还有提升空间,思维方式需要改变。解决一个问题,最忌讳的就是思维定势。经验有好处也有坏处。正确的做法是,忘记储备的知识,采用最原始的手段,从研究表象入手,寻找内部的规律,然后用理论验证。其次,进行迁移,演化。单独从排序算法来看的话,如果数据量<1000w的话,quickSort性能会好一些,达到上亿级别的,mergeSort会好一些。如果给你一个海量数据,要求寻找出topK最大值或者最小值来,采用排序当然能解决。因为抛开问题本身,单独来看,mergeSort可能是最好的。但是,对于这个问题,性能却是十分拙劣的。所以说,没有绝对好的算法。抛开应用场景的算法,即使是好的,最后也可能是拙劣的。这个问题的着手点,可以从下面开始:

    对于数字取交集,可以画一个数轴,先从简单的连续型数字入手,然后再迁移到离散型的数字。看下图:

    对于图中的①,A~B,C~D为两个数组的取值范围,交集就是CB部分。如果两个数组中的数字是连续型的,那么,CB就是结果,非常简单。但是,大部分数组是离散型的数字。CB里面的数字,只有一部分是想要的结果。需要对CB进行进一步的处理。很容易想到的是,把AC和BD部分砍掉,对剩余的CB部分进行相同的处理,如图中的②和③。在这个过程中,每次找相同的数字,都是从只有两个数字(取值范围)的集合中寻找,之后两个数组只保留取值范围的交集部分,然后不断循环,大大降低了时间和空间复杂度这个算法本身并不难,但是,如何从两个取值范围的数组里寻找相同的值,(从AB和CD里找),如何判断算法何时收敛,需要耐心地寻找规律,逻辑分类要清晰,经得起各种等价值和边界值的测试,保证算法准确无误,可能要花费一些时间。整理起来,思路大致如下:

    即:1.分别计算两个数组的min和max(取值范围),加入到rangeList 中,然后计算rangeLis中重复的数值,加入到result(list)中;

    2.计算rangeList的取值范围交集,比如[1,20,3,15],两个数组的取值范围交集为[3,15],放在数组中,然后根据这个交集分别去除两个数组中不在此范围内的数值,清空rangeList,清零数组;

    3.重复上述步骤,直到符合终止条件位置。

    从取值范围中寻找相同值以及算法收敛条件:

    寻找相同值的过程中,要注意收敛条件的判断,所以比较好的思路是:把两个取值范围加到一个集合中,再把这四个数字加到set中,分别求和,然后根据set的size大小辅助判断。和值sum1和sum2分别为第一个集合和set的和值。①size == 1:把结果加载到结果集中,算法收敛;

    ②size == 3:说明有一对数字重复。重复的数字分布情况有两种:一是分别分布在两个数组中;二是全部分布在一个数组中,这种情况,直接返回结果,算法收敛;对于第一种情况,sum1 - sum2就是相同的数值,加载到结果集中,继续后面的处理。

    ③size == 2:说明有两组数字重复,分布情况如第一张图的下面部分,2代表矩阵两行相等,0代表矩阵两列相等,1代表一行一列各相等。如果是2的话,直接返回结果,算法收敛;如果是0的话,set本身就是相同的数值。

    ④size == 4:有两种情况,其中一种是两个压缩集合没有交集,在后面的代码中应该增加收敛判断条件,如果有交集,直接进行后面的处理。

    在这个迭代过程中,循环终止的整体条件是:两个数组中有任何一个size == 0或者取值范围的交集倒置。

    通过不断减少数组的元素个数,动态控制迭代次数,迭代次数大大降低,当容量非常大时,会显示出优越的性能。此为目前最优的算法。

    以上是逻辑实现,最重要的还是数据结构,由于在这个过程中,会不断地去除数组中的数值,所以底层采用链式存储的线性表,性能会比较高。

    经过调试后,准确无误,现在上传代码,以供分享:

    package com.txq.test;
    
    import java.util.List;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.HashSet;
    import java.util.Iterator;
    import java.util.Queue;
    import java.util.Set;
    import java.util.concurrent.ConcurrentLinkedDeque;
    /**
     * 两个数组取交集算法,优于先排序后搜索的算法
     * @author XueQiang Tong
     * @date 2017/10/21
     */
    public class IntersectionForPairArray {
    	private List<Integer> inter = new ArrayList<Integer>(4);//存储压缩后的集合	
    	private Set<Integer> s = new HashSet<Integer>(4);//过滤压缩集合中的重复数字
    	private Queue<Integer> arr1 = new ConcurrentLinkedDeque<Integer>();//存储原始数据的队列,链式存储
    	private Queue<Integer> arr2 = new ConcurrentLinkedDeque<Integer>();
    	private List<Integer> result = new ArrayList<Integer>();//结果集
    	private List<Integer> intersec = new ArrayList<Integer>(2);//压缩集合的交集
    	
    	public List<Integer> intersection(int[]ar1,int[]ar2){
    		if(ar1.length == 0 || ar2.length == 0 || ar1 == null || ar2 == null) return result;
    		
    		//1.把数据加载到队列中
    		int len = Math.max(ar1.length, ar2.length);
    		for (int i = 0;i < len;i++){
    			if (i <= ar1.length-1){
    				arr1.add(ar1[i]);
    			}
    			if (i <= ar2.length-1){
    				arr2.add(ar2[i]);
    			}			
    		}
    		while(true) {
    			//2.集合压缩
    			inter.add(Collections.min(arr1));
    			inter.add(Collections.max(arr1));
    			inter.add(Collections.min(arr2));
    			inter.add(Collections.max(arr2));
    			
    			for (int i = 0;i < inter.size();i++){//把压缩后的集合加入到set中
    				s.add(inter.get(i));				
    			}
    			int size = s.size();
    			//下面开始寻找相同的数字
    			
    			if(size == 4){				
    				
    			}
    			//先求和	
    			int sum = computeSum(inter);		
    			int sum1 = computeSum(s);
    			int res = sum - sum1;
    			
    			if (size == 3){				
    				if ((inter.get(0) == inter.get(1)) || (inter.get(2) == inter.get(3))){
    					return result;					
    				}
    				else {					
    					result.add(res);
    					arr1.remove(res);
    					arr2.remove(res);
    				}
    			}
    			if (size == 2) {//有三个元素和两对儿元素重复的情况,收敛情况是两个压缩集合各自重复,三个元素重复的情况其结果是res/2
    				if ((inter.get(0) == inter.get(1)) && (inter.get(2) == inter.get(3))) {
    					return result;					
    				}				
    				else {
    					if((inter.get(0) == inter.get(2)) && (inter.get(1) == inter.get(3))){
    						result.addAll(s);
    						for (int element:s){
    							arr1.remove(element);
    							arr2.remove(element);
    						}						
    					} else {
    						result.add(res/2);
    						arr1.remove(res/2);
    						arr2.remove(res/2);
    					}
    					
    				}
    			}
    			if (size == 1) {
    				result.addAll(s);
    				return result;
    			}
    			//4.计算inter的交集,并分别去除两个集合中不在此范围内的元素			
    			intersec.add(Math.max(inter.get(0),inter.get(2)));
    			intersec.add(Math.min(inter.get(1),inter.get(3)));
    			
    			if (intersec.get(0) > intersec.get(1)) break;//当size == 4并且两个压缩集合没有交集时,到此终止
    			
    			removeElement(arr1);
    			removeElement(arr2);
    			
    			if (arr1.size() == 0 || arr2.size() == 0) break;
    			
    			s.clear();
    			inter.clear();
    			intersec.clear();
    		}
    		return result;	
    		
    	}
    
    	private void removeElement(Queue<Integer> queue) {
    		Iterator<Integer> it = queue.iterator();
    		while (it.hasNext()){
    			int n = it.next();
    			if (n < intersec.get(0) || n > intersec.get(1)) {
    				queue.remove(n);
    			}
    		}
    	}
    
    	private int computeSum(Collection<Integer> col) {
    		int sum = 0;
    		for (int i :col){
    			sum += i;			
    		}
    		return sum;		
    	}	
    	
    }
    

     数据结构,底层就两种,一为顺序存储的散列结构,另一个为链式结构struct。第一种结构,在搜索方面有优势,另一个在存储空间及增删改方面有优势。利用这两种数据结构,结合数据安全(比如CAS算法,多线程)和算法,可以根据业务需求设计出更加复杂的数据结构,比如三叉树,哈夫曼树,红黑树,堆 and so on。平时使用现成的库里的数据结构,比如map,set等等,底层都是基于上述两种结构。不同的结构,有不同的优势。比如,solr内部自置的搜索智能提示功能,数据结构采用三叉树。三叉树的优势是树中有树,能够节省内存空间,但是在查找方面,不及平衡的二叉树。所以,在构建三叉树的时候,采用了折中处理,以提高搜索时间。设计平衡的二叉树,就是要解决时间和空间问题,所以底层数据结构,才采用struct(封装对象属性和指针的类)。

  • 相关阅读:
    简述SharePoint designer 工作流实现用户撤回申请得解决方案,how to revoke/recall application in SharePoint designer workflow
    怎么给SharePoint得视图设置多个过滤条件? How to set multiple complex filter conditions to the SharePoint list view ?
    SharePoint designer workflow给一个hyperlink类型得field赋值, How to set value to a hyperlink field by designer workflow
    [SharePoint]解决用户权限被无缘无故自动删除的问题
    [SharePoint Online]SharePoint Designer无法打开世纪互联版sp online站点得解决方法,报错信息:请安装更新后再重新打开
    [Javascript]利用当前时间生成yyyymmddhhmmss这样的字符串
    SharePoint项目实践中如何实现非打破继承看上去很美的权限控制
    SharePoint如何将使列表不被爬网爬到。
    [SharePoint]javascript client object model 获取lookup 类型的field的值,包括user类型(单人或者多人)的值。how to get the multiple user type/lookup type field value by Javascript client object model
    SharePoint 自定义的列表页面中添加javascript的一个 For循环语句后,该页面就打不开了。
  • 原文地址:https://www.cnblogs.com/txq157/p/5487078.html
Copyright © 2011-2022 走看看