题目及测试
package pid300;
/*Longest Increasing Subsequence
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
*/
public class main {
public static void main(String[] args) {
int[][] testTable = {{10,9,2,5,3,7,101,18},{1,2,3,4},{1,0,1,3,0,4,0,2,0,2},{0,6,2,7,0}};
for (int[] ito : testTable) {
test(ito);
}
}
private static void test(int[] ito) {
Solution solution = new Solution();
int rtn;
long begin = System.currentTimeMillis();
for (int i = 0; i < ito.length; i++) {
System.out.print(ito[i]+" ");
}
System.out.println();
//开始时打印数组
rtn=solution.lengthOfLIS(ito);//执行程序
long end = System.currentTimeMillis();
//System.out.println(ito + ": rtn=" + rtn);
System.out.println("rtn="+rtn );
System.out.println();
System.out.println("耗时:" + (end - begin) + "ms");
System.out.println("-------------------");
}
}
解法1(成功,16ms,较快)
使用动态规划。
状态的定义:以 num[i] 结尾的最长上升子序列的长度。
状态转移方程:之前的数中比 num[i] 小的最长上升子序列的长度 + 1。
对于原数组每个元素,二重循环从头遍历原数组,每当找到一个比当前元素小的值,证明至少可以形成一个dp[j]+1的上升子序列,所以dp[i] = max(dp[i], dp[j] + 1),而dp[j]之前已经求得。
速度o(n^2)
public int lengthOfLIS(int[] nums) {
int length=nums.length;
if(length==0){
return 0;
}
if(length==1){
return 1;
}
// lis为长度为nums中[0,index]的以index为尾巴最长递增子序列的长度
int[] lis=new int[length];
lis[0]=1;
for(int i=1;i<length;i++){
int max=1;
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
max=Math.max(max, lis[j]+1);
}
}
lis[i]=max;
}
int max=0;
for(int i=0;i<length;i++){
max=Math.max(max, lis[i]);
}
return max;
}
解法二(8s,成功,极快):
10,9,2,5,3,7,101,18
首先看到10,加入备选集,备选集合为{10};
之后看到了9,没有形成上升序列,那么9不应该加入备选集合。但是因为9小于10,所以如果把10替换成9会增加接下来产生上升序列的机会,且并不影响备选集合元素的个数(因为是替换),所以替换掉,备选集现在有{9};
遇到2道理同上,替换掉9,备选集变成{2};
遇到5,这时候形成了上升序列,此时应该是添加到备选集合,变为{2,5};
遇到3,没有形成上升序列,但还是道理同加入9的情况,如果此时把5替换成3,会增加接下来形成上升序列的机会,且备选集保持上升,并且个数也没变,所以替换掉5,备选集变成{2,3};
遇到7,同遇到5,添加元素,备选集{2,3,7};
遇到101,同上,备选集{2,3,7,101};
遇到18,还是一样,虽然没有形成上升序列,但是如果把101替换掉,那么接下来形成上升序列的机会会增加,并且备选集的上升属性和元素个数都不变,所以替换,备选集变为{2,3,7,18}。
至此所有元素添加完毕,备选集的元素个数就是最长上升子序列长度。但这里注意,备选集里面的元素并不是最后最长子序列的元素。因为在寻找最长子序列的过程中,目标是尽可能的让以后形成上升序列的机会增加,所以进行了替换。
“人工”做出来之后,只要用程序实现思考过程就好。总结起来就是:
如果遇到的元素比备选集合里面的元素都大,那么就添加进去,使得上升序列长度增加;
如果遇到的元素比备选集合里最后一个元素小,那么代表它无法被添加到备选集。但是为了使后面得到上升序列的机会增加,需要在不破坏集合上升属性和元素总数的情况下,替换掉备选集中的元素,那么就是替换掉大于他的元素中最小的那个,这样才能满足条件。
相当于原来是10,20,30, 现在将15插入,换成10,15,30,更容易插入。
如果下一个是25,变成10,15,25,显然更容易让后一个加入。
如果下一个是40,会变成10,15,30,40,总共4个,但实际排序是10,20,30,40,但是长度一样,所以之前替换为15无妨,因为长度没变。
新加入的元素要比最后一个大,而最后一个元素显然是在新加入元素之前,所以无妨。
例如顺序为10,20,30,15,25,27 会从10,20,30,,替换为10,15,25,最后加入27
这时候,发现备选集一直是保持有序,寻找替换元素的时候就可以用到二分查找,得到O(n log n)的时间复杂度。其中还要注意的是如果元素已经在备选集合中,是不需要任何操作的,因为它并不能增加上升序列的长度,也不会增加之后遇到上升序列的机会,所以直接跳过。
这个做法的精髓是即使用小的元素替换掉中间的元素,备选集的大小不变,还是原来的大小,
public int lengthOfLIS(int[] nums) {
int length=nums.length;
if(length==0){
return 0;
}
if(length==1){
return 1;
}
List<Integer> list = new ArrayList<Integer>();
list.add(nums[0]);
for(int i=1;i<length;i++) {
int now = nums[i];
if(now > list.get(list.size() - 1)) {
list.add(now);
}else {
list.set(getMoreIndex(list, now), now);
}
}
return list.size();
}
/** 找到list中大于等于num的index
* @param list
* @param num
* @return
*/
private int getMoreIndex(List<Integer> list,int num) {
int begin = 0;
int end = list.size() - 1;
while(begin <= end) {
int mid = (begin + end)/2;
if(mid == 0) {
if(list.get(mid) > num) {
return mid;
}
}
if(mid > 0 && list.get(mid) >= num && list.get(mid - 1) < num) {
return mid;
}
if(list.get(mid) >= num) {
end = mid - 1;
}else {
begin = mid + 1;
}
}
return 0;
}