最长递增子序列

181 阅读1分钟

「这是我参与2022首次更文挑战的第27天,活动详情查看:2022首次更文挑战

前言

笔者除了大学时期选修过《算法设计与分析》和《数据结构》还是浑浑噩噩度过的(当时觉得和编程没多大关系),其他时间对算法接触也比较少,但是随着开发时间变长对一些底层代码/处理机制有所接触越发觉得算法的重要性,所以决定开始系统的学习(主要是刷力扣上的题目)和整理,也希望还没开始学习的人尽早开始。

系列文章收录《算法》专栏中。

力扣题目链接

问题描述

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

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

  示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 10^4  

进阶:

  • 你能将算法的时间复杂度降低到 O(n log(n)) 吗?

剖析

看到题目,第一反应是:

  1. 直接先封装一个类,字段有关键字和下标,然后重新放入集合中,再进行排序
  2. 后面遍历已经排好序的集合,在遍历过程中进行二分找到比当前关键字大的最小元素同时看是否小标也大,找到的话对本次递增子序的长度(从0开始)加1,没有找到直接退出本次遍历。
  3. 然后根据第二步找到的较大中最小的元素进行重复步骤2。
  4. 在退出本次遍历的时候和当前最大的递增子序的长度(默认为1)比较,进行替换。
  5. 重复2-4步骤,直到遍历完,返回大的递增子序的长度。

可以看到时间复杂度大约为O(n+n * n^2 * logn),效率很低了。

我们可以使用动态规划,我们可以设以i为尾节点即最大的值A,这样至少保证下标是最大的,然后往前找递增子序,如果找到比它小的数字B按正常的思路就是看包括B在内前面的项是否存在递增子序需要一个一个比较,但是可以发现其实B已经检查过“否存在递增子序”了,我们不用再比较了,以此我们可以总结下:

  • 定义dp[i]为每个位置要找的以i为尾节点的“递增子序”数。
  • 可以发现在确定dp[i]的时候前面我们已经算出dp[0...i-1]的值,这样我们只需要i前面的每一个数设为j(0<=j<i),如果大于一个数肯定说明,dp[i]至少为dp[j]+1,所以我们只要找出max(dp[j])+1即为dp[i],因此dp[i]=max(dp[j])+1。

因此时间复杂度为O(n^2),空间复杂度为O(n)。还有优化空间,后面再进行更新。

番外

力扣官方题解还给出了,贪心+二分的解法。就是要对a进行贪心,如果大于当前贪心数组最后一个值就进行放入,小于就找到贪心数组中小于等于a的最大值进行替换,但是不符合题目的要求“子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序”即下标升序子序列,所以可以作为贪心算法的了解。后期笔者会带来专门的贪心算法的题解。

public static int lengthOfLIS1(int[] nums) {
    int len = 1, n = nums.length;
    if (n == 0) {
        return 0;
    }
    int[] d = new int[n + 1];
    d[len] = nums[0];
    for (int i = 1; i < n; ++i) {
        if (nums[i] > d[len]) {
            d[++len] = nums[i];
        } else {
            int l = 1, r = len, pos = 0; // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
            while (l <= r) {
                int mid = (l + r) >> 1;
                if (d[mid] < nums[i]) {
                    pos = mid;
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
            d[pos + 1] = nums[i];
        }
    }
    return len;
}

代码

/**
 * 最长递增子序列
 * https://leetcode-cn.com/problems/longest-increasing-subsequence/
 */
public class LengthOfLIS {
    /**
     * 我们可以使用**动态规划**,我们可以设以i为尾节点即最大的值A,这样至少保证下标是最大的,然后往前找递增子序,如果找到比它小的数字B按正常的思路就是看包括B在内前面的项是否存在递增子序需要一个一个比较,但是可以发现其实B已经检查过“否存在递增子序”了,我们不用再比较了,以此我们可以总结下:
     * - 定义dp[i]为每个位置要找的以i为尾节点的“递增子序”数。
     * - 可以发现在确定dp[i]的时候前面我们已经算出dp[0...i-1]的值,这样我们只需要i前面的每一个数设为j(0<=j<i),如果大于一个数肯定说明,dp[i]至少为dp[j]+1,所以我们只要找出max(dp[j])+1即为dp[i],因此dp[i]=max(dp[j])+1。
     *
     * @param nums
     * @return
     */
    public int lengthOfLIS(int[] nums) {
        int currentMax = 1;

        //存放每个位置的“递增子序”长度
        int[] dp = new int[nums.length];

        // 0下标肯定为1
        dp[0] = 1;
        for (int i = 1; i < nums.length; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                //大于前面的数字所以当前dp[i]为Math.max(dp[i], dp[j] + 1)
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            //最后做替换
            currentMax = Math.max(currentMax, dp[i]);
        }

        return currentMax;
    }
}