java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846 |
---|
解题思路 |
---|
- 处理回文串有2种经典算法,一种是中心扩展法,另一种是Manacher 算法
- 中心扩展法,逻辑相对简单一些,时间复杂度O(n^2), 而不需要额外空间复杂度O(1)
- Manacher 算法是线性时间处理回文串的算法,但需要额外的空间。时间复杂度O(n),空间复杂度O(n)
- 当然还有另类的算法,那就是强行使用动态规划。时间复杂度和空间复杂度都是O(n^2)
代码 |
---|
- Manacher算法:了解即可,一般用不到。除非你工作中确实遇到了,大量处理回文串的场景。不过几乎是遇不到的。遇到了再学习也可以
class Solution {
/**
方法一:Manacher 算法 O(n) 空间复杂度O(n) 。非常复杂,推荐先掌握方法二
比如abaaba这个字符串
先统一奇偶性,变成#a#b#a#a#b#a#,就是用一个特殊字符,将每一个字符分隔,首尾也要
那么偶数个的字符串abaaba的中心就是aba(#)aba中间的#号
奇数个的字符串aba的中心是#a#b#a# 正好是中间的b
而且无论源串是奇数还是偶数,加完#号后,都是奇数
而且加完后的回文串长度是2*n+1.(aba => #a#b#a# = 2*3+1 = 7. abba => #a#b#b#a# = 2*4+1 = 9)
因此我们还原时,只需要(len-1)/2 就可以了。(#a#b#a# = (7-1)/2 = 3. #a#b#b#a# = (9-1)/2 = 4)
*/
public String longestPalindrome(String s) {
int start = 0, end = -1;
StringBuffer t = new StringBuffer("#");
for (int i = 0; i < s.length(); ++i) {
t.append(s.charAt(i));
t.append('#');
}
t.append('#');
s = t.toString();
List<Integer> arm_len = new ArrayList<Integer>();
int right = -1, j = -1;
for (int i = 0; i < s.length(); ++i) {
int cur_arm_len;
if (right >= i) {
int i_sym = j * 2 - i;
int min_arm_len = Math.min(arm_len.get(i_sym), right - i);
cur_arm_len = expand(s, i - min_arm_len, i + min_arm_len);
} else {
cur_arm_len = expand(s, i, i);
}
arm_len.add(cur_arm_len);
if (i + cur_arm_len > right) {
j = i;
right = i + cur_arm_len;
}
if (cur_arm_len * 2 + 1 > end - start) {
start = i - cur_arm_len;
end = i + cur_arm_len;
}
}
StringBuffer ans = new StringBuffer();
for (int i = start; i <= end; ++i) {
if (s.charAt(i) != '#') {
ans.append(s.charAt(i));
}
}
return ans.toString();
}
public int expand(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
--left;
++right;
}
return (right - left - 2) / 2;
}
}
- 中心扩展法:推荐掌握的方法,面试遇到了,可以快速准确的做出相关题目
class Solution {
/**
方法二:中心扩展法(中心枚举) O(n^2) 空间复杂度O(1),推荐的方法,简单,8ms
比如abcba这个字符串
我们先找到最开始的a,它本身是回文串,然后中心扩散,发现左边没有,那么长度为1(right-left-1)
然后找到b,本身回文串,扩散,a!=c.因此abc不是回文串,只有b本身,长度为1
然后c为中心,扩散,b = b,bcb是回文串,可以继续扩散。a=a,那么abcba是回文串,再次扩散,没有了,长度为5
例外的abccba这样的字符串也是回文串,cc为中心
同样的abcccba以c为中心,abccccba,以cc,就是中间的c(cc)c,为中心
如何获取找到的回文串下标呢?
abccba是回文串,长度len为6,中心为cc,
循环下标index为2指向中心,也就是说len的一半,让index减去,就是起始下标
但是下标从0开始,所以,(len-1)/2才对,因此
start = i-(len-1)/2
同理end = i+len/2;
*/
public String longestPalindrome1(String s) {
if(s==null || s.length()<1) return"";
char c[] = s.toCharArray();
int start = 0,end = 0;
for(int i = 0;i<s.length();i++){
int len1 = expandAroundCenter(c,i,i);
int len2 = expandAroundCenter(c,i,i+1);
int len = Math.max(len1,len2);
if(len > end-start){
start=i-(len-1)/2;
end = i+len/2;
}
}
return s.substring(start,end+1);
}
private int expandAroundCenter(char[] c,int i,int j){
while(i>=0 && j<c.length && c[i] == c[j]){
i--;j++;
}
return j - i - 1;
}
}
- 动态规划, 驴唇不对马嘴的感觉。处理回文串感觉完全不是动态规划该干的。
class Solution {
//方法三:动态规划 O(n^2) 空间复杂度O(n^2) 不推荐,浪费资源,160ms
// 字符串[b,a,b,a,d]
// 下标 [0,1,2,3,4]
//动态规划:[0~4]这个串babad是否回文,取决于0和4对应字符(b,d)是否相同,以及它中间的1~3对应的子串(aba)是否是回文串
//因为b和d不同,所以不是回文串
//[1~3]aba这个串是否回文,取决于1和3对应字符a,a是否相同,以及中间2对应的子串,b是不是回文串,
//因为a=a,并且b是回文串(1个字符就是回文串),所以[1~3]是回文串,长度为3-1+1 = 3(j-i+1)
//规划表 arr[i][j] = arr[i-1][]
// 0 1 2 3 4
//0 true false true false false
//1 true false true false
//2 true false fasle
//3 true fasle
//4 true
public String longestPalindrome2(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i..j] 是否是回文串
boolean[][] dp = new boolean[len][len];
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
char[] charArray = s.toCharArray();
// 递推开始
// 先枚举子串长度
for (int L = 2; L <= len; L++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int i = 0; i < len; i++) {
// 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
int j = L + i - 1;
// 如果右边界越界,就可以退出当前循环
if (j >= len) {
break;
}
if (charArray[i] != charArray[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
}
}
刷题一定要坚持,总结套路,不单单要把题做出来,要举一反三,也要参考别人的思路,学习别人解题的优点,找出你觉得可以优化的点。
- 单链表解题思路:双指针、快慢指针、反转链表、预先指针
- 双指针:对于单链表而言,可以方便的让我们遍历结点,并做一些额外的事
- 快慢指针:常用于找链表中点,找循环链表的循环点,一般快指针每次移动两个结点,慢指针每次移动一个结点。
- 反转链表:通常有些题,将链表反转后会更好做,一般选用三指针迭代法,递归的空间复杂度有点高
- 预先指针:常用于找结点,比如找倒数第3个结点,那么定义两个指针,第一个指针先移动3个结点,然后两个指针一起遍历,当第一个指针遍历完成,第二个指针指向的结点就是要找的结点
- 数组解题思路:双指针、三指针,下标标记
- 双指针:多用于减少时间复杂度,快速遍历数组
- 三指针:多用于二分查找,分为中间指针,左和右指针
- 下标标记:常用于在数组范围内找东西,而不想使用额外的空间的情况,比如找数组长度为n,元素取值范围为[1,n]的数组中没有出现的数字,遍历每个元素,然后将对应下标位置的元素变为负数或者超出[1,n]范围的正数,最后没有发生变化的元素,就是缺少的值。
- 栈解题思路:倒着入栈,双栈
- 倒着入栈:适用于出栈时想让输出是正序的情况。比如字符串’abc’,如果倒着入栈,那么栈中元素是(c,b,a)。栈是先进后出,此时出栈,结果为abc。
- 双栈:适用于实现队列的先入先出效果。一个栈负责输入,另一个栈负责输出。