• 图解一道腾讯笔试算法题:「最长上升子序列」


    点击蓝色“五分钟学算法”关注我哟

    加个“星标”,天天中午 12:15,一起学算法

    来源 | 五分钟学算法

    今天分享的题目来源于 LeetCode 第 300 号问题:最长上升子序列。这道题在 腾讯 笔试中出现过 3 次。

    题目描述

    给定一个无序的整数数组,找到其中最长上升子序列的长度。

    示例:

    输入: [10,9,2,5,3,7,101,18]
    输出: 4 
    解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

    说明:

    • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

    • 你算法的时间复杂度应该为 O(n2) 。

    进阶: 你能将算法的时间复杂度降低到 O(nlog n) 吗?

    题目解析

    首先仔细审题,明确题目中的条件。

    1、子序列:不要求连续子序列,只要保证元素前后顺序一致即可;

    2、上升:这里的“上升”是“严格上升”,类似于 [2, 3, 3, 6, 7] 这样的子序列,因为 3 重复了,所以不是“严格上升”的。

    一个序列可能有多个最长上升子序列,题目中只要我们求这个最长的长度。如果使用回溯搜索,选择所有的子序列进行判断,时间复杂度为 ????((2^????)∗????)。

    这个问题具有最优子结构,因此可以考虑使用动态规划完成。

    方法一:动态规划

    定义状态:dp[i] 表示以第 i 个数字为结尾的最长上升子序列的长度。即在 [0, ..., i] 的范围内,选择 以数字 nums[i] 结尾 可以获得的最长上升子序列的长度。注意:以第 i 个数字为结尾,即 要求 nums[i] 必须被选取。反正一个子序列一定会以一个数字结尾,那我就将状态这么定义,这一点是常见的。

    状态转移方程:遍历到索引是 i 的数的时候,我们应该把索引是 [0, ... ,i - 1]dp 都看一遍,如果当前的数 nums[i] 严格大于之前的某个数,那么 nums[i] 就可以接在这个数后面形成一个更长的上升子序列。把前面的 i 个数都看了,dp[i] 的值就是它们的最大值加 1 。即比当前数要小的那些里头,找最大的,然后加 1 。

    于是状态转移方程是:dp(i) = max{1 + dp(j) if j < i and dp[i] > dp[j]}

    最后不要忘了,扫描一遍这个 dp 数组,其中最大值的就是题目要求的最长上升子序列的长度。

    Python 代码:

    class Solution:

        # 将 dp 数组定义为:以 nums[i] 结尾的最长上升子序列的长度
        # 那么题目要求的,就是这个 dp 数组中的最大者
        # 以数组  [10, 9, 2, 5, 3, 7, 101, 18] 为例
        # dp 的值:1 1 1 2 2 3 4    4

        def lengthOfLIS(self, nums):
            size = len(nums)
            # 特判
            if size <= 1:
                return size

            dp = [1] * size
            for i in range(1, size):
                for j in range(i):
                    if nums[i] > nums[j]:
                        # + 1 的位置不要加错了
                        dp[i] = max(dp[i], dp[j] + 1)
            # 最后要全部一看遍,取最大值
            return max(dp)

    Java 代码:

    import java.util.Arrays;

    public class Solution {

        //【关键】将 dp 数组定义为:以 nums[i] 结尾的最长上升子序列的长度
        // 那么题目要求的,就是这个 dp 数组中的最大者
        // 以数组  [10, 9, 2, 5, 3, 7, 101, 18] 为例:
        // dp 的值:1 1 1 2 2 3 4    4
        // 注意实现细节。
        public int lengthOfLIS(int[] nums) {
            int len = nums.length;
            if (len == 0) {
                return 0;
            }
            // 状态的定义是:以 i 结尾的最长上升子序列的长度
            // 状态转移方程:之前比最后那个数字小的最长上升子序列的长度 + 1
            int[] dp = new int[len];
            // 如果只有 1 个元素,那么这个元素自己就构成了最长上升子序列,所以设置为 1 是合理的
            Arrays.fill(dp, 1);
            // 从第 2 个元素开始,逐个写出 dp 数组的元素的值
            for (int i = 1; i < len; i++) {
                int curVal = nums[i];
                // 找出比当前元素小的哪些元素的最小值
                for (int j = 0; j < i; j++) {
                    if (curVal > nums[j]) {
                        dp[i] = Integer.max(dp[j] + 1, dp[i]);
                    }
                }
            }
            // 最后要全部走一遍,看最大值
            int res = dp[0];
            for (int i = 0; i < len; i++) {
                res = Integer.max(res, dp[i]);
            }
            return res;
        }

        public static void main(String[] args) {
            int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
            Solution solution = new Solution();
            int lengthOfLIS = solution.lengthOfLIS(nums);
            System.out.println(lengthOfLIS);
        }
    }

    复杂度分析:

    • 时间复杂度:????(????2)。

    • 空间复杂度:????(????)。

    这道题还有一个时间复杂度为 ????(????log????)的解法,如下。

    方法二:贪心算法(二分法)

    思路:每一次来一个新的数 num,在 tail 数组(tail 数组的定义在下面的示意图中有)中找大于等于 num 的那个数,试图让它变小,以致于新来的数有更多的可能性接在它后面,成为一个更长的“上升子序列”,这是“贪心算法”的思想。

    tail 数组中找大于等于 num 的那个数,可以使用“二分法”

    LeetCode 第 300 题:最长上升子序列-2

    Python 代码:

    class Solution:
        def lengthOfLIS(self, nums: List[int]) -> int:
            size = len(nums)
            if size < 2:
                return size

            tail = []
            for num in nums:
                # 找到大于等于 num 的第 1 个数
                l = 0
                r = len(tail)
                while l < r:
                    mid = l + (r - l) // 2
                    if tail[mid] < num:
                        l = mid + 1
                    else:
                        r = mid 
                if l == len(tail):
                    tail.append(num)
                else:
                    tail[l] = num
            return len(tail)

    Java 代码:

    public class Solution {

        public int lengthOfLIS(int[] nums) {
            int len = nums.length;
            if (len < 2) {
                return len;
            }
            // 这个数组实际上的长度,就是最后所求
            int[] tail = new int[len];
            int end = 0;
            tail[0] = nums[0];
            for (int i = 1; i < len; i++) {
                if (nums[i] > tail[end]) {
                    end++;
                    tail[end] = nums[i];
                } else {
                    // 使用二分搜索法来做这件事情
                    // 数组长度不变,一定会更新一次
                    // 特殊例子:1 2 3 4 5 7 7 7 7 7 7 7 (只是举个例子,这道题不会出现这种情况),来了一个 6
                    // 我要将从左到右边数第 1 个大于 6 的数字更新为 6
                    int left = 0;
                    int right = end;
                    int target = nums[i];
                    while (left < right) {
                        int mid = left + (right - left) / 2;
                        if (tail[mid] < target) {
                            // 只要比目标值要小,我们要找的位置就至少是当前位置 + 1
                            left = mid + 1;
                        } else {
                            assert tail[mid] >= target;
                            // 大于目标值,我们不能盲目向前走,因为向前走很可能,值会变得比目标值小
                            right = mid;
                        }
                    }
                    tail[right] = target;
                }
            }
            return end + 1;
        }
    }

    复杂度分析:

    • 时间复杂度:????(????log????)。

    • 空间复杂度:????(????)。

    有热门推荐????

    1.【程序员】我们就必须承认:这个世界上,有很多问题,就是无解的

    2.【GitHub】我在 GitHub 上看到了一个丧心病狂的开源项目!

    3.【算法】动画:七分钟理解什么是KMP算法

    4.【数据结构】十大经典排序算法动画与解析,看我就够了!

  • 相关阅读:
    使用electron+vue开发一个跨平台todolist(便签)桌面应用
    初识ABP vNext(12):模块的独立运行与托管
    【老孟Flutter】41个酷炫的 Loading 组件库
    【Flutter 实战】酷炫的开关动画效果
    【老孟Flutter】自定义文本步进组件
    【老孟Flutter】6种极大提升Flutter开发效率的工具包
    【Flutter 实战】pubspec.yaml 配置文件详解
    Flutter 1.22版本新增的Button
    【Flutter 混合开发】添加 Flutter 到 iOS
    【Flutter 混合开发】添加 Flutter 到 Android Fragment
  • 原文地址:https://www.cnblogs.com/csnd/p/16675085.html
Copyright © 2020-2023  润新知