项目介绍
- 本项目通过分解各大厂的常见笔面试题,追本溯源至数据结构和算法的底层实现原理,知其然知其所以然;
- 建立知识结构体系,方便查找,欢迎更多志同道合的朋友加入项目AlgorithmPractice,(欢迎提issue和pull request)。
什么是最长递增子序列
- 在一个给定的数值序列中,找到一个子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能地大。最长递增子序列中的元素在原序列中不一定是连续的。
- 学完这道题的六种解法,就可以去秀面试官了 。
六种解法
- 1、暴力法
- 2、动态规划法
- 3、分治法
- 4、字符串对比法
- 5、分支限界法
- 6、扑克法
正文开始
1、暴力法
- 代码实现:LIS_Violence,测试用例:TestLIS_Violence
- 设计思路:
- 遍历字符串的所有组成可能,对这些可能进行判断是否符合递增规律,并统计最长的子串长度。
- 主要代码:
for (int beginLocation = 0; beginLocation < sequence.length() - 1; beginLocation++) {
for (int subLength = 1; subLength < sequence.length() - beginLocation; subLength++) {
sb = new StringBuffer();
sb.append(sequence.charAt(beginLocation));
dealString(sb, sequence, beginLocation, subLength);
}
}
public void dealString(StringBuffer sb, String s, int beginPosition, int strdepth) {
if (strdepth == 0 && judge(sb.toString())) {
if (sb.length() > best_length) {
best_length = sb.length();
}
return;
}
for (int i = beginPosition + 1; i < s.length(); i++) {
sb.append(s.charAt(i));
dealString(sb, s, i, strdepth - 1);
sb.deleteCharAt(sb.length() - 1);
}
}
- 注意事项:
- 每次用于统计的temp数组或者temp List,在进行下一次运算的时候需要清空。
- 遍历字符串的所有组成是采用递归做的,递归前后的取舍需要注意,特别是StringBuffer.deleteCharAt()方法,这里面的参数不能用当前的循环值,而应该是StringBuffer.length() - 1。
2、动态规划法
- 代码实现:LIS_Dynamic,测试用例:TestLIS_Dynamic
- 设计思路:
- 对于字符串中任意某点 J 的最大子串,等于其前面的最大子串数加一。
- 状态转换方程:longest[i] = (longest[j] + 1) > longest[i] ? (longest[j] + 1) : longest[i];
- 主要代码:
for (int i = 0; i < length; i++) {
for (int j = 0; j < i; j++) {
if ((intArray[j] < intArray[i])) {
longest[i] = (longest[j] + 1) > longest[i] ? (longest[j] + 1) : longest[i];
}
}
if (longest[i] > best) {
best = longest[i];
point = i;
}
}
3、分治法【局限于连续子串,本题仅供参考】
- 代码实现:LIS_Divide,测试用例:TestLIS_Divide
- 设计思路:
- 对于指定字符串,一定存在某个最大长度的递增子序列,现在把指定字符串分成左边和右边,那么这个最大的递增子序列,要么存在于左边子串,要么存在于右边子串,要么横跨左右(这特么不是废话)。
- 对于横跨两边的子串,分别向左扩展找出小于其的最长递增子序列,向右同理,但是本题我在做得时候,我仅仅做了大小判断,所以这种方法只能解决连续的递增问题,非连续子串的递增,可能需要才有动态规划的方式。
- 主要代码:
public int divide(int[] stringArr, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
int leftValue = divide(stringArr, left, mid);
int rightValue = divide(stringArr, mid + 1, right);
int midValue = middleHandle(stringArr, left, right);
return Math.max(Math.max(leftValue, rightValue), midValue);
}
return 0;
}
while (leftPoint - 1 >= left && stringArr[leftPoint] > stringArr[leftPoint - 1]) {
count++;
leftPoint--;
}
while (rightPoint + 1 <= right && stringArr[rightPoint] < stringArr[rightPoint + 1]) {
count++;
rightPoint++;
}
- 注意事项:
- 本解法不要用于解决最长递增子序列,仅仅用于计算最长连续递增子串
4、字符串对比法
- 代码实现:LIS_Lcs,测试用例:TestLIS_Lcs
- 设计思路:
- 将字符串转出数组,进行排序
- 排序后的数组,进行去重(考虑递增不单调,不存在重复的数据)
- 将去重后的数组再转成字符串与原字符串进行最长公共子序列对比(其本质还是动态规划的思想)。
- 主要代码:
QuickSortDuplexing q = new QuickSortDuplexing();
q.sortMethod(ints);
HashMap hashMap = new HashMap();
for (int i = 0; i < c.length; i++) {
hashMap.put(ints[i], 1);
}
String temp = hashMap.keySet().toString().replace(",", "").replace("[", "").replace("]", "").replace(" ", "");
LCS lcs = new LCS();
int length = lcs.count(temp, sequence).getCommondLength();
- 注意事项:
- 去重选取的是hashmap的key
- hashmap转String,需要replace很多,暂时没找到比较好的办法。
5、分支限界法
- 代码实现:LIS_Branch,测试用例:TestLIS_Branch
- 设计思路:
- 分支限界法是对暴力法的改进,对一些显而易见的条件进行删除。
- 比如:
- 当前temp的值加上剩下待遍历的距离,小于等于最优值的时候,就没有必要再继续下去了。
- 剩下的待遍历距离小于当前的最优解,就没有必要再继续下去了。
- 主要代码:
for (int i = 1; count_best <= length - i; i++) {
list_temp = new ArrayList();
list_temp.add(StringArray[i - 1]);
count_temp = 1;
count(i);
}
if ((length - 1) - depth + (count_temp + 1) <= count_best) {
return;
}
if (count_temp > count_best || depth == length - 1) {
if (count_temp > count_best) {
list_best = new ArrayList<>(list_temp);
count_best = count_temp;
}
if (depth == length - 1) {
return;
}
}
for (int i = depth; i < length; i++) {
if (list_temp.get(count_temp - 1) < StringArray[i]) {
count_temp++;
list_temp.add(StringArray[i]);
count(i + 1);
list_temp.remove(list_temp.get(--count_temp));
}
}
- 注意事项:
- List赋值的时候,这样才不会造成引用跟随:list_best = new ArrayList<>(list_temp);
6、扑克法【扑克法本质是分治思想】
- 代码实现:LIS_Poker,TestLIS_Poker
- 设计思路:
- 按照扑克牌的玩法,第一张自启一摞
- 第二张比第一张小,则压在第一张上
- 第二张比第一张大,则另启一摞。
- 第三张比第二张小,则压在第二张上
- 第三张比第二张大,则另启一摞。
- 最后分成的摞数,就是我们要求的最长递增子序列数,你一定可以找到一个组合,位于不同的摞中,是严格递增的存在,数学证明略。
- 主要代码:
for (int i = 0; i < count; i++) {
left = 0;
right = piles;
poker = intArray[i];
while (left < right) {
mid = (left + right) / 2;
if (poker < top[mid]) {
right = mid;
} else if (poker > top[mid]) {
left = mid + 1;
} else {
right = mid;
}
}
if(left == piles){
piles++;
}
top[left] = poker;
}
- 注意事项:
- 限于表达能力,扑克法描述可能表述的不清晰,大家可以参考这篇详细的博文:动态规划设计之最长递增子序列。