如果我们有一个 HTML 的列表,现在我们对列表的某些元素进行了更新,因为操作 DOM 是一个比较消耗性能的事情,所以我们想尽可能少移动 DOM 来完成更新:
更新前:
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
我们想更新成:
<li>1</li>
<li>3</li>
<li>4</li>
<li>2</li>
<li>5</li>
为了方便描述,我们把上面两段 DOM 记作数组:前者 [1, 2, 3, 4, 5]
,记作 a;后者 [1, 3, 4, 2, 5]
,记作 b。
解决上述的问题的一种思路是:先求出变化后数组的最长子序列,然后保持最长子序列不动,只移动最长子序列之外的元素到它应该的位置。
在我们的示例中,数组 b 的长度比较短,我们可以通过观察看出它的最长子序列的长度是 3,它的最长子序列可以是 [1, 2, 5]
,也可以是 [3, 4, 5]
。求出子序列之后,我们只需要移动 a 数组的另外两个不在最长子序列中的元素到指定位置,就能把它变为 b 数组了。利用这样的思路,我们可以就能只移动非常少的元素,就得到目标数组了。
希望我已经介绍清楚了最长子序列的一个应用场景,现在我们来讨论一下,如何求解数组的最长子序列。
这个问题一般有两个解决思路,一种是使用动态规划,一种是使用贪心加二分的方法。本文讲解的是后者。同时,您可以点击到达对应的: LeetCode 问题
我们示例的数组是:[10,9,2,5,3,7,101,18]
思路讲解
最开始,我们先讲解思路,为了方便大家理解,我们还是拿图说话:
讲解
看完上面过程的同学心里应该对我们要做什么有数了,我们再总结一下。
我们在循环中,更新最长子序列的策略有两个,如果我们当前数组的遍历到的值是 target
:
- 如果大于最长子序列的最大值,就把
target
加到当前最长子序列后面 - 如果不大于的话,就找到第一个大于或等于
target
的索引,将最长子序列数组对应索引的元素更新为target
。
为了提高算法效率,在满足第 2 点的时候,我们会使用二分查找去找到目标值。明白了这两点,我们最后来看一下算法:
public class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] tails = new int[nums.length];
int LISIndex = 0;
tails[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
int tail = tails[LISIndex];
if (tail < nums[i]) {
LISIndex++;
tails[LISIndex] = nums[i];
} else if(tail > nums[i]) {
searchFirstBiggerAndReplace(tails, 0, LISIndex, nums[i]);
}
}
return LISIndex + 1;
}
private void searchFirstBiggerAndReplace(
int[] tails,
int lo,
int hi,
int target
) {
if (lo >= hi) {
tails[hi] = target;
return;
}
int mid = lo + (hi - lo) / 2;
if (tails[mid] < target) {
searchFirstBiggerAndReplace(tails, mid + 1, hi, target);
} else {
searchFirstBiggerAndReplace(tails, lo, mid, target);
}
}
public static void main(String[] args) {
int[] nums = new int[]{4,10,4,3,8,9};
Solution solution = new Solution();
System.out.println(solution.lengthOfLIS(nums));
}
}