CodeTop028 最长递增子序列

最长递增子序列

给你一个整数数组nums,找到其中最长严格递增子序列的长度

先说几句吧,这道题确实适合深入的研究和剖析,今天好好来研究这道题.
首先,最简单的做法就是动态规划,dp[i]表示"在数组nums中i位置的最长子序列的长度"
,因此很简单的动态规划,时间复杂度在O(n^2)

//纯用动态规划来做可以实现O(n^2),dp[i]表示数组i位置(包含i)的最长子序列长度
    public static int lengthOfLIS1(int[] nums){
        if (nums.length==0) return 0;
        int[] dp = new int[nums.length];
        int maxLen = 0;
        dp[0] = 1;
        for (int i=1;i<nums.length;i++){
            dp[i] = 1;//初始化都是长度为1的 毕竟自己就算一个
            for (int j=0;j<i;j++){
                if (nums[j]<nums[i]){
                    dp[i] = Math.max(dp[i],dp[j]+1);
                }
            }
            maxLen = Math.max(maxLen,dp[i]);
        }
        return maxLen;
    }

其次,是使用动态规划+二分,可以实现时间复杂度O(nlogn).这里的dp[i]表示的是"长度为i的子序列末尾元素的最小值"

public static int lengthOfLIS2(int[] nums){
        if (nums.length==0) return 0;
        //维护一个数组dp[i],表示长度为i的最长上升子序列的末尾元素的最小值,用len记录目前最长上升子序列的长度,起始时len为1,dp[1]=nums[0]
        int[] dp = new int[nums.length+1];
        int len = 1;
        dp[1] = nums[0];

        for (int i=1;i<nums.length;i++){
            if (nums[i]>dp[len]){//将nums[i]加入到dp[len]后面
                len++;
                dp[len] = nums[i];
            }else{//利用二分查找找到nums[i]应该插入的位置 即dp[i−1]<nums[j]<dp[i]的下标i,并更新dp[i]=nums[j]
                int left = 1,right = len,pos = 0;//如果找不到 说明所有的数都比nums[i]大,此时要更新dp[1],所以这里将 pos 设为 0
                while (left<=right){
                    int mid = left + ((right-left)>>1);
                    if (dp[mid]<nums[i]){
                        pos = mid;
                        left = mid+1;
                    }else{
                        right = mid-1;
                    }
                }
                //找到了应该放的位置
                dp[pos+1] = nums[i];
            }
        }
        return len;
    }

上面的代码中还有一个比较有用的部分,就是给你一个target,让你在有序数组nums中找到小于target的最大的那个数,也就是target应该出入的位置

public int findnum(int[] nums,int target){
        int left = 0,right = nums.length-1,pos = 0;
        while (left<=right){
            int mid = left+(right-left)>>1;
            if (nums[mid]<target){
                pos = mid;//更新位置 然后一轮一轮的循环 最终逼近到最佳的那个位置
                left = mid+1;
            }else{
                right=mid-1;
            }
        }
        return pos;
    }

现在来看最难的部分了,让你返回这个最长的子序列!!!
方法一:利用,时间复杂度在O(n^2)的纯动态规划进行修改.整一个"List<List< Integer >> ans"用于存放每个位置的最长子序列

//进阶版 要求出那个最长的子序列(用纯动态规划改编)
    public static int lengthOfLIS3(int[] nums){
        if (nums.length==0) return 0;
        int[] dp = new int[nums.length];
        List<List<Integer>> ans = new ArrayList<>();//用于记录每个位置的最长子序列
        int maxLen = 0,index = -1;
        for (int i=0;i<nums.length;i++){
            dp[i] = 1;//初始化都是长度为1的 毕竟自己就算一个
            List<Integer> res = new ArrayList<>();
            res.add(nums[i]);//子序列的初始化都是自己一个元素
            ans.add(res);
            for (int j=0;j<i;j++){
                if (nums[j]<nums[i]){
                    if (dp[i]<dp[j]+1){
                        List<Integer> newres = new ArrayList<>(ans.get(j));//j的路径拿过来
                        newres.add(nums[i]);
                        ans.remove(i);//原来i这个地方的路径就是一个初始化的自己
                        ans.add(newres);//现在换成了newres
                        dp[i] = dp[j]+1;
                    }
                }
            }
            if (maxLen<dp[i]){
                maxLen = dp[i];
                index = i;
            }
        }

        System.out.println(Arrays.toString(ans.get(index).toArray()));
        return maxLen;
    }

方法二:用时间复杂度O(nlogn)的动态规划+二分改编,这里确实是有点绕,但是捋清楚了就不难了.
- 首先dp[i]表示以nums[i]结尾的最长递增子序列长度
- tail[i]表示长度为i的最长子序列的最小结尾数字
这一步你仔细看,其实就是把纯动态规划时候的dp[i]和动态规划+二分时的dp[i]两个全拿过来了.就这是最绕的,只要这两个数组搞明白就简单了.

//进阶版 要求出那个最长的子序列(用动态规划+二分改编)
    //这题是真的逆天,注意注意注意!!!dp[]数组代表的是每个位置的最长子序列长度
    //tail[]数组则表示的是长度为i的子数组的最小尾元素
    public static int lengthOfLIS4(int[] nums){
        if (nums.length==0) return 0;
        int[] dp = new int[nums.length+1];//dp[i]表示以arr[i]结尾的最长递增子序列长度
        int[] tail = new int[nums.length+1];//tail[i]表示长度为i的最长子序列的最小结尾数字
        int len = 0;
        tail[0] = Integer.MIN_VALUE;

        for (int i=0;i<nums.length;i++){

            if (nums[i]>tail[len]){//将nums[i]加入
                len++;
                tail[len] = nums[i];//长度为len的子序列的末尾最小数为nums[i]
                dp[i] = len;//以arr[i]结尾的最长递增子序列长度为i
            }else{//利用二分查找找到nums[i]应该插入的位置 即dp[i−1]<nums[j]<dp[i]的下标i,并更新dp[i] = nums[j]
                int left = 1,right = len,pos = 0;//如果找不到 说明所有的数都比nums[i]大,此时要更新dp[1],所以这里将pos设为0
                while (left<=right){
                    int mid = left + ((right-left)>>1);
                    if (dp[mid]<nums[i]){
                        pos = mid;
                        left = mid+1;
                    }else{
                        right = mid-1;
                    }
                }
                //找到了应该放的位置
                tail[pos+1] = nums[i];//长度为pos+1的子序列的末尾最小数为nums[i]
                dp[i] = pos+1;//以arr[i]结尾的最长递增子序列长度为pos+1
            }
        }

        //dp[]数组记录了每个位置的子序列长度
        //tail[]数组记录了每个长度的最小结尾数字
        int[] ans = new int[len];
        int index = len;
        for (int i=nums.length-1;i>=0;i--){
            if (dp[i] == index){//寻找长度等于index的序列
                ans[index - 1] = tail[index];
                index--;
            }
        }
        System.out.println(Arrays.toString(ans));
        return len;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值