300. 最长递增子序列

题目:

思路:

【1】动态规划

【2】贪心 + 二分查找

// 贪心的思路,使用结果数组来存储,相同子序列中的最小值
// 如【10,9,2,5,3,7,101,18】
// 本质10,9,2都是只有一个最长递增子序列
// 那么其实10 和 9就没什么用了,为什么呢,
// 因为后面出现的数字与2结合的话更容易出现最长递增子序列
// 如 10 和 3 组成不了 ,但 2 和 3却能组成 , 10 和 18 能组成 ,但 2 和 18也能组成
// 所以此时只需要保留 2 即可,即当最长递增子序列是1的时候(tails数组的下标0),存放2
// 那么既然存放了2 对于 后面的 5 , 3 也是同理的 ,tails数组的下标1,存放3
// 所以本质上相同的最长递增子序列 优先保留最小值,其他的值就没必要保留了
// 这种情况相比于遍历nums,tails过滤了很多没有必要的元素
// (当然对于这个tails数组的遍历还可以用二分来查找,因为它本质上是有序的)
// 所以存完所有的元素,结果数组使用了多少,结果就是多少

 

代码展示:

【1】动态规划

//时间63 ms 击败 33.40%
//内存42 MB 击败 20.15%
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        // 提前返回部分,因为没有或者只有一个的话
        // 最大连续就是他们长度的本身
        if (n < 2) return n;
        // 对每个位置的结果集进行保存
        int[] dp = new int[n];
        // 初始化都为1,因为只算自身的话,长度就是1
        Arrays.fill(dp,1);
        int max = 0;
        // 示例数据:[10,9,2,5,3,7,101,18]
        // 做法是先截取部分,然后扩大
        // 先设定结果集res = [1,1,1,1,1,1,1,1]
        // 然后截取[10,9]部分
        // 由于9<10,所以下标结果不变
        // 再变成截取[10,9,2]
        // 由于2<10和2<9,构不成升序所以结果集不变
        // 再变成截取[10,9,2,5]
        // 由于5<10和5<9,构不成升序所以结果集不变
        // 但由于5>2,构成升序所以结果为 res[3] = res[2] + 1
        // 因为2的位置已经知道升序的个数了,所以在5这个与2组合的话升序个数是要+1的
        // 所以如果存在多个组合形成升序的话5这个下标的结果集只会保留最大组成的个数(满足最长递增子序列)
        // 以此类推
        // 结果集最后为[1, 1, 1, 2, 2, 3, 4, 4]
        // 故最大为4
        for (int i = 1; i < n; i++){
            for (int j = 0; j < i; j++){
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            max = Math.max(max,dp[i]);
        }
        return max;
    }
}

【2】贪心 + 二分查找

//时间3 ms 击败 86.26%
//内存42 MB 击败 14.16%
// 这种是纯贪心的做法
class Solution {
    public int lengthOfLIS(int[] nums) {
        // 贪心的思路,使用结果数组来存储,相同子序列中的最小值
        // 如【10,9,2,5,3,7,101,18】
        // 本质10,9,2都是只有一个最长递增子序列
        // 那么其实10 和 9就没什么用了,为什么呢,
        // 因为后面出现的数字与2结合的话更容易出现最长递增子序列
        // 如 10 和 3 组成不了 ,但 2 和 3却能组成 , 10 和 18 能组成 ,但 2 和 18也能组成
        // 所以此时只需要保留 2 即可,即当最长递增子序列是1的时候(tails数组的下标0),存放2
        // 那么既然存放了2 对于 后面的 5 , 3 也是同理的 ,tails数组的下标1,存放3
        // 所以本质上相同的最长递增子序列 优先保留最小值,其他的值就没必要保留了
        // 这种情况相比于遍历nums,tails过滤了很多没有必要的元素
        // (当然对于这个tails数组的遍历还可以用二分来查找,因为它本质上是有序的)
        // 所以存完所有的元素,结果数组使用了多少,结果就是多少
        int[] tails = new int[nums.length];
        // 记录使用的结果集的数组长度
        int used = 0;
        for(int num : nums) {
            int j = 0;
            for (; j < used; j++){
                if (tails[j] >= num) break;
            }
            tails[j] = num;
            if (j == used) used++;
        }
        return used;
    }
}

//时间2 ms 击败 99.54%
//内存41.7 MB 击败 46.41%
// 这种是在贪心的基础上利用二分查找tails数组进行优化
//(如果tails数组的元素越多优化效果越好,否则其实和遍历是一个层级的)
class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] tails = new int[nums.length];
        // 记录使用的结果集的数组长度
        int res = 0;
        // 示例数据:[10,9,2,5,3,7,101,18]
        // 生成结果集数组tails = [0,0,0,0,0,0,0,0] ,记录使用下标index=0;
        // 当前数字为:10 ,因为index==0 ,然后index++
        // 将数据填入数组中:[10, 0, 0, 0, 0, 0, 0, 0]
        // 当前数字为:9 ,因为index==1,即tails数组不为空,应从tails数组中找出合适的位置(tails 左边界值 i = 0)
        // 进入数组进行二分查找:0
        // 将数据填入数组中:[9, 0, 0, 0, 0, 0, 0, 0]
        // 当前数字为:2 同理得
        // 将数据填入数组中:[2, 0, 0, 0, 0, 0, 0, 0]
        // 当前数字为:5
        // 进入数组进行二分查找:(i + index)/2 = 0 , 由于 2 < 5,所以5此事的最大升序个数为2,应放置于下标1中
        // 将数据填入数组中:[2, 5, 0, 0, 0, 0, 0, 0]
        // 当前数字为:3
        // 进入数组进行二分查找:1
        // 进入数组进行二分查找:0
        // 将数据填入数组中:[2, 3, 0, 0, 0, 0, 0, 0]
        // 以此类推
        for(int num : nums) {
            int i = 0, j = res;
            // 二分查找,找出tails合适的位置
            while(i < j) {
                int m = (i + j) / 2;
                if(tails[m] < num) i = m + 1;
                else j = m;
            }
            // 在合适的位置放置更小的值
            tails[i] = num;
            if(res == j) res++;
        }
        return res;
    }
}
posted @ 2023-06-30 16:41  忧愁的chafry  阅读(18)  评论(0编辑  收藏  举报