问题
给定一个无序的整数数组,找到其中最长递增子序列的长度。
样例
第一个样例
输入:[ 10, 9, 2, 5, 3, 7, 101, 18 ]
输出:4
解释:最长的递增子序列是 [ 2, 3, 7, 101 ]
第二个样例
输入:[ 1, 2, 2, 3, 3 ]
输出:3
解释:最长的递增子序列是 [ 1, 2, 3 ]
解析
先仔细留意第二个样例,从输出和解释可以发现,题目要求的最长递增子序列必须是一个严格递增子序列!这个点可不能忽视!
提供两种思路:动态规划、贪心算法,贪心算法又可以配合二分查找算法进一步降低时间复杂度。
动态规划
动态规则最关键的解题思路就是能否找到状态转移方程。
本题中,定义 为前
个元素中,以第
个元素结尾的最长递增子序列的长度。那么,状态转移方程就是:
举个例子,假设以 结尾的最长递增子序列长度是6,如果
,那么,以
结尾的最长递增子序列长度就是
动态规划的时间复杂度是 ,空间复杂度是
public int lengthOfLIS(int[] nums) {
int length = nums.length;
if (length <= 1) return length;
int[] dp = new int[length];
Arrays.fill(dp, 1);
int max = 1;
for (int i = 1; i < length; 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;
}
贪心算法 + 二分查找
贪心算法不太好理解,需要反复想象可能的场景。
先用语言描述一下贪心算法的思想,大概就是:如果想让递增子序列尽可能的长,就需要让序列上升的尽可能慢。
再用语言描述一下贪心算法的过程,大概就是:
- 创建一个临时数组,用于保存最长递增子序列
- 顺序遍历目标数组,尝试着将目标元素插入临时数组
- 如果目标元素比临时数组中所有元素都大,就把它追加到临时数组最后面
- 否则,就用目标元素替代临时数组中不小于目标值的最小元素
遍历完目标数组后,临时数组的长度就是求解的最长递增子序列的长度。但是!临时数组不一定就是最长递增子序列!
可以自己用一个数组走一遍算法流程,体验一下:[10,9,2,5,3,7,101,18,6,5,4,2,1,0]
能否理解这个贪心算法的过程,以及临时数组为什么不一定是最长递增子序列,就看个人的造化了。
说到这里,其实问题已经用贪心思路解决了,但是还有可优化的地方。优化点在临时数组的元素查找上面,虽然临时数组不一定是最长递增子序列,但是它一定是单调递增的,所以,查找元素可以用二分查找算法,降低这部分的时间复杂度。
贪心+二分查找的时间复杂度是,空间复杂度是
public int lengthOfLIS(int[] nums) {
int length = nums.length;
if (length <= 1) return length;
int[] temp = new int[length];// 有可能的最长长度
temp[0] = nums[0];// 至少有一个
int index = 0;// 指向temp数组存储的最后一个元素
for (int i = 1; i < length; i++) {
int num = nums[i];
if (num > temp[index]) temp[++index] = num;
else {// 二分查找
int left = 0, right = index;
while (left < right) {
int mid = (left + right) / 2;
if (num > temp[mid]) left = mid + 1;
else right = mid;
}
temp[left] = num;// 此时 left == right 仔细体会
}
}
return index + 1;
}