Appearance
题目
给你一个整数数组 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 。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
- 1 <= nums.length <= 2500
- -104 <= nums[i] <= 104
进阶:
你能将算法的时间复杂度降低到 O(n log(n)) 吗?
理解题目
给定一个数组,从数组中寻找递增组成的新数组的长度
题解
利用当前项去跟当前的前一项去比较,如果 nums[i]>nums[i-1]那么就是递增的 动态规划,从大变小,每个元素都至少可以单独成为子序列,此时长度都为 1 声明数组 dp,dp[i]的值代表 nums[i] 结尾的最长子序列长度:即递增的长度 当我们去比较的是,前面一项之前已经比较过了她也存了之前递增的长度了,依次向前拿去即可 当拿到的前一项的长度+1 和当前项的长度取最大值 赋值给当前项的长度
答案
动态规划
ts
function lengthOfLIS(nums: number[]): number {
if (nums.length === 0) return 0;
// 动态规划,从大变小,每个元素都至少可以单独成为子序列,此时长度都为 1
// 声明数组dp,dp[i]的值代表 nums[i] 结尾的最长子序列长度
let dp = new Array(nums.length).fill(1);
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
// num[i]:当前项,nums[j]当前项的前一项
// 如果nums[i]大于nums[j],nums[i] 可以接在 nums[j] 之后(因为要求严格递增【排除了相等等情况】),
// 此情况下最长子序列长度为 dp[j] + 1
// console.log(i,j)
if (nums[i] > nums[j]) {
// console.log([10, 9, 2, 5, 3, 7, 21, 18], dp);
// console.log(`i=${i},nums[${i}]=${nums[i]},j=${j},nums[${j}]=${nums[j]},dp[${i}]=${dp[i]},dp[${j}]=${dp[j]},dp[${j}] + 1=${dp[j] + 1}`);
// console.log(`因为Math.max(dp[${i}], dp[${j}] + 1)所以dp[${i}]=${Math.max(dp[i], dp[j] + 1)}`);
// debugger
dp[i] = Math.max(dp[i], dp[j] + 1);
}
// console.log("=========");
}
}
// console.log(dp)
return Math.max(...dp);
}
function lengthOfLIS(nums: number[]): number {
if (nums.length === 0) return 0;
// 动态规划,从大变小,每个元素都至少可以单独成为子序列,此时长度都为 1
// 声明数组dp,dp[i]的值代表 nums[i] 结尾的最长子序列长度
let dp = new Array(nums.length).fill(1);
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
// num[i]:当前项,nums[j]当前项的前一项
// 如果nums[i]大于nums[j],nums[i] 可以接在 nums[j] 之后(因为要求严格递增【排除了相等等情况】),
// 此情况下最长子序列长度为 dp[j] + 1
// console.log(i,j)
if (nums[i] > nums[j]) {
// console.log([10, 9, 2, 5, 3, 7, 21, 18], dp);
// console.log(`i=${i},nums[${i}]=${nums[i]},j=${j},nums[${j}]=${nums[j]},dp[${i}]=${dp[i]},dp[${j}]=${dp[j]},dp[${j}] + 1=${dp[j] + 1}`);
// console.log(`因为Math.max(dp[${i}], dp[${j}] + 1)所以dp[${i}]=${Math.max(dp[i], dp[j] + 1)}`);
// debugger
dp[i] = Math.max(dp[i], dp[j] + 1);
}
// console.log("=========");
}
}
// console.log(dp)
return Math.max(...dp);
}
二分搜索法+位运算+贪心算法
ts
/**
*
* @param nums
* @returns
* 复杂度分析
* 时间复杂度:O(nlogn)
* 遍历 nums 列表需 O(N),在每个 nums[i] 二分法需 O(logN)
* 空间复杂度:O(n)
* tails需要额外空间
*/
function lengthByDichotomy(nums: number[]): number {
if (nums.length === 0) return 0
// 维护一个数组 tails,其中每个元素 tails[k] 的值代表 长度为 k+1 的子序列尾部元素的值(k为自然数)
// 如tail[0]是求长度为1的连续子序列时的最小末尾元素
// 例:在 1 8 4中 tail[0]=1 tail[1]=4 没有tail[2] 因为无法到达长度为3的连续子序列
// 初始tails数组[0],其值为子序列列长度为1,即num[0]
// 由于js数组的特点,可以不用初始化tails数组的长度,不用每个元素都初始化0---当然初始化也是可以的
let tails: number[] = [nums[0]]
// 设 result 为 tails 当前长度,代表直到当前的最长递增子序列长度
let result = 1
// 从nums第二个元素开始遍历数组
for (let i = 1; i < nums.length; i++) {
// 比 tails[result] 大,则更新 tails[result],同时 result++
// console.log(i,result,nums[i] > tails[result-1])
if (nums[i] > tails[result-1]) {
tails[++result - 1] = nums[i]
} else {
// 否则,使用二分查找法,找到比 nums[i] 大的最小的 tails[j],即tails[j]<nums[i]<tails[j+1],然后更新 tails[j]
let left = 0, right = result
while (left < right) {
// 这里运用了位运算,为什么可以这么算呢?
// >>相当于处于2
//那这里为什么除以2呢?
//根据二分搜索法 基本原理:
/**
* 首先将要查找的元素[key]与数组的中间元素比较
* 1、如果key元素小于中间元素,组需要在数组的前一半中继续查找
* 2、如果key和中间元素相等,匹配成功,查找结束
* 3、如果key大于中间元素,只需要在数组的后一般元素中继续查找key
*/
const mid = (left + right) >> 1
// console.log("if (tails[mid] < nums[i]) {",tails[mid],nums[i])
if (tails[mid] < nums[i]) {
left = mid + 1
} else {
right = mid
}
}
tails[left] = nums[i]
// console.log('tails[left] = nums[i]',tails[left])
}
// 打印tails数组---不能等同于不同长度时对应的子序列,因为遇到小的数,会插在更前面,改变了在元素组的位置(证明用例[10, 9, 2, 5, 3, 7, 1, 18]中的1插入时)
// console.log(tails)
}
return result
}
/**
*
* @param nums
* @returns
* 复杂度分析
* 时间复杂度:O(nlogn)
* 遍历 nums 列表需 O(N),在每个 nums[i] 二分法需 O(logN)
* 空间复杂度:O(n)
* tails需要额外空间
*/
function lengthByDichotomy(nums: number[]): number {
if (nums.length === 0) return 0
// 维护一个数组 tails,其中每个元素 tails[k] 的值代表 长度为 k+1 的子序列尾部元素的值(k为自然数)
// 如tail[0]是求长度为1的连续子序列时的最小末尾元素
// 例:在 1 8 4中 tail[0]=1 tail[1]=4 没有tail[2] 因为无法到达长度为3的连续子序列
// 初始tails数组[0],其值为子序列列长度为1,即num[0]
// 由于js数组的特点,可以不用初始化tails数组的长度,不用每个元素都初始化0---当然初始化也是可以的
let tails: number[] = [nums[0]]
// 设 result 为 tails 当前长度,代表直到当前的最长递增子序列长度
let result = 1
// 从nums第二个元素开始遍历数组
for (let i = 1; i < nums.length; i++) {
// 比 tails[result] 大,则更新 tails[result],同时 result++
// console.log(i,result,nums[i] > tails[result-1])
if (nums[i] > tails[result-1]) {
tails[++result - 1] = nums[i]
} else {
// 否则,使用二分查找法,找到比 nums[i] 大的最小的 tails[j],即tails[j]<nums[i]<tails[j+1],然后更新 tails[j]
let left = 0, right = result
while (left < right) {
// 这里运用了位运算,为什么可以这么算呢?
// >>相当于处于2
//那这里为什么除以2呢?
//根据二分搜索法 基本原理:
/**
* 首先将要查找的元素[key]与数组的中间元素比较
* 1、如果key元素小于中间元素,组需要在数组的前一半中继续查找
* 2、如果key和中间元素相等,匹配成功,查找结束
* 3、如果key大于中间元素,只需要在数组的后一般元素中继续查找key
*/
const mid = (left + right) >> 1
// console.log("if (tails[mid] < nums[i]) {",tails[mid],nums[i])
if (tails[mid] < nums[i]) {
left = mid + 1
} else {
right = mid
}
}
tails[left] = nums[i]
// console.log('tails[left] = nums[i]',tails[left])
}
// 打印tails数组---不能等同于不同长度时对应的子序列,因为遇到小的数,会插在更前面,改变了在元素组的位置(证明用例[10, 9, 2, 5, 3, 7, 1, 18]中的1插入时)
// console.log(tails)
}
return result
}
Vue3快速Diff算法中的实现
注意:在快速Diff算法中,求解最长递增子序列的目的是对子节点重新编号,所以肯定不只是求解出长度即可。
ts
function getSequence(arr: number[]): number[] {
const p = arr.slice()
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
while (u < v) {
c = (u + v) >> 1
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
function getSequence(arr: number[]): number[] {
const p = arr.slice()
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
while (u < v) {
c = (u + v) >> 1
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
分析:getSequence相比于解法二,思路相近。但除了求解出长度,还会回溯查找原数组的索引--通过维护数组p,p记录了当前位置元素arr[i]插入时,递增序列前一个元素的真实值。