有序數組取交集是一個非常常見的問題,也是搜索引擎的核心算法之一,然而,當搜索引擎的數據量很大時,倒排索引會很長,即每個有序數組的長度會很大。按照常見的算法求兩個有序數組交集(同時遍歷兩個數組),時間復雜度為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;
}
