力扣解题-392. 判断子序列

4 阅读6分钟

力扣解题-392. 判断子序列

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

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

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

致谢: 特别感谢 @pbrother 添加此问题并且创建所有测试用例。

示例 1: 输入:s = "abc", t = "ahbgdc" 输出:true

示例 2: 输入:s = "axc", t = "ahbgdc" 输出:false

提示: 0 <= s.length <= 100 0 <= t.length <= 10^4 两个字符串都只由小写字符组成。

Related Topics 双指针、字符串、动态规划


第一次解答

解题思路

核心方法:双指针贪心匹配法,用两个指针分别遍历st,逐个匹配字符且保持相对顺序,是本题最基础、空间效率最优的解法,时间复杂度O(n)(n为t的长度)、空间复杂度O(1)。

核心逻辑拆解

判断子序列的核心是“按顺序匹配,不回头”:

  1. i指针遍历s(待匹配的子序列),j指针遍历t(原始字符串);
  2. 遍历过程中,若s[i] == t[j],说明s的第i个字符匹配成功,i指针右移(继续匹配下一个字符);
  3. 无论是否匹配成功,j指针都右移(t的字符只能向后走,不能回头);
  4. i遍历完si == s.length()),说明s的所有字符都按顺序匹配完成,返回true
  5. j遍历完ti未遍历完s,说明匹配失败,返回false
具体步骤(以示例1 s="abc",t="ahbgdc"为例)
  1. 初始:i=0(指向'a'),j=0(指向'a');
  2. s[0] == t[0] → i=1(指向'b'),j=1(指向'h');
  3. s[1] != t[1] → j=2(指向'b');
  4. s[1] == t[2] → i=2(指向'c'),j=3(指向'g');
  5. s[2] != t[3] → j=4(指向'd');
  6. s[2] != t[4] → j=5(指向'c');
  7. s[2] == t[5] → i=3(等于s.length()=3),循环结束,返回true。
性能说明
  • 时间复杂度:O(n)(仅遍历t一次,s的遍历次数最多等于其长度,可忽略);
  • 空间复杂度:O(1)(仅使用两个指针变量,无额外内存开销),内存消耗41.9MB击败98.10%用户;
  • 耗时说明:2ms击败54.35%用户,是因为该解法是基础实现,无预处理优化,但逻辑简单、稳定性高;
  • 边界处理:
    • s为空字符串,直接返回true(空字符串是任何字符串的子序列);
    • t为空但s非空,返回false(代码中j会快速遍历完ti未遍历完s)。
    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();
    }

示例解答

解题思路

解法1:预处理+二分查找(进阶场景最优解)

核心方法:预处理t的字符位置 + 二分查找匹配,针对进阶问题(大量s需要匹配同一个t),先对t做一次预处理,后续每个s的匹配时间复杂度降为O(m×logn)(m为s长度,n为t长度),大幅提升批量匹配效率。

核心原理铺垫(针对进阶场景)

当有海量s需要匹配时,双指针法需要对每个s都遍历一次t(总时间O(k×n),k为s的数量),效率极低。预处理法的核心是:

  1. 提前为t中每个字符(a-z)建立“出现位置的有序列表”;
  2. 匹配s时,对每个字符,用二分查找在对应列表中找“大于上一个匹配位置的最小位置”,确保相对顺序。
具体步骤
  1. 预处理t
    • 创建长度为26的列表数组pospos[c]存储字符c('a'-'z')在t中出现的所有下标,且按升序排列;
    • 遍历t,将每个字符的下标添加到对应列表中(如t="ahbgdc"pos['a'-'a'] = [0]pos['b'-'a'] = [2]);
  2. 匹配s
    • 初始化prev = -1(记录上一个匹配字符在t中的位置,初始为-1);
    • 遍历s的每个字符c
      • pos[c-'a']中用二分查找找“大于prev的最小下标”;
      • 若找不到,返回false;若找到,更新prev为该下标;
    • 遍历完成后返回true
代码实现
import java.util.ArrayList;
import java.util.List;
import java.util.Collections;

public boolean isSubsequence(String s, String t) {
    // 预处理t:为每个字符建立位置列表
    List<Integer>[] pos = new List[26];
    for (int i = 0; i < 26; i++) {
        pos[i] = new ArrayList<>();
    }
    for (int j = 0; j < t.length(); j++) {
        char c = t.charAt(j);
        pos[c - 'a'].add(j);
    }

    int prev = -1; // 上一个匹配字符的位置
    for (char c : s.toCharArray()) {
        List<Integer> list = pos[c - 'a'];
        // 二分查找大于prev的最小下标
        int idx = Collections.binarySearch(list, prev + 1);
        if (idx < 0) {
            idx = -idx - 1; // 转换为插入位置
        }
        // 找不到符合条件的下标,匹配失败
        if (idx >= list.size()) {
            return false;
        }
        // 更新上一个位置
        prev = list.get(idx);
    }
    return true;
}
优势说明
  • 预处理成本:O(n)(仅需处理一次t);
  • 单条s匹配成本:O(m×logn)(m≤100,logn≤14),远低于双指针的O(n);
  • 进阶场景适配:当k≥10亿时,预处理+二分的总时间O(n + k×m×logn),远优于双指针的O(k×n),是唯一可行的方案。
解法2:动态规划法(理解子序列匹配的另一种思路)

核心方法:预处理t的跳转表dp[i][c]表示在t的第i个位置之后,字符c第一次出现的位置,通过动态规划构建跳转表,匹配时直接查表跳转。

代码实现
public boolean isSubsequence(String s, String t) {
    int n = t.length();
    int m = s.length();
    // dp[i][c]:t中第i个位置之后,字符c('a'-'z')第一次出现的位置
    int[][] dp = new int[n + 1][26];
    
    // 初始化最后一个位置(n)的跳转表:所有字符都为n(表示不存在)
    for (int c = 0; c < 26; c++) {
        dp[n][c] = n;
    }
    
    // 从后往前构建跳转表
    for (int i = n - 1; i >= 0; i--) {
        for (int c = 0; c < 26; c++) {
            if (t.charAt(i) == 'a' + c) {
                dp[i][c] = i; // 当前字符就是c,跳转位置为i
            } else {
                dp[i][c] = dp[i + 1][c]; // 继承下一个位置的跳转结果
            }
        }
    }
    
    // 匹配s
    int idx = 0; // 当前在t中的位置
    for (char c : s.toCharArray()) {
        int cIdx = c - 'a';
        if (dp[idx][cIdx] == n) { // 找不到该字符
            return false;
        }
        idx = dp[idx][cIdx] + 1; // 跳转到该字符的下一个位置
    }
    return true;
}
优势说明
  • 匹配效率:O(m)(查表跳转,无需遍历t),适合批量匹配场景;
  • 逻辑价值:动态规划的跳转表思路是字符串匹配的经典技巧,可扩展到更复杂的子序列问题;
  • 空间开销:O(n×26)(n≤10⁴,总空间约26万),在可接受范围内。

总结

  1. 双指针法(第一次解答):基础解法,空间O(1),适合单条s匹配,逻辑简单易实现;
  2. 预处理+二分法:进阶场景最优解,预处理一次t后,批量匹配海量s效率极高;
  3. 动态规划法:另一种批量匹配思路,查表跳转更直观,空间开销略高于二分法;
  4. 关键选择技巧:
    • 单条s匹配:优先选双指针法(空间最优);
    • 海量s匹配:优先选预处理+二分法(时间最优,空间开销更小);
    • 理解子序列匹配逻辑:可学习动态规划跳转表思路。