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; } }