有序数组取交集(搜索算法)的算法优化


  有序数组取交集是一个非常常见的问题,也是搜索引擎的核心算法之一,然而,当搜索引擎的数据量很大时,倒排索引会很长,即每个有序数组的长度会很大。按照常见的算法求两个有序数组交集(同时遍历两个数组),时间复杂度为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的差值的办法,可以大大减小算法计算量,提高效率

 四、抽样检测

  由于难以计算,可以用以下程序来进行抽样检测来比较两种算法的性能,我做过很多次测试,实验结果符合上述结论

  注:以下测试程序是用的随机数,随机数服从均匀分布,所以该测试程序可以改变1)和2)两个条件,不能改变条件3)
     还有,没有考虑到数组内数字重复出现的情况,会导致结果又一点点误差,并不影响整个结论
---------------------------代码分割线-----------------

#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;
}

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM