写一个时间复杂度尽可能低的程序,求一个一维数组(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)。

小结

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

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

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

  1. 程序员面试100题之十五:数组分割

    一.题目概述:有一个没有排序,元素个数为2N的正整数数组.要求把它分割为元素个数为N的两个数组,并使两个子数组的和最接近. 假设数组A[1..2N]所有元素的和是SUM.模仿动态规划解0-1背包问题的 ...

  2. 程序员面试100题之十六:二叉树中两个节点的最近公共父节点

    这个问题可以分为三种情况来考虑: 情况一:root未知,但是每个节点都有parent指针 此时可以分别从两个节点开始,沿着parent指针走向根节点,得到两个链表,然后求两个链表的第一个公共节点,这个 ...

  3. 程序员面试100题之十六:二叉树中两个节点的最近公共父节点(最低的二叉树共同祖先)

    这个问题可以分为三种情况来考虑: 情况一:root未知,但是每个节点都有parent指针 此时可以分别从两个节点开始,沿着parent指针走向根节点,得到两个链表,然后求两个链表的第一个公共节点,这个 ...

  4. 程序员面试100题之十:快速寻找满足条件的两个数

    能否快速找出一个数组中的两个数字,让这两个数字之和等于一个给定的值,为了简化起见,我们假设这个数组中肯定存在至少一组符合要求的解. 假如有如下的两个数组,如图所示: 5,6,1,4,7,9,8 给定S ...

  5. 程序员面试100题之十四:强大的和谐

    实现一个挺高级的字符匹配算法: 给一串很长字符串,要求找到符合要求的字符串,例如目的串:123 1******3***2 ,12*****3 这些都要找出来,其实就是类似一些和谐系统..... 这题的 ...

  6. 程序员面试100题之九:求子数组的最大和

    题目:输入一个整形数组,数组里有正数也有负数.数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和.求所有子数组的和的最大值.要求时间复杂度为O(n). 例如输入的数组为1, -2, 3, ...

  7. 程序员面试100题之十三:求二叉查找树的镜像

    题目:输入一颗二元查找树,将该树转换为它的镜像,即在转换后的二元查找树中,左子树的结点都大于右子树的结点.用递归和循环两种方法完成树的镜像转换. 例如输入:      8 / \ 6   10 /\  ...

  8. 每日一题(79) - 求数组中最长递增子序列

    题目来自编程之美 题目 思路(1) 动态规划(复杂度为n^2) 方程: F[i]:表示以nArr[i]为结尾的最长递增子序列的最大长度. F[i] = Max(F[j]) + 1 && ...

  9. 程序员面试100题之六:最长公共子序列

           题目:如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,则字符串一称之为字符串二的子串.注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中.请编写一个函数,输 ...

最新文章

  1. getDeclaredField(x);与set方法的用法
  2. 云企业网的应用于功能详解
  3. ssm查询一条数据并显示_高亮显示查询数据,其实很简单
  4. 量子位MEET大会报名开启!各领域头部玩家集结,AI年度榜单揭晓,在这里预见智能科技新未来...
  5. linux journalctl 命令 查询systemd init 系统日志
  6. laravel大型项目系列教程(六)之优化、单元测试以及部署
  7. php 一键登录插件,FastAdmin一键管理插件
  8. ES6 里面的 class
  9. linux下配置Java和Go环境
  10. django excel 导出页面_Django Admin中增加导出Excel功能过程解析
  11. Python退出命令-为什么要使用这么多?何时使用?
  12. 动态文本_(302期)【动态】|| 立足相同文本,描绘不同风景 ——工作室开展“同课异构”活动...
  13. SLAM--位姿图优化和PnP求解
  14. 图形学基础|深度缓冲(DepthBuffer)
  15. GoldWave2020注册激活码版下载音频处理制作教程
  16. GB与BIG5内码转换COM原代码
  17. 55-将单链表原地逆置(三种方法)
  18. 利用Excel可视化分析,柱形图、条形图、饼图、复合饼图,圆环图、组合图、漏斗图、地图的操作方法(适合小白)
  19. Avoid mutating a prop directly since the value will be overwritten whenever the parent ...
  20. Redis学习之设置验证密码

热门文章

  1. MQTT 轻量版实例发布,满足更多移动互联场景
  2. 终于有人把Java技术知识面试体系整理出来了,这些文档让你的面试稳如泰山
  3. 在现代引擎游戏中使用正确的渲染打光流程
  4. C盘不够用了怎么办,看我4年未重做系统如何清理出25G的temp磁盘空间?
  5. 我的Go+语言初体验——go【Format】goplus
  6. 使用 Authid Current_User 为调用者授权
  7. ORA-01940:cannot drop a user that is currently connected
  8. vSphere 计算vMotion的迁移原理
  9. 浏览器登陆时纪录自动登陆时限
  10. Gralde 网络代理