在垂直搜索中,有很多方法可以控制返回結果的數量。比如用戶輸入"上海世博會",要求只顯示跟上海世博會相關的內容。有三種方法可以參考:①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(封裝對象屬性和指針的類)。
