Skip to content

Commit 71b72db

Browse files
committed
300
1 parent 09f18b7 commit 71b72db

3 files changed

+502
-1
lines changed

SUMMARY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,4 +243,5 @@
243243
* [295*. Find Median from Data Stream](leetcode-295-Find-Median-from-Data-Stream.md)
244244
* [297. Serialize and Deserialize Binary Tree](leetcode-297-Serialize-and-Deserialize-Binary-Tree.md)
245245
* [299. Bulls and Cows](leetcode-299-Bulls-and-Cows.md)
246-
* [更多](more.md)
246+
* [300. Longest Increasing Subsequence](leetcode-300-Longest-Increasing-Subsequence.md)
247+
* [更多](more.md)
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# 题目描述(中等难度)
2+
3+
300、Longest Increasing Subsequence
4+
5+
Given an unsorted array of integers, find the length of longest increasing subsequence.
6+
7+
**Example:**
8+
9+
```
10+
Input: [10,9,2,5,3,7,101,18]
11+
Output: 4
12+
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.
13+
```
14+
15+
**Note:**
16+
17+
- There may be more than one LIS combination, it is only necessary for you to return the length.
18+
- Your algorithm should run in O(*n2*) complexity.
19+
20+
**Follow up:** Could you improve it to O(*n* log *n*) time complexity?
21+
22+
最长上升子序列的长度。
23+
24+
# 解法一
25+
26+
比较经典的一道题,之前笔试也遇到过。最直接的方法就是动态规划了。
27+
28+
`dp[i]`表示以第 `i` 个数字**为结尾**的最长上升子序列的长度。
29+
30+
`dp[i]` 的时候,如果前边的某个数 `nums[j] < nums[i]` ,那么我们可以将第 `i` 个数接到第 `j` 个数字的后边作为一个新的上升子序列,此时对应的上升子序列的长度就是 `dp[j] + 1`
31+
32+
可以从下边情况中选择最大的。
33+
34+
如果 `nums[0] < nums[i]``dp[0] + 1` 就是 `dp[i]` 的一个备选解。
35+
36+
如果 `nums[1] < nums[i]``dp[1] + 1` 就是 `dp[i]` 的一个备选解。
37+
38+
如果 `nums[2] < nums[i]``dp[2] + 1` 就是 `dp[i]` 的一个备选解。
39+
40+
...
41+
42+
如果 `nums[i-1] < nums[i]``dp[i-1] + 1` 就是 `dp[i]` 的一个备选解。
43+
44+
从上边的备选解中选择最大的就是 `dp[i]` 的值。
45+
46+
```java
47+
public int lengthOfLIS(int[] nums) {
48+
int n = nums.length;
49+
if (n == 0) {
50+
return 0;
51+
}
52+
int dp[] = new int[n];
53+
int max = 1;
54+
for (int i = 0; i < n; i++) {
55+
dp[i] = 1;
56+
for (int j = 0; j < i; j++) {
57+
if (nums[j] < nums[i]) {
58+
dp[i] = Math.max(dp[i], dp[j] + 1);
59+
}
60+
}
61+
max = Math.max(max, dp[i]);
62+
}
63+
return max;
64+
}
65+
```
66+
67+
时间复杂度:`O(n²)`
68+
69+
空间复杂度:`O(1)`
70+
71+
# 解法二
72+
73+
还有一种很巧妙的方法,最开始知道这个方法的时候就觉得很巧妙,但还是把它忘记了,又看了一遍 [这里](https://leetcode.com/problems/longest-increasing-subsequence/discuss/74824/JavaPython-Binary-search-O(nlogn)-time-with-explanation) 才想起来。
74+
75+
不同之处在于 `dp` 数组的定义。
76+
77+
`dp[i]` 表示长度为 `i + 1` 的所有上升子序列的末尾的最小值。
78+
79+
比较绕,举个例子。
80+
81+
```java
82+
nums = [4,5,6,3]
83+
len = 1 : [4], [5], [6], [3] => tails[0] = 3
84+
长度为 1 的上升子序列有 4 个,末尾最小的值就是 4
85+
86+
len = 2 : [4, 5], [5, 6] => tails[1] = 5
87+
长度为 2 的上升子序列有 2 个,末尾最小的值就是 5
88+
89+
len = 3 : [4, 5, 6] => tails[2] = 6
90+
长度为 3 的上升子序列有 1 个,末尾最小的值就是 6
91+
```
92+
93+
有了上边的定义,我们可以依次考虑每个数字,举个例子。
94+
95+
```java
96+
nums = [10,9,2,5,3,7,101,18]
97+
98+
开始没有数字
99+
dp = []
100+
101+
1----------------------------
102+
10 9 2 5 3 7 101 18
103+
^
104+
105+
先考虑 10, 只有 1 个数字, 此时长度为 1 的最长上升子序列末尾的值就是 10
106+
len 1
107+
dp = [10]
108+
109+
2----------------------------
110+
10 9 2 5 3 7 101 18
111+
^
112+
考虑 9, 9 比之前长度为 1 的最长上升子序列末尾的最小值 10 小, 更新长度为 1 的最长上升子序列末尾的值为 9
113+
len 1
114+
dp = [9]
115+
116+
3----------------------------
117+
10 9 2 5 3 7 101 18
118+
^
119+
考虑 2, 2 比之前长度为 1 的最长上升子序列末尾的最小值 9 小, 更新长度为 1 的最长上升子序列末尾的值为 2
120+
len 1
121+
dp = [2]
122+
123+
4----------------------------
124+
10 9 2 5 3 7 101 18
125+
^
126+
考虑 5,
127+
5 比之前长度为 1 的最长上升子序列末尾的最小值 2 大,
128+
此时可以扩展长度, 更新长度为 2 的最长上升子序列末尾的值为 5
129+
len 1 2
130+
dp = [2 5]
131+
132+
5----------------------------
133+
10 9 2 5 3 7 101 18
134+
^
135+
考虑 3,
136+
3 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑
137+
3 比之前长度为 2 的最长上升子序列末尾的最小值 5 小, 更新长度为 2 的最长上升子序列末尾的值为 3
138+
len 1 2
139+
dp = [2 3]
140+
141+
6----------------------------
142+
10 9 2 5 3 7 101 18
143+
^
144+
考虑 7,
145+
7 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑
146+
7 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑
147+
此时可以扩展长度, 更新长度为 3 的最长上升子序列末尾的值为 7
148+
len 1 2 3
149+
dp = [2 3 7]
150+
151+
7----------------------------
152+
10 9 2 5 3 7 101 18
153+
^
154+
考虑 101,
155+
101 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑
156+
101 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑
157+
101 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑
158+
此时可以扩展长度, 更新长度为 4 的最长上升子序列末尾的值为 101
159+
len 1 2 3 4
160+
dp = [2 3 7 101]
161+
162+
8----------------------------
163+
10 9 2 5 3 7 101 18
164+
^
165+
考虑 18,
166+
18 比之前长度为 1 的最长上升子序列末尾的最小值 2 大, 向后考虑
167+
18 比之前长度为 2 的最长上升子序列末尾的最小值 3 大, 向后考虑
168+
18 比之前长度为 3 的最长上升子序列末尾的最小值 7 大, 向后考虑
169+
3 比之前长度为 4 的最长上升子序列末尾的最小值 101 小, 更新长度为 4 的最长上升子序列末尾的值为 18
170+
len 1 2 3 4
171+
dp = [2 3 7 18]
172+
173+
遍历完成,所以数字都考虑了,此时 dp 的长度就是最长上升子序列的长度
174+
```
175+
176+
总结上边的规律,新来一个数字以后,我们去寻找 `dp` 中第一个比它大的值,然后将当前值更新为新来的数字。
177+
178+
如果 `dp` 中没有比新来的数字大的数,那么就扩展长度,将新来的值放到最后。
179+
180+
写代码的话,因为 `dp` 是一个动态扩容的过程,我们可以用一个 `list` 。但由于比较简单,我们知道 `dp` 最大的长度也就是 `nums` 的长度,我们可以直接用数组,然后自己记录当前数组的长度即可。
181+
182+
```java
183+
public int lengthOfLIS(int[] nums) {
184+
int n = nums.length;
185+
if (n == 0) {
186+
return 0;
187+
}
188+
int dp[] = new int[n];
189+
int len = 0;
190+
for (int i = 0; i < n; i++) {
191+
int j = 0;
192+
// 寻找 dp 中第一个大于等于新来的数的位置
193+
for (j = 0; j < len; j++) {
194+
if (nums[i] <= dp[j]) {
195+
break;
196+
}
197+
}
198+
// 更新当前值
199+
dp[j] = nums[i];
200+
// 是否更新长度
201+
if (j == len) {
202+
len++;
203+
}
204+
}
205+
return len;
206+
}
207+
```
208+
209+
上边花了一大段话讲这个解法,但是上边的时间复杂度依旧是 `O(n²)`,当然不能满足。
210+
211+
这个解法巧妙的地方在于,通过上边 `dp` 的定义,`dp` 一定是有序的。我们要从一个有序数组中寻找第一个大于等于新来数的位置,此时就可以通过二分查找了。
212+
213+
```java
214+
public int lengthOfLIS(int[] nums) {
215+
int n = nums.length;
216+
if (n == 0) {
217+
return 0;
218+
}
219+
int dp[] = new int[n];
220+
int len = 0;
221+
for (int i = 0; i < n; i++) {
222+
int start = 0;
223+
int end = len;
224+
while (start < end) {
225+
int mid = (start + end) >>> 1;
226+
if (dp[mid] < nums[i]) {
227+
start = mid + 1;
228+
} else {
229+
end = mid;
230+
}
231+
}
232+
dp[start] = nums[i];
233+
if (start == len) {
234+
len++;
235+
}
236+
}
237+
return len;
238+
}
239+
```
240+
241+
这样的话时间复杂度就是 `O(nlog(n))` 了。
242+
243+
上边需要注意的一点是,在二分查找中我们将 `end` 初始化为 `len`, 平常我们习惯上初始化为 `len - 1`
244+
245+
赋值为 `len` 的目的是当 `dp` 中没有数字比当前数字大的时候,最后 `start` 刚好就是 `len`, 方便扩展数组。
246+
247+
#
248+
249+
解法一比较常规,比较容易想到。
250+
251+
解法二的话就很巧妙了,关键就是 `dp` 的定义使得 `dp` 是一个有序数组了。这种也不容易记住,半年前笔试做过这道题,但现在还是忘记了,不过还是可以欣赏一下的,哈哈。
252+

0 commit comments

Comments
 (0)