一、题目
给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。
如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [1,2,3,4,5] 输出:true 解释:任何 i < j < k 的三元组都满足题意
示例 2:
输入:nums = [5,4,3,2,1] 输出:false 解释:不存在满足题意的三元组
示例 3:
输入:nums = [2,1,5,0,4,6]
输出:true
解释:三元组 (3, 4, 5) 满足题意,因为 nums[3] == 0 < nums[4] == 4 < nums[5] == 6
提示:
- 1 <= nums.length <= 5 * 105
- -231 <= nums[i] <= 231 - 1
进阶:你能实现时间复杂度为 O(n) ,空间复杂度为 O(1) 的解决方案吗?
二、代码
class Solution {
public boolean increasingTriplet(int[] nums) {
if (nums == null && nums.length < 3) {
return false;
}
int n = nums.length;
// ends数组
// ends[i]表示 : 目前所有长度为i+1的递增子序列的最小结尾数
int[] end = new int[n + 1];
// 根据含义, 一开始ends[0] = arr[0]
end[0] = nums[0];
// ends有效区范围是0...right,right往右为无效区
// 所以一开始right = 0, 表示有效区只有0...0范围
int right = 0;
// 最长递增子序列的长度
// 全局变量,抓取每一步的答案,取最大的结果
int max = 1;
for (int i = 0; i < n; i++) {
int l = 0;
int r = right;
// 在ends[l...r]范围上二分
// 如果 当前数(arr[i]) > ends[m],砍掉左侧
// 如果 当前数(arr[i]) <= ends[m],砍掉右侧
// 整个二分就是在ends里寻找 >= 当前数(arr[i])的最左位置
// 就是从while里面出来时,l所在的位置。
// 如果ends中不存在 >= 当前数(arr[i])的情况,将返回有效区的越界位置
// 也就是从while里面出来时,l所在的位置,是有效区的越界位置
// 比如 : ends = { 3, 5, 9, 12, 再往右无效}
// 如果当前数为8, 从while里面出来时,l将来到2位置
// 比如 : ends = { 3, 5, 9, 12, 再往右无效}
// 如果当前数为13, 从while里面出来时,l将来到有效区的越界位置,4位置
while (l <= r) {
int mid = (l + r) >> 1;
if (nums[i] > end[mid]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
// 从while里面出来,看l的位置
// 如果l比right大,说明扩充了有效区,那么right变量要随之变大
// 如果l不比right大,说明l没有来到有效区的越界位置,right不变
right = Math.max(right, l);
// l的位置,就是当前数应该填到ends数组里的位置(有两种情况,是将l位置原有的数修改的更小,或者是将nums[i]放到一个新扩充的位置)
end[l] = nums[i];
// 更新最大递增子序列长度
max = Math.max(max, l + 1);
}
// 最长递增子序列长度大于等于3就是true
return max >= 3;
}
}
三、解题思路
一般子序列的这种题我们就使用动态规划求解。以i位置结尾的子序列怎么怎么样,以这个角度去写动态规划。只要把所有位置作为某一个子序列的结尾的最大值都求出来,然后在里面取最大值,肯定就能把最终答案求出来,不会有遗漏子序列的情况。
这个优化点就是引入了end数组,这个数组将我们需要的信息有序化,进而可以使用二分法实现快速查找,就不用在dp数组中进行遍历查找了,因为dp数组中的数据并不是有序的,所以不能用二分。