二分查找——链接 (这个一不小心就会错)
方法1,时间复杂度为O(N*N)
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums == null || nums.length == 0)
return 0;
int [] dp = new int[nums.length];
dp[0] = 1;
int maxlen = 1;
for(int i = 1;i<dp.length;i++){
dp[i] = 1;
for(int j = 0;j<i;j++){
if(nums[i]>nums[j] && dp[j]+1>dp[i]){
dp[i] = dp[j]+1;
}
}
if(dp[i]>maxlen)
maxlen = dp[i];
}
return maxlen;
}
}
方法2,时间复杂度O(nlogn)——解析和代码(这里更详细)
算法的执行流程:
1、设置一个有序数组 tail,初始时为空;
数组命名为 tail 即 PPT 中各个行表示的数组(是一个“上升子序列”)的结尾,注意:有序数组 tail 虽然有“上升”的性质,但它不是每时每刻都表示问题中的“最长上升子序列”(下文还会强调),不能命名为 LIS,有序数组 tail 是用于求解 LIS 问题的辅助数组。
2、在遍历数组 nums 的过程中,每来一个新数 num,如果这个数严格大于有序数组 tail 的最后一个元素,就把 num 放在有序数组 tail 的后面,否则进入第 3 点;注意:这里的大于是“严格”大于,不包括等于的情况。
3、在有序数组 tail 中查找第 1 个等于大于 num 的那个数,试图让它变小;如果有序数组 tail 中存在等于 num 的元素,什么都不做,因为以 num 结尾的最短的“上升子序列”已经存在;
如果有序数组 tail 中存在大于 num 的元素,找到第 1 个,让它变小,这样我们就找到了一个“结尾更小”的“相同长度”的上升子序列。
这一步可以使用“二分查找法”。4、遍历新的数 num ,先尝试上述第 2 点,第 2 点行不通则执行第 3 点,直到遍历完整个数组 nums,最终有序数组 tail 的长度,就是所求的“最长上升子序列”的长度。
算法的关键之处:
以上算法能够奏效的关键是:
最开始提到的“基本思想”,可以认为是一种“贪心选择”的思想:只要让前面的数尽量小,在算法的执行过程中,第 2 点被执行的机会就越多。
package com.niuke.huawei20190828;
import java.util.Scanner;
/**
* @Author huanglei
* @Date 2019-09-02 16:59
* @Description TODO 最长上升子序列 {1,3,0,2} 它的最长上升子序列为{1,3} 长度为2
**/
public class Main03_1 {
public static void main(String [] args){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int [] numsA = new int[n];
input(sc, numsA);
System.out.println(LengthOfLIS(numsA));
}
//最长递增子序列的长度, 最优方法,时间复杂度O(nlogn),空间复杂度,o(n)
public static int LengthOfLIS(int [] arr){
int len = arr.length;
if(len<=1)// 特判
return len;
// tail 数组的定义:长度为 i + 1 的上升子序列的末尾最小是几
int [] tail = new int[len];
tail[0] = arr[0];// 遍历第 1 个数,直接放在有序数组 tail 的开头
int end = 0;// end 表示有序数组 tail 的最后一个已经赋值元素的索引
for(int i = 1;i<len;i++){
// 这里,因为当前遍历的数,有可能比有序数组 tail 数组实际有效的末尾的那个元素还大
// 【逻辑 1】因此 end + 1 应该落在候选区间里
if(arr[i]>tail[end]){
end++;
tail[end] = arr[i];
}else {
int left = 0;
int right = end;
while(left<right){
// 选左中位数不是偶然,而是有原因的,原因请见 LeetCode 第 35 题题解
int mid = (left+right)>>>1;
if(tail[mid]<arr[i]){
// 中位数肯定不是要找的数,把它写在分支的前面
left = mid+1;
}else {
right = mid;
}
}
// 走到这里是因为 【逻辑 1】 的反面,因此一定能找到第 1 个大于等于 nums[i] 的元素
// 因此,无需再单独判断
tail[left] = arr[i];
}
}
// 此时 end 是有序数组 tail 最后一个元素的索引
// 题目要求返回的是长度,因此 +1 后返回
end++;
return end;
}
public static void input(Scanner sc, int [] nums){
for(int i = 0;i<nums.length;i++){
nums[i] = sc.nextInt();
}
}
}