子串、子序列问题 专题

351 阅读6分钟

子串问题

LC-3 无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/lo…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

使用滑动窗口方法,维护left和right两个指针,不断增加right来扩大窗口范围,当窗口不满足要求(窗口内出现重复字符)的时候,增加left来缩小窗口范围,直到窗口再次满足要求。

public int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> window = new HashMap<>();

    int left = 0, right = 0;
    int max = 0;
    while (right < s.length()) {
        char c = s.charAt(right);
        right++;

        window.put(c, window.getOrDefault(c, 0) + 1);

        while (window.get(c) > 1) {
            char d = s.charAt(left);
            left++;
            window.put(d, window.get(d) - 1);
        }
        max = Math.max(max, right - left);
    }
    return max;
}

LC-5 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/lo…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

寻找回文串的问题核心思想是:从中间开始向两边扩散来判断回文串。对于最长回文子串,就是这个意思:

for 0 <= i < len(s):
    找到以 s[i] 为中心的回文串
    更新答案

但是回文串的长度可能是奇数也可能是偶数,比如abba这种情况,因此需要考虑长度是奇数和偶数两种情况:

for 0 <= i < len(s):
    找到以 s[i] 为中心的回文串
    找到以 s[i]s[i+1] 为中心的回文串
    更新答案
public String longestPalindrome(String s) {
    if (s == null || s.length() < 2) return s;

    String ans = "";
    for (int i = 0; i < s.length(); i++) {
        String s1 = palindrome(s, i, i);
        String s2 = palindrome(s, i, i + 1);
        ans = ans.length() > s1.length() ? ans : s1;
        ans = ans.length() > s2.length() ? ans : s2;
    }
    return ans;
}

private 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 + 1, r);
}

LC-125 验证回文串

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/va…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

使用两个指针分别指向头和尾,依次判断是否相同字符,注意要跳过非字母和数字的字符。

public boolean isPalindrome(String s) {
    if (s == null || s.length() == 0) return true;
    s = s.toLowerCase();
    int low = 0, high = s.length() - 1;
    while (low < high){
        while (low < high && !Character.isLetterOrDigit(s.charAt(low)))
            low++;
        while (low < high && !Character.isLetterOrDigit(s.charAt(high)))
            high--;
        if (s.charAt(low) != s.charAt(high))
            return false;
        low++;
        high--;
    }
    return true;
}

LC-647 回文子串

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/pa…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

s[i..j]是否是回文串,依赖于s[i+1..j-1]是回文串且s[i] == s[j],比较容易想到动态规划解法。使用一个dp数组,用来存储子串是否是回文串。然后再遍历一遍dp数组,计算出所有回文子串的个数。

public int countSubstrings(String s) {
    int n = s.length();
    boolean[][] dp = new boolean[n][n];
    for (int i = 0; i < n; i++) {
        Arrays.fill(dp[i], true);
    }
    for (int i = n - 1; i >= 0; i--) {
        for (int j = i + 1; j < n; j++) {
            dp[i][j] = dp[i + 1][j - 1] && s.charAt(i) == s.charAt(j);
        }
    }

    int res = 0;
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            if (dp[i][j]) {
                res += 1;
            }
        }
    }
    return res;
}

LC-131 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/pa…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这道题是动态规划和回溯法结合起来的一道题目。通过动态规划得出s[i..j]子串是否是回文串,再通过回溯找到所有可能的分隔方案。

public List<List<String>> partition(String s) {
    int n = s.length();
    boolean[][] dp = new boolean[n][n];
    for (int i = 0; i < n; i++) {
        Arrays.fill(dp[i], true);
    }
    for (int i = n - 1; i >= 0; i--) {
        for (int j = i + 1; j < n; j++) {
            dp[i][j] = dp[i + 1][j - 1] && s.charAt(i) == s.charAt(j);
        }
    }

    List<List<String>> res = new ArrayList<>();
    LinkedList<String> ans = new LinkedList<>();
    dfs(s, 0, n, dp, ans, res);
    return res;
}

private void dfs(String s, int i, int n, boolean[][] dp, LinkedList<String> ans, List<List<String>> res) {
    if (i == n) {
        res.add(new ArrayList<>(ans));
        return;
    }

    for (int j = i; j < n; j++) {
        if (dp[i][j]) {
            ans.add(s.substring(i, j + 1));
            dfs(s, j + 1, n, dp, ans, res);
            ans.removeLast();
        }
    }
}

子序列问题

LC-392 判断子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/is…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

原题目思路非常简单,利用双指针 i, j 分别指向 s, t,一边前进一边匹配子序列。

public boolean isSubsequence(String s, String t) {
    int i = 0, j = 0;
    while (i < s.length() && j < t.length()) {
        if (s.charAt(i) == t.charAt(j)) {
            i++;
        }
        j++;
    }
    return i == s.length();
}

对于进阶题目,用一个循环依次检查字符串Sk是否是T的子序列,时间复杂度是O(N2)。利用二分查找可以降低到O(MlogN)

二分查找的思路是对T做预处理,把每一个字符出现的索引位置记录下来。在向前扫描某个字符c是否在T中时,只需要查找比j大的那个索引位置。

public boolean isSubsequence(String s, String t) {
    int m = s.length(), n = t.length();
    ArrayList<Integer>[] index = new ArrayList[256];
    for (int i = 0; i < n; i++){
        char c = t.charAt(i);
        if (index[c] == null) index[c] = new ArrayList<>();
        index[c].add(i);
    }

    int j = 0;
    for (int i = 0; i < m; i++) {
        char c = s.charAt(i);
        if (index[c] == null) return false;
        int pos = left_bound(index[c], j);
        if (pos == index[c].size()) return false;
        j = index[c].get(pos) + 1;
    }
    return true;
}

private int left_bound(ArrayList<Integer> list, int target) {
    int low = 0, high = list.size();
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (target > list.get(mid)) {
            low = mid + 1;
        } else {
            high = mid;
        }
    }
    return low;
}

LC-1143 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/lo…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

计算最长公共子序列(Longest Common Subsequence,简称 LCS)是一道经典的动态规划题目。

定义数组dp[i][j]表示text1[0..i-1]text2[0..j-1]的最长功能子序列,其中text1[0..i-1]表示text1中第0个元素到第i-1个元素,text2[0..j-1]表示text2中第0个元素到第j-1个元素。

  • text1[i-1] == text2[j-1]时,说明两个字符串中最后一个字符相等,因此最长公共子序列增加了1,所以有dp[i][j] = dp[i-1][j-1] + 1
  • 否则,说明最后一个字符不相等,分别把最后一个字符放进去看哪个的最长公共子序列更长,所以有dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])

初始化要考虑当 i = 0j = 0 时, dp[i][j]应该取值为多少。

i = 0 时,dp[0][j] 表示的是 text1 中取空字符串跟 text2 的最长公共子序列,结果肯定为 0. 当 j = 0 时,dp[i][0] 表示的是 text2 中取空字符串跟 text1 的最长公共子序列,结果肯定为 0. 综上,当 i = 0 或者 j = 0 时,dp[i][j] 初始化为 0.

public int longestCommonSubsequence(String text1, String text2) {
    int m = text1.length(), n = text2.length();
    int[][] dp = new int[m + 1][n + 1];

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (text1.charAt(i - 1) == text2.charAt(j - 1))
                dp[i][j] = dp[i - 1][j - 1] + 1;
            else
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
        }
    }
    return dp[m][n];
}

LC-300 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/lo…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

定义dp[i]标识以第i个数字结尾的最长递增子序列的长度。考虑将nums[i]加入到子序列的时候,要看从0到i-1构成的各个子序列,是否能和nums[i]构成新的递增子序列,能构成的话再计算最长的那个,就是以nums[i]为结尾的最长递增子序列的长度。

状态转移方程可表示为:

dp[i] = max(dp[j]) + 1,其中0 <= j < inums[j] < nums[i]

public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) return 0;

        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }

        int res = 0;
        for (int i = 0; i < dp.length; i++) {
            res = Math.max(res, dp[i]);
        }
        return res;
    }

LC-516 最长回文子序列

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/lo…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

定义dp[i][j]表示在子串s[i..j]中的最长回文子序列的长度。根据s[i]s[j],可能有如下两种情况:

  • s[i] == s[j]:那他俩加上s[i+1..j-1]的最长回文子序列长度即可,因此有dp[i][j] = dp[i+1][j-1] + 2
  • s[i] != s[j]:说明它们不可能同时出现在s[i..j]的最长回文子序列中,那就分别试试看哪个更大,因此有dp[i][j] = max(dp[i+1][j], dp[i][j-1])
public int longestPalindromeSubseq(String s) {
    int N = s.length();
    int[][] dp = new int[N][N];
    // base case
    for (int i = 0; i < N; i++)
        dp[i][i] = 1;

    for (int i = N - 1; i >= 0; i--) {
        for (int j = i + 1; j < N; j++) {
            if (s.charAt(i) == s.charAt(j)) {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            } else {
                dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[0][N - 1];
}

LC-594 最长和谐子序列

和谐数组是指一个数组里元素的最大值和最小值之间的差别 正好是 1 。

现在,给你一个整数数组 nums ,请你在所有可能的子序列中找到最长的和谐子序列的长度。

数组的子序列是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素、且不改变其余元素的顺序而得到。

来源:力扣(LeetCode)

链接:leetcode-cn.com/problems/lo…

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

public int findLHS(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        int res = 0;
        for (int num : map.keySet()) {
            if (map.containsKey(num + 1))
                res = Math.max(res, map.get(num) + map.get(num + 1));
        }
        return res;
    }