子串问题题解汇总 ---通俗易懂版
找到字符串的最长无重复字符子串
https://www.nowcoder.com/practice/b56799ebfd684fb394bd315e89324fb4?tpId=190&&tqId=35220&rp=1&ru=/ta/job-code-high-rd&qru=/ta/job-code-high-rd/question-ranking
本文参考了几篇题解区大佬的文章,对以下做过的子串问题进行解析和总结。
找到字符串的最长无重复字符子串
方法一 滑动窗口法:
- 我们可以使用双指针模拟一个滑动窗口。
- 初始化窗口为 (left, right]。 所以left从-1开始。
- 窗口不断往右扩大。
- 根据题目的要求,即遇到有重复数字的,在窗口左侧缩小。
- 在每次滑动时,对窗口的大小进行比较,保留最大的长度。
代码实现:
public int maxLength (int[] arr) { int res = 0, left = -1; //用来存放窗口存放过的数字 HashMap<Integer, Integer> windows = new HashMap<>(); //窗口不断往右移 for(int right = 0; right < arr.length; right++){ //根据题目,当遇到重复的数字时,缩小左侧窗口 if( windows.containsKey(arr[right])){ //因为我们有可能遇到索引比left原来还小的相同数字 //所以这里要进行比较,目的还是为了缩小左侧窗口,确保窗口内全是不重复的数字 left = Math.max(left, windows.get(arr[right])); } //更新窗口内数字的索引 windows.put(arr[right], right); //right-left是窗口大小 //因为要找最长,所以要进行比较 res = Math.max(res, right-left); } return res; }
方法二 **双指针+回头遍历**:
过程可以看注释
public int maxLength (int[] arr) { int res = 0, tmp = 0; //right指针往右移动 for(int right = 0; right < arr.length; right++){ int left = right - 1; //回头扫描,要是没有找到相同的,左指针一直倒退 while(left >= 0 && arr[right] != arr[left]) left--; //暂时保存子串长度 //若指针距离比上一个字符时拥有的子串长度大,就tmp + 1,否则就设置为指针距离,方便下一步res进行比较 tmp = tmp < right - left ? tmp + 1 : right - left; res = Math.max(res,tmp); } return res;
最长公共子串
这道题有点类似 最长公共子序列。
公共子串和公共子序列的区别
- 公共子串:必须是连续的。
- 公共子序列:不必是连续的,但是相对位置必须不能改变。
好,接下来,我们上动态规划解法:
- 其实解法跟最长公共子序列的是差不多的。
- 先确定状态和选择
- 状态有两个,分别是str1 和 str2 的索引 i,j 变化
- 选择有两个,当str1[i-1] 和 str2[j-1]相等时,dp[i][j] + 1;当不相等时,dp[i][j]置为0。
- 确定状态转移方程和 base case
- 这里画个图就清晰多了
当字符相等时,dp[i][j] = dp[i-1][j-1] + 1;
不相等时,dp[i][j] = 0 - base case:dp[0][j] 和 dp[i][0] 全部置为0。
- 这里画个图就清晰多了
代码实现:
public String LCS(String str1, String str2){ if(str1.length() == 0 || str2.length() == 0){ return "-1"; } //起始位置 int start1 = 0; //偏移量 int max = 0; int len1 = str1.length(); int len2 = str2.length(); int[][] dp = new int[len1+1][len2+1]; for(int i = 1; i <= len1; i++){ for(int j = 1; j <= len2; j++){ if(str1.charAt(i-1) == str2.charAt(j-1)){ //将当前相等的字符计入 + 1 dp[i][j] = dp[i-1][j-1] + 1; }else { dp[i][j] = 0; } if(max < dp[i][j]){ //max记录已经拥有的子串长度 max = dp[i][j]; //因为字符串索引是从0开始的,所以 i - max 刚好是起始位置 start1 = i - max; } } } if(max == 0) return "-1"; return str1.substring(start1, max+start1); }
最长回文子串
代码实现:
public class Palindrome { public int getLongestPalindrome(String A, int n) { int res = 0; //暴力解法 for(int i = 0; i < n; i++){ for(int j = i+1; j <= n; j++){ String tmp = A.substring(i,j); //调用判断是否为回文子串的方法 if(isPalindrome(tmp)){ res = Math.max(res, tmp.length()); } } } return res; } public boolean isPalindrome(String s) { int len = s.length(); for(int i=0; i< len/2; i++){ if(s.charAt(i) != s.charAt(len-1-i)) return false; } return true; } }
回文问题
寻找最长回文子串,不是求长度
其实大同小异,也是同样使用双指针法
public class Palindrome{ String res; public String longest(String s){ res = new String(); for(int i = 0; i < s.length(); i++){ //寻找长度为基数的回文子串 String s1 = String palindrome(s, i, i); //寻找长度为偶数的回文子串 String s2 = String palindrome(s, i, i+1); res = res.length() > res : s1; res = res.length() > res : s2; } return res; } public String palindrome(String s, int l, int r){ while( l >= 0 && r < s.length(); && s.charAt(l) == s.charAt(r)){ l--; r++; } return s.subString(l, r+1); } }
最长回文串 不是子串
class Solution { public int longestPalindrome(String s) { //因为在ASCII码中,A到z是65到122 总共58个 int[] cnt = new int[58]; for(char c : s.toCharArray()){ cnt[c - 'A'] += 1; } int ans = 0; for(int x : cnt){ //如果是偶数 就ans+x; 如果是奇数 就减掉一个再ans+x ans += x - (x & 1); } //回文串要么全是偶数个数 要么除了偶数个数外,只有一个奇数 return ans < s.length() ? ans + 1 : ans; } }
最长的括号子串
绝了这道题,一开始写了个使用栈的解法,测试一直过不了。后来看了题解,才发现自己理解错题目了呜呜呜。 上正确解法:
方法一 使用栈 :
这个应该是很容易想到的解法
- 栈存放的是括号的索引。
- 当遇到 '('时,入栈,当遇到 ')'时,如果栈为空,将它放进去;否则弹出栈顶,再将当前索引减去栈顶的索引,再跟原来的res进行比较,更新res值,保证取到最大的数字。
代码实现:
public int longestValidParentheses (String s) { int res = 0; Stack<Integer> stack = new Stack<>(); stack.push(-1); for(int i=0; i < s.length(); i++){ if(s.charAt(i) == '('){ stack.push(i); } else { stack.pop(); if(stack.empty()) { stack.push(i); } else { res = Math.max(res, i - stack.peek()); } } } return res; }
方法二 动态规划:
dp数组存放的是当前所拥有的括号子串长度。
在动态规划框架上,处理了三种情况:
- 当前字符为 '(',跳过,默认初始化为0;
- 当前字符为 ')',又分为两种情况:
- 当前面只有单个'()'出现或者判断字符正处于嵌套括号的最里层时,dp[i] = dp[i-2] + 2;
- 当处于嵌套括号的外层时, dp[i] = dp[i-1] + dp[i-dp[i-1]-2] + 2;
代码实现:
public int longestValidParentheses (String s) { int res = 0; int[] dp = new int[s.length()]; for(int i = 1; i < s.length(); i++){ //只对当前字符是')'进行处理 if(s.charAt(i) == ')'){ //当()时 if(s.charAt(i-1) == '('){ dp[i] = (i >= 2 ? dp[i-2] : 0) + 2; } else if(i - dp[i-1] > 0 && s.charAt(i - dp[i-1] - 1) == '('){ //当 (())时 dp[i] = dp[i-1] + ((i-dp[i-1]) >= 2 ? dp[i - dp[i-1] - 2] : 0) + 2; } res = Math.max(res, dp[i]); } } return res; }
最长的覆盖子串
这道题也可以用滑动窗口方法:
与第一题不同的是,窗口左侧收缩的满足条件是 当S包含T的所有字符时。
代码实现:
public String minWindow (String S, String T) { // write code here HashMap<Character, Integer> need, window; need = new HashMap<Character, Integer>(); window = new HashMap<Character, Integer>(); for(int i=0; i < T.length(); i++){ char c = T.charAt(i); need.put(c,need.getOrDefault(c,0)+1); } int left = 0, right = 0; //包含目标T的字符数 int valid = 0; //记录最小覆盖子串的起始索引及长度 int start = 0, len = Integer.MAX_VALUE; //窗口不断往右移 while(right < S.length()){ char c = S.charAt(right); //右移窗口 right++; //进行窗口内数据更新 if(need.containsKey(c)){ window.put(c,window.getOrDefault(c,0)+1); if(window.get(c) == need.get(c)) valid++; } //判断左侧是否进行收缩 while(valid == need.size()){ //更新最小覆盖子串 if(right - left < len){ start = left; len = right - left; } char d = S.charAt(left); //左移窗口 left++; //进行窗口内数据更新 if(need.containsKey(d)) { if(window.get(d) == need.get(d)) valid--; window.put(d,window.get(d)-1); } } } return len == Integer.MAX_VALUE ? "" : S.substring(start,start+len); }