leetcode-300-最长上升子序列(longest increasing subsequence)-java

题目及测试

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;
	}

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值