程序员面试100题之十二:求数组中最长递增子序列

写一个时间复杂度尽可能低的程序,求一个一维数组(N个元素)中最长递增子序列的长度。

例如:在序列1,-1,2,-3,4,-5,6,-7中,其最长递增子序列为1,2,4,6。

分析与解法

根据题目要求,求一维数组中的最长递增子序列,也就是找一个标号的序列b[0],b[1],... b[m](0<=b[0]<b[1]<...<b[m]<N),使得array[b[0]]< array[b[1]]<...<array[b[m]]。

解法一

根据无后效性的定义我们知道,将各阶段按照一定的次序排列好之后,对于某个给定阶段的状态来说,它以前各阶段的状态无法直接影响它未来的决策,而只能间接地通过当前状态来影响。换句话说,每个状态都是过去历史的一个完整总结。

同样地,仍以序列1,-1,2,-3,4,-5,6,-7中为例,我们在找到4之后,并不关心4之前的两个值具体是怎样,因为它对找到6并没有直接影响。因此,这个问题满足无后效性,可以使用动态规划来解决。

可以通过数字的规律来分析目标串:1,-1,2,-3,4,-5,6,-7。

使用i来表示当前遍历的位置:

当i=1时,显然,最长的递增序列为(1),序列长度为1.

当i=2时,由于-1<1。因此,必须丢弃第一个值然后重新建立串。当前的递增序列为(-1),长度为1.

当i=3时,由于2>1,2>-1。因此,最长的递增序列为(1,2),(-1,2),长度为2。在这里,2前面是1还是-1对求出后面的递增序列没有直接影响。

依次类推之后,可以得出如下的结论。

假设在目标数组array[]的前i个元素中,最长递增子序列的长度为LIS[i]。那么,

LIS[i+1]=max{1,LIS[k]+1},array[i+1]>array[k],for any k<=i

即如果array[i+1]大于array[k],那么第i+1个元素可以接在LIS[k]长的子序列后面构成一个更长的子序列。与此同时array[i+1]本身至少可以构成一个长度为1的子序列。

根据上面的分析,可以得到如下的代码:

int LIS(int[] array) { int *LIS = new int[array.Length]; for(int i = 0; i < array.Length; i++) { LIS[i] = 1; //初始化默认的长度 for(int j = 0; j < i; j++) //前面最长的序列 { if(array[i] > array[j] && LIS[j] + 1 > LIS[i]) { LIS[i] = LIS[j] + 1; } } } return Max(LIS); //取LIS的最大值 }

这种方法的时间复杂度为O(N^2+N)= O(N^2)。

解法二

显然O(N^2)的算法只是一个比较基本的解法,我们须要想想看是否能够进一步提高效率。在前面的分析中,当考虑第i+1个元素的时候,我们是不考虑前面i个元素的分布情况的。现在我们从另一个角度分析,即当考虑第i+1个元素的时候考虑前面i个元素的情况。

对于前面i个元素的任何一个递增子序列,如果这个子序列的最大的元素比array[i+1]小,那么就可以将array[i+1]加在这个子序列后面,构成一个新的递增子序列。

比如当i=4的时候,目标序列为:1,-1,2,-3,4,-5,6,-7最长递增序列为:(1,2),(-1,2)。那么,只要4>2,就可以把4直接增加到前面的子序列形成一个新的递增子序列。

因此,我们希望找到前i个元素中的一个递增子序列,使得这个递增子序列的最大的元素比array[i+1]小,且长度尽量地长。这样将array[i+1]加在该递增子序列后,便可找到以array[i+1]为最大元素的最长递增子序列。

仍然假设在数组的前i个元素中,以array[i]为最大元素的最长递增子序列的长度为LIS[i]。

同时,假设:

长度为1的递增子序列最大元素的最小值为MaxV[1];

长度为2的递增子序列最大元素的最小值为MaxV[2];

......

长度为LIS[i]的递增子序列最大元素的最小值为MaxV[LIS[i]]。

假如维护了这些值,那么,在算法中就可以利用相关的信息来减少判断的次数。

具体算法实现如代码所示:

int LIS(int array[]) { //记录数组中的递增序列信息 int *MaxV = new int[array.Length + 1]; MaxV[1] = array[0]; //数组中的第一值,边界值 MaxV[0] = Min(array) - 1; //数组中最小值,边界值 int *LIS = new int[array.Length]; //初始化最长递增序列的信息 for(int i = 0;i < LIS.Length; i++) { LIS[i] = 1; } int nMaxLIS = 1; //数组最长递增子序列的长度 for(int i = 1; i < array.Length; i++) { //遍历历史最长递增序列信息 int j; for(j = nMaxLIS; j >=0; j--) { if(array[i] > MaxV[j]) { LIS[i] = j + 1; break; } } //如果当前最长序列大于最长递增序列长度,更新最长信息 if(LIS[i] > nMaxLIS) { nMaxLIS = LIS[i]; MaxV[LIS[i]] = array[i]; } else if(MaxV[j] < array[i] && array[i] < MaxV[j + 1]) { MaxV[j + 1] = array[i]; } } return nMaxLIS; }

由于上述解法中的穷举遍历,时间复杂度仍然为O(N^2)。

解法三
解法二的结果似乎仍然不能让人满意。我们是否把递增序列中间的关系全部挖掘出来了呢?再分析一下临时存储下来的最长递增序列信息。

在递增序列中,如果i<j,那么就会有MaxV[i]<MaxV[j]。如果出现MaxV[j]<MaxV[i]的情况,则跟定义矛盾,为什么?

因此,根据这样单调递增的关系,可以将上面方法中的穷举部分进行如下修改:

for(int j = LIS[i - 1]; j >= 1; j--) { if(array[i] > MaxV[j]) { LIS[i] = j + 1; break; } }

如果把上述的查询部分利用二分搜索进行加速,那么就可以把时间复杂度降为O(N*log2N)。

小结

从上面的分析中可以看出我们先提出一个最直接(或者说最简单)的解法,然后从这个最简单解法来看是否有提升的空间,进而一步一步地挖掘解法中的潜力,从而减少解法的时间复杂度。

在实际的面试中,这样的方法同样有效。因为面试者更加看中的是应聘者是否有解决问题的思路,不会因为最后没有达到最优算法而简单地给予否定。应聘者也可以先提出简单的办法,以此投石问路,看看面试者是否会有进一步的提示。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值