有序数组取交集是一个非常常见的问题,也是搜索引擎的核心算法之一,然而,当搜索引擎的数据量很大时,倒排索引会很长,即每个有序数组的长度会很大。按照常见的算法求两个有序数组交集(同时遍历两个数组),时间复杂度为0(n+m)[n和m为两个数组的长度];如果求多个有序数组的交集,时间复杂度为多个数组长度之和,显然这样的算法代价是很高的,下面介绍一种复杂度更低的算法——分治查找算法。
首先,对于两个数组求交集:有序数组M(长度为m)和有序数组N(长度为n)
分治查找算法的最坏情况下时间复杂度为m+n,最优的情况下为logm * logn
一、算法过程描述如下:
分治的思想,步骤如下:
①找到较短的数组(比如M),在N中用二分查找M[mid](mid=m/2)
②如果m<=0或者n<=0,算法结束
③如果找到M[mid]==N[x]:
把M[mid]放到交集中,
另M=M[0,mid-1],N=N[0,x-1],重复该算法;
另M=M[mid+1,m],N=N[x+1,n],重复该算法;
④如果在N中没有找到M[mid],即存在x,使得N[x]<M[mid]<N[x+1]:
另M=M[0,mid-1],N=N[0,x],重复该算法;
另M=M[mid+1,m],N=N[x+1,n],重复该算法;
二、时间复杂度分析
显然,该分治查找的算法时间复杂度取决于x的值,由于下一次的算法耗时取决于本次x的值,即
下一次的耗时 = 在N[0,x]查找M[1M/4]的耗时 + 在N[x+1,n]查找M[3M/4]的耗时
用数学公式表示一下,即
time(next) = logx + log(n-x) = log(x(n-x))
可以得出
①当x = n/2时,算法每一步的耗时高,即最差的情况
②当x->0或者x->n时,每一步耗时最低,即最好情况
接下来分别对最差情况和最好情况分别进行计算:
①最差情况复杂度整理:

经化简可得:

因为m<n,所以当m=Xn时(经计算X为某个比较接近1的常数),上式取得最大值
但上式最大值仍是n的X1倍(经计算,这个X1为比1大一点点的常数),也就是说,在最坏的情况下(每一步都有x=n/2),改算法时间复杂度为m+n
②最优情况复杂度整理:
最优情况为logm * logn,即有logm个logn复杂度的二分查找
③平均情况:
该算法的平均复杂度比较复杂,
简单来说,受每一步x的取值影响
展开来说,受m和n的差值、两个数组的内数值的取值范围、两个数组的数值概率分布影响
虽然说平均情况比较复杂,但是可以确定的是:
1)m和n相差越大
2)两个数组取值范围交叉越小
3)两个数组的数值概率分布相差越大
该算法平均复杂度越低
三、推广到多数组取交集(搜索引擎算法)
如果是多个有序数组求交集(海量数据的搜索引擎),可以
①先用该算法求出其中长度较小的两个数组的交集A(此时数组A长度应该会更小)
②再用小长度的数组A和接下来最小的数组取交集,得到更小的数组A1
③另A=A1,重复②,直到所有数组都完成
用增大m和n的差值的办法,可以大大减小算法计算量,提高效率
四、抽样检测
由于难以计算,可以用以下程序来进行抽样检测来比较两种算法的性能,我做过很多次测试,实验结果符合上述结论
#include <iostream>
#include <vector>
#include <time.h>
#include <stdlib.h>
#include <algorithm>
using namespace std;
vector<int> ret;//交集数组
int times;
//递归二分查找
//nextDivideEnde 为下次分治第一部分的终点
//nextDivideStart 为下次分治第二部分的起点
void BinSearch(const vector<int> Arry, const int startIndex, const int endIndex, const int key, int &nextDivideEnd, int &nextDivideStart)
{
times += 1;
if(startIndex <= endIndex)
{
times += 1;
int mid = startIndex + (endIndex - startIndex) / 2;
if(key == Arry[mid])
{
ret.push_back(key);
nextDivideEnd = mid;
nextDivideStart = mid;
}
else if(key < Arry[mid])
{
BinSearch(Arry, startIndex, mid-1, key, nextDivideEnd, nextDivideStart);
}
else if(key > Arry[mid])
{
BinSearch(Arry, mid+1, endIndex, key, nextDivideEnd, nextDivideStart);
}
}
else
{
nextDivideStart = startIndex;
nextDivideEnd = endIndex;
}
}
int divideSearch(const vector<int> ArryA, const int AstartIndex, const int AendIndex,
const vector<int> ArryB, const int BstartIndex, const int BendIndex)
{
times += 1;
//BinSearch in the longer one to reduce the complexity
if((AendIndex - AstartIndex) <= (BendIndex - BstartIndex))
{
times += 1;
if(AstartIndex <= AendIndex)
{
int mid = AstartIndex + (AendIndex - AstartIndex)/2;
int nextDivideEnd;
int nextDivideStart;
BinSearch(ArryB, BstartIndex, BendIndex, ArryA[mid], nextDivideEnd, nextDivideStart);
divideSearch(ArryA, AstartIndex, mid-1, ArryB, BstartIndex, nextDivideEnd);
divideSearch(ArryA, mid+1, AendIndex, ArryB, nextDivideStart, BendIndex);
}
}
else//arryB is shorter, then BinSearch mid of B in A
{
times += 1;
if(BstartIndex <= BendIndex)
{
int mid = BstartIndex + (BendIndex - BstartIndex)/2;
int nextDivideEnd;
int nextDivideStart;
BinSearch(ArryA, AstartIndex, AendIndex, ArryB[mid], nextDivideEnd, nextDivideStart);
divideSearch(ArryB, BstartIndex, mid-1, ArryA, AstartIndex, nextDivideEnd);
divideSearch(ArryB, mid+1, BendIndex, ArryA, nextDivideStart, AendIndex);
}
}
}
//to produce the random number in thr range of m and n
int Random(const int m, const int n)
{
int pos, dis;
if(m == n)
{
return m;
}
else if(m < n)
{
pos = m;
dis = n - m + 1;
return rand() % dis + pos;
}
else
{
pos = n;
dis = m - n + 1;
return rand() % dis + pos;
}
}
//common search
int commonSearch(const vector<int> &arryA, const int countA, const vector<int> &arryB, const int countB)
{
int i=0,j=0;
while(i < countA && j < countB)//2 judge: countA and countB
{
times += 2;//2 judge, so +2
if(arryA[i] == arryB[j])
{
ret.push_back(arryA[i]);
times++;
i++;
j++;
}
else if(arryA[i] > arryB[j])
{
times++;
j++;
}
else
i++;
}
}
int main(int argc,char* argv[])
{
if(argc != 7)
{
cout << "please input the countA, minA, maxA, countB, minB, maxB!" << endl;
return -1;
}
vector<int> A;
vector<int> B;
int minA, maxA, minB, maxB;
int countA, countB;
countA = atoi(argv[1]);
minA = atoi(argv[2]);
maxA = atoi(argv[3]);
countB = atoi(argv[4]);
minB = atoi(argv[5]);
maxB = atoi(argv[6]);
srand((int)time(NULL));
for(int i = 0; i < countA; i++)
A.push_back(Random(minA, maxA));
for(int j = 0; j < countB; j++)
B.push_back(Random(minB, maxB));
sort(A.begin(), A.end());
sort(B.begin(), B.end());
divideSearch(A, 0, countA, B, 0, countB);
cout << "ret.size(): " << ret.size() << endl;
cout << "divide search use" << times << " times comparision" << endl;
//now for the common search
ret.clear();//clear() 只是清空vector 并不回收内存 perfect
times = 0;
commonSearch(A, countA, B, countB);
cout << "ret.size(): " << ret.size() << endl;
cout << "common search use" << times << " times comparision" << endl;
return 0;
}
