LeetCode - 300 最长递增子序列

news2024/10/7 4:31:34

目录

题目来源

题目描述

示例

提示

题目解析

算法源码


题目来源

300. 最长递增子序列 - 力扣(LeetCode)

题目描述

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

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

示例

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

提示

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

题目解析

首先本题描述中说明了递增子序列是可以不连续的,因此本题无法使用双指针来处理。双指针一般只可以求解连续递增子序列。

本题需要使用动态规划的思维,将大问题划分为相同子问题,从子问题中寻找规律。

比如我们需要求下面nums的最长递增子序列,

nums = [10,9,2,5,3,7,101,18]

首先缩小数据规模,先求

nums = [10] 的最长递增子序列,发现就是10

接着扩大数据规模,求

nums = [10,9 ] 的最长递增子序列,新加入元素9,则9尝试和10组合,发现不是严格递增,因此无法组合,因此该子问题得最长递增子序列是9

继续扩大数据规模,求

nums = [10,9,2] 的最长递增子序列,新加入元素2,分别尝试和9子序列合并,和10子序列组合,发现无法形成严格递增,因此无法组合,最终最长递增子序列是2

继续扩大数据规模,求

nums = [10,9,2,5] 的最长递增子序列,新加入元素5,分别尝试和9子序列,10子序列,2子序列组合,发现可以和2子序列形成严格递增,因此得到最长递增子序列为2 5

继续扩大数据规模,求

nums = [10,9,2,5,3]的最长递增子序列,新加入元素3,分别尝试和9子序列,10子序列,2子序列,2 5子序列组合,发现可以和2子序列形成严格递增,因此得到最长递增子序列为2 3

.....

按此逻辑,直到求解道nums原来得数据规模。

我们定义一个dp数组,dp[i]元素值得含义是:以nums[i]结尾的严格递增子序列最大长度,dp数组初始化时,每个元素的初始值都是1,因为严格递增子序列至少有一个nums[i]元素,即序列长度至少为1。

按照上面子问题分解的逻辑,我们可得:

dp[0] = 1

dp[i] = 下面求解过程的最大值

if(num[i] > nums[i-1])

        dp[i] 可能取值 dp[i-1] + 1,如果 dp[i-1] + 1 > dp[i] 的话

if(num[i] > nums[i-2]) 

        dp[i] 可能取值 dp[i-2] + 1,如果 dp[i-2] + 1 > dp[i] 的话

.....

if(num[i] > nums[0])

        dp[i] 可能取值 dp[0] + 1,如果 dp[0] + 1 > dp[i] 的话

最终代码实现如下

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    const dp = new Array(nums.length).fill(1)

    for(let i = 1; i < nums.length; i++) {
        for(let j = 0; j < i; j++) {
            if(nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1)
            }
        }
    }

    return Math.max.apply(null, dp)
};

上面动态规划的算法时间复杂度差不多是O(n^2),因此面对1 <= nums.length <= 2500数据规模,也能有上百万次循环。

那么是否存在优化的可能呢?

我们可以发现nums = [10,9,2]这个子问题的有三个递增子序列,分别是10子序列、9子序列以及2子序列。

那么这三个子序列哪个是最优的呢?

所谓最优,即当我为nums = [10,9,2]追加一个数据,那么上面三个子序列哪个是最有可能和追加的数组合在一起,形成一个长度为2的递增子序列呢?

答案是2子序列,因为2 < 9 < 10,如果追加的数可以和9或10组合形成递增子序列,那么一定可以和2组合形成递增子序列。

因此2子序列是是长度为1的最优子序列。

接下来再追加一个数据3,nums = [10,9,2,5,3]

那么哪个子序列是长度为2的最优子序列呢?

首先长度为2的子序列已经有了一个2 5,当追加一个数据3进来后,发现2 3形成的长度为2子序列的子序列更优。因此此时长度为2的最优子序列是2 3。

nums = [10,9,2,5,3,7,101,18]

按照此逻辑,我们可以求得

  • 长度为1的最优子序列为2
  • 长度为2的最优子序列为2 3
  • 长度为3的最优子序列为2 3 7
  • 长度为4的最优子序列为2 3 7 18

其实最优子序列的特性是由子序列的最后一位绝对的,而前面的数据无关,因此我们可以将上面总结改为

  • 长度为1的最优子序列的尾数为2
  • 长度为2的最优子序列的尾数为3
  • 长度为3的最优子序列的尾数为7
  • 长度为4的最优子序列的尾数为18

我们可以重新定义dp数组,dp[i]的含义就是长度为i+1的最优子序列的尾数值。

因此dp数组的长度即为最长递增子序列的长度。

如果大家对于上面逻辑还是不懂,这里还有一个生动的例子可以帮助理解:

即有一组牌 [10,9,2,5,3,7,101,18] ,可以分为多组,但是每组的下面的牌必须比上面的牌大,因此分牌过程如下

首先,取出第一张牌10,放到一组中

接着取出9,发现比10小,因此可以放在10上,此时第一组最上面牌变为9,

接着取出2,发现比9小,因此可以放在9上,此时第一组最长面牌变为2

接着取出5,发现第一组最长的2<5,因此5需要另起一组

接着取出3,发现比第一组2大,因此尝试放道第二组,发现比5小,因此可以放

同理处理7,101,18

 可以发现一共分成了4组,而这就是我们需要求得递增子序列的最长长度。

而这个分牌逻辑其实就是耐心排序,耐心排序同样适用于前面求每个固定长度下,最优子序列的尾数的逻辑,因此算法实现如下

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function (nums) {
  const dp = [nums[0]];

  for (let i = 1; i < nums.length; i++) {
    if (nums[i] > dp[dp.length - 1]) {
      dp.push(nums[i]);
      continue;
    }

    for (let j = 0; j < dp.length; j++) {
      if (nums[i] <= dp[j]) {
        dp[j] = nums[i];
        break;
      }
    }
  }

  return dp.length;
};

我们发现,虽然算法的性能得到大幅的提升,但是算法的时间复杂度好像还是O(n^2)。

此时,我们需要注意到上面算法的内层for循环遍历的dp数组其实是一个升序的,如下图所示

而内层for想要完成的功能是,找到第一个比nums[i]大的dp[j],然后将nums[i]作为dp[j]的新值。

而dp本身是有序的,因此我们可以使用二分查找来替代当前的顺序查找,因为二分查找的时间复杂度是O(logN),而顺序查找的时间复杂度是O(n)。

由于JS没有实现二分查找,因此需要我们手动实现,而Java在Arrays.binarySearch中实现了二分查找,我们可参考其源码实现JS版的二分查找

function binarySearch(arr, key, from=0, to=arr.length) {
    let low = from
    let high = to - 1

    while(low <= high) {
        let mid = (low + high) >>> 1
        let midVal = arr[mid]

        if(midVal < key) {
            low = mid + 1
        } else if(midVal > key) {
            high = mid - 1
        } else {
            return mid
        }
    }
    return -(low + 1)
}

上面二分查找的逻辑其实很简单,每次找到范围[start, end]内中间值midVal,和目标值key比较,若相同则返回,若不同:

  • key > midVal,则说明目标值不可能在[start, mid],只可能在[mid+1, end]中,因此查找范围缩小为[mid+1, end];
  • ket < midVal,则说明目标值不可能在[mid, end],只可能在[start, mid-1]中,因此查找范围缩小为[start, mid-1];
  • key === midVal,说明找到了目标值得位置,就是mid

如果一直找到start > end时,还没有发现和key相等得midVal,则说明数组中不存在key。

而上面源码中,返回了 -(low+1) 作为找不到时的binarySearch返回值, -(low+1)的含义是啥呢?

而二分查找,其实就是通过双指针low,high移动,来不断缩小目标值得搜索范围,如果

  • 如果:目标值 < 中间值,则high指针左移,变为high = mid-1,如果之后一直是目标值 < 中间值,则high会不断左移,直到high < low,此时low的位置,目标值插入数组后依旧保持数组有序的位置
  • 如果:目标值 > 中间值,则low指针右移,变为low = mid+1,如果之后一直是目标值 > 中间值,则low会不断右移,直到low > high,此时low的位置,目标值插入数组后依旧保持数组有序的位置

由于low>=0,因此如果binarySearch在找不到值得情况下,直接返回low,那么会误导使用者,认为low得位置就是目标值在数组中得位置,为了区别,我们需要让找不到的返回值位置为负数,而low可能为0,因此为了避免出现-0的情况,返回了-(low+1)。

因此,我们可以根据binarySearch找不到目标时的返回值idx,

推导出目标值的有序插入位置:-idx-1

算法源码

/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function (nums) {
  const dp = [nums[0]];

  for (let i = 1; i < nums.length; i++) {
    if (nums[i] > dp[dp.length - 1]) {
      dp.push(nums[i]);
      continue;
    }

    if (nums[i] < dp[0]) {
      dp[0] = nums[i];
      continue;
    }

    const idx = binarySearch(dp, nums[i])
    if(idx < 0) dp[-(idx+1)] = nums[i]
  }

  return dp.length;
};

// 参考Java的Arrays.binarySearch实现
function binarySearch(arr, key, from=0, to=arr.length) {
    let low = from
    let high = to - 1

    while(low <= high) {
        let mid = (low + high) >>> 1
        let midVal = arr[mid]

        if(midVal < key) {
            low = mid + 1
        } else if(midVal > key) {
            high = mid - 1
        } else {
            return mid
        }
    }
    return -(low + 1)
}

这里二分查找的性能优化似乎没有显现出来, 可能是因为dp数组的数据量太小了,随着dp数组的数据量变大,O(logN)的时间复杂度的优势会越来越明显。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/20059.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows进程间利用管道通信

实验一 一、实验内容或题目&#xff1a; 在父进程中创建一个子进程&#xff0c;并建立一个管道&#xff0c;子进程向管道中写入一个字符串&#xff0c;父进程从管道中读出字符串。 二、实验目的与要求&#xff1a; 利用CRT相关接口&#xff0c;学习在父子进程间实现管道通信…

酒水商城|基于Springboot实现酒水商城系统

作者主页&#xff1a;编程千纸鹤 作者简介&#xff1a;Java、前端、Pythone开发多年&#xff0c;做过高程&#xff0c;项目经理&#xff0c;架构师 主要内容&#xff1a;Java项目开发、毕业设计开发、面试技术整理、最新技术分享 收藏点赞不迷路 关注作者有好处 项目编号&…

【Java八股文总结】之集合

文章目录Java集合一、集合概述1、List、Set、Queue、Map的区别&#xff1f;2、Collections和Collection的区别&#xff1f;3、集合和数组的区别二、List1、ArrayList和LinkedList的区别&#xff1f;2、ArrayList和Vector的区别3、Vector、ArrayList和LinkedList的区别4、ArrayL…

Echarts:简单词云图实现

Echarts是一个开源的可视化图表库&#xff0c;支持丰富的图表&#xff0c;官网中还有大量示例可以选择使用、参考。 其中比较好玩、有趣的是词云&#xff0c;词云就是用关键词组成的一朵云&#xff0c;更广泛的定义是&#xff0c;由关键词组成的任意一种图案均称为词云。因此&…

[附源码]java毕业设计社区空巢老人关爱服务平台

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【服务器】无法进行ssh连接的问题逐一排查以及解决方法

一、检查服务器网络 先检查是否是网络的问题。按快捷键WinR&#xff0c;在弹出的对话框中输入cmd。 点击确定运行。在cmd窗口输入ping一下服务器的ip地址。 如果出现请求超时&#xff0c;解决办法如下&#xff1a; 在服务器端输入ifconfig命令&#xff0c;查看要连接的网络的…

[计算机毕业设计]知识图谱的检索式对话系统

前言 &#x1f4c5;大四是整个大学期间最忙碌的时光,一边要忙着准备考研,考公,考教资或者实习为毕业后面临的就业升学做准备,一边要为毕业设计耗费大量精力。近几年各个学校要求的毕设项目越来越难,有不少课题是研究生级别难度的,对本科同学来说是充满挑战。为帮助大家顺利通过…

NX二次开发-调内部函数SEL_set_type_filter_index_by_label设置类型过滤器例子剖析怎么查找内部函数调用内部函数

NX二次开发-调内部函数SEL_set_type_filter_index_by_label设置类型过滤器例子剖析怎么查找内部函数调用内部函数 前言 给那些不会调内部函数的人,一个学习方法,大概知道怎么找内部接口,怎么调用内部函数的。 复杂的东西我也不会,等我研究出来了,在更新到博客上。 版本…

业务级灾备架构设计

同城多中心架构 同城双中心基本架构 关键特征&#xff1a; 相同城市&#xff0c;相距50km以上光纤互联机房间网络延时<2ms 同城双中心架构本质 同城双中心可以当做一个逻辑机房可以应对机房级别的灾难 同城双中心应用技巧-多光纤通路 同一集群&#xff0c;部署在同城两个…

异常~~~

异常 异常体系 编译时异常和运行时异常的区别 Java中的异常被分为两大类&#xff1a;编译时异常和运行时异常&#xff0c;也别成为受检异常和非受检异常 所有的RuntimeException类及其子类被称为运行时异常&#xff0c;其他的异常都是编译时异常 编译时异常&#xff1a;必须…

怎么在bios里设置光驱启动 bios设置光驱启动图文教程

大部分主板都是在开机以后按DEL键进入BIOS设置。 第一部分&#xff1a;学会各种bios主板的光驱启动设置&#xff0c;稍带把软驱关闭掉。 图1&#xff1a; 图2&#xff1a;光驱启动设置 。 图3&#xff1a;回车后要保存退出 。 图4&#xff1a;提示用户&#xff0c;必须选择“…

机器学习:BP神经网络

神经网络人工神经网络的结构特点人工神经网络单层神经网络双层神经网络多层神经网络BP神经网络通过TensorFlow实现BP神经网络单层感知网络是最初的神经网络&#xff0c;具有模型清晰、结构简单、计算量小等优点&#xff0c;但是它无法处理非线性问题。BP神经网络具有任意复杂的…

性能工具之前端分析工Chrome Developer Tools性能标签

文章目录一、前言二、第一部分三、第二部分四、第三部分五、第四部分六、小结一、前言 之前本博曾经写过几篇和前端性能分析相关的文章&#xff0c;如下&#xff1a; 常见性能工具一览性能工具之常见压力工具是否能模拟前端&#xff1f;性能工具之前端分析工Chrome Developer…

【Pytorch with fastai】第 19 章 :从零开始的 fastai 学习者

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

AutoDWG 文件属性编辑修改控件/Attribute Modifier-X

AutoDWG 文件属性编辑修改控件组件/AttributeX 控件组件 属性控件组件是开发者无需AutoCAD就可以直接在dwg文件中提取或修改属性值的组件。 主要特征&#xff1a; 从 dwg 文件中提取属性值。 直接在dwg文件中添加或修改属性值。 支持 R9 到 2016 版本的 DWG 和 DXF。 独立于 A…

如何学习Python技术?自学Python需要多久?

前言 Python要学习的时间取决于学习方式&#xff1a;自学至少需要学8个月;报班的话&#xff0c;可能需要4个月左右的时间。如果想具体了解Python要学多久&#xff0c;那不妨接着往下看吧! &#xff08;文末送读者福利&#xff09; 这个问题分为两种情况&#xff0c;分为自学和…

机械设计基础总复习

《机械设计基础》 一、简答题 1. 机构与机器的特征有何不同&#xff1f; 机器的特征&#xff1a;&#xff08;1&#xff09;人为机件的组合&#xff1b;&#xff08;2&#xff09;有确定的运动&#xff1b;&#xff08;3&#xff09;能够进行能量转换或代替人的劳动。 机构…

攻防世界Encode

Encode 题目描述&#xff1a;套娃&#xff1f; 题目环境&#xff1a;https://download.csdn.net/download/m0_59188912/87094879 打开timu.txt文件&#xff0c;猜测是rot13加密。 Rot13在线解码网址&#xff1a;https://www.jisuan.mobi/puzzm6z1B1HH6yXW.html 解密得到&#x…

跟艾文学编程《Python基础》(1)Python 基础入门

作者&#xff1a; 艾文&#xff0c;计算机硕士学位&#xff0c;企业内训讲师和金牌面试官&#xff0c;现就职BAT一线大厂公司资深算法专家。 邮箱&#xff1a; 1121025745qq.com 博客&#xff1a;https://wenjie.blog.csdn.net/ 内容&#xff1a;跟艾文学编程《Python基础入门》…

专题18:Django之Form,ModelForm

原始思路实现添加用户功能的缺点&#xff1a; 1&#xff09;用户提交的数据没有校验 2&#xff09;如果用户输入的数据有错误&#xff0c;没有错误提示 3&#xff09;前端页面上的每一个字段都需要我们重新写一次 4&#xff09;关联的数据需要手动取获取并循环展示在页面 1…