上个月参加腾讯校园招聘的笔试,填空部分有一道题问:计算最长上升子序列的最快算法
的时间复杂度和空间复杂度是多少?
例如序列:{1 4 2 3 7 6 5 7}的最长上升子序列是{1 2 3 6 7},长度为5。
此题的答案是O(nlogn)和O(n),在这之前我只了解n^2的算法,没有看过nlogn的算法,到网上搜索
发现对于该算法的介绍都比较晦涩难懂,因此我决定弄懂它后写一篇blog,详细介绍该算法,让他尽量
容易理解。
为了便于描述,假设序列存放在num数组中。
对于该问题,我们很快就能找到一个简单算法。
定义数组length,lenth[i]表示以下标为i元素结尾的最长上升子序列的长度。
则可以想到如下的转换关系 length[i] = max(length[j])+1,其中0<=j<i并且num[i]>=num[j],用程序实现
如下:
int cal(const int *num ,int size)
{
int *length ,i ,j ,r ,maxl = 1;
length = (int*)malloc(size<<2);
for(i = 0 ;i < size ;i ++)
{
r = 0;
for(j = 0 ;j < i ;j ++)
if(num[j] <= num[i] && length[j] > r)
r = length[j];
length[i] = r + 1;
maxl = r+1 > maxl ? r+1 : maxl;
}
free(length);
return maxl;
}
上述算法的时间复杂度是O(n^2)的,空间复杂度是O(n)
怎么在O(nlogn)的复杂度下求解最长上升子序列问题呢?
我们知道一般O(n^2)的算法,加入二分的思想后大多数都可以优化到O(nlogn),所以如何在上述算法中利用二分是一个关键。
我们定义一个数组minv,minv[i]表示长度为i的子序列中最大元素的值,该数组一定有如下性质:
对于任意i>j ,一定有minv[i] >= minv[j],也就是说minv一定是升序的。
证明:长度为i的上升子序列中的最后一个元素minv[i]必然大于该序列中的任意一个元素,因为i>j,所以一定有minv[i]>minv[j]。
对于题目中的序列,它对应的minv数组为:{1,2,3,5,7}。
假如现在要在原序列的末尾加上一个10,则minv数组变为{1,2,3,5,7,10}。上述过程不难看出来,如果添加一个数x后再求minv数组,
就是在原来的minv数组中查找位置最靠后并且值小于等于x的元素,设该元素的下标为k,则x和长度为k的子序列可以组成长度为k+1的子序列,
然后更新minv[k+1] = min(x ,min[k+1])即可。
由于minv是有序的,因此上面的过程可以利用二分查找,从而使时间复杂度降至O(nlogn),用程序实现如下:
int cal(const int *num ,int size)
{
int i ,len=1 ,*minv;
int left ,right ,mid ,pos;
minv = (int*)malloc(size<<2);
minv[0] = num[0];
for(i = 1 ; i < size ;i ++)
{
left = 0 ;
right = len - 1;
while(left<=right)
{
mid = (left+right)>>1;
if(minv[mid] > num[i])
right = mid - 1;
else
left = mid + 1;
}
pos = right;
if(pos == len-1)
{
minv[len++] = num[i];
}
else
{
minv[pos+1] = num[i] < minv[pos+1] ? num[i] : minv[pos+1];
}
}
free(minv);
return len;
}