题目描述

✅ 392. 判断子序列

image-20260928221302288

image-20260928221302289

题意分析

判断是否能从字符串 t 中删除若干字符,使剩余字符恰好组成 s。保留下来的字符必须保持原有相对顺序,但不必连续,同一个位置只能使用一次。

空字符串是任意字符串的子序列;只有 s 的所有字符都按顺序找到才算成功。原题还要求考虑固定 t、需要判断大量不同 s 的情形,下面补充一次预处理后批量查询的方法。

解法:双指针贪心匹配

核心思路

[!blue]

用 i 指向 s 中下一个需要匹配的字符,用 j 从左到右扫描 t。扫描期间,s[0..i-1] 已经在 t[0..j-1] 中按顺序匹配完成。

如果当前字符相等,就用 t[j] 匹配 s[i],并推进 i;不相等就跳过 t[j]。无论是否匹配,j 都前进一步,避免重复使用同一个文本位置。

遇到相等字符时为什么可以立即使用?设某个可行方案用更靠后的相同字符匹配当前需求,把它替换成眼前这个更早位置后,后续匹配位置仍在它右边,原方案依然可行。选择最早位置只会给后续字符留下更多空间,不会丢掉任何解,所以不需要回溯。

当 i 到达 s 末尾时已经成功;若 t 耗尽而 i 尚未到末尾,则剩余字符没有可用位置。空 s 在进入循环前就已全部匹配,返回条件自然得到 true。

解题步骤

  1. 令 i = 0,表示还没有匹配 s 的任何字符。
  2. 从左到右扫描 t,同时确保 i < s.length(),避免访问已匹配完成的字符串。
  3. 当前字符相等时令 i++,否则只继续扫描 t。
  4. s 匹配完或 t 扫描完后停止,返回 i == s.length()。

代码实现

class Solution {
    public boolean isSubsequence(String s, String t) {
        int i = 0;

        for (int j = 0; i < s.length() && j < t.length(); j++) {
            // 只在命中当前所需字符时推进模式,文本每轮都前进。
            if (s.charAt(i) == t.charAt(j)) {
                i++;
            }
        }

        return i == s.length();
    }
}
func isSubsequence(s string, t string) bool {
    i := 0
    for j := 0; i < len(s) && j < len(t); j++ {
        // 只在命中当前所需字符时推进模式,文本每轮都前进。
        if s[i] == t[j] {
            i++
        }
    }
    return i == len(s)
}

复杂度分析

  • 时间复杂度:$O(m)$,其中 $m$ 是 t 的长度。文本指针最多前进 $m$ 次,模式指针只随匹配前进。
  • 空间复杂度:$O(1)$,只保存两个下标。

关键点总结

[!green]

  • 子序列需要顺序一致,允许跳过文本字符,不能用子串查找或字符计数替代。
  • 总选择最早可用位置,为剩余需求保留尽可能长的后缀。
  • i 表示已匹配长度,只有命中才增加;文本位置每轮都要消耗。

进阶解法:预处理后继位置应对多次查询

核心思路

[!blue]

固定 t 后反复扫描同一段文本,会产生大量重复工作。可以提前记录:从每个位置出发,下一个指定字符最早出现在哪里。输入只有小写字母,因此每个位置只需记录 26 种字符。

令 next[pos][c] 表示从 pos 开始、包含 pos 的后缀中,字符 c 第一次出现的下标。文本长度为 m,用 m 表示不存在;额外创建第 m 行并全部填为 m,代表空后缀。

从右向左建立表。先把后一行复制到当前行,因为除当前位置的字符外,其他字符的最早位置都不变;再令 next[pos][t[pos]] = pos,让当前字符指向自己。

查询一个 s 时,从 pos = 0 开始,逐字符直接查出最早可用位置。查到 m 说明没有匹配,立即失败;否则令 pos = found + 1,保证下一个字符只使用其后的文本。匹配策略与双指针完全相同,只是把扫描过程压缩成一次查表。

下方查询器创建时只接收固定文本并预处理一次,之后每次传入一个模式进行判断。模式可以逐条处理,不必把海量查询或全部结果同时保存在内存中。

解题步骤

  1. 创建 (m + 1) × 26 的后继表,将空后缀的全部后继设为 m。
  2. 从文本末尾向前复制后一行,并更新当前字符的最早下标。
  3. 对每个模式从位置零开始,逐字符查询后继;找到后移动到其下一位。
  4. 任一字符没有后继则该模式失败;全部字符找到则成功。

代码实现

class SubsequenceMatcher {
    private final int[][] next;

    public SubsequenceMatcher(String t) {
        int m = t.length();
        next = new int[m + 1][26];
        for (int c = 0; c < 26; c++) {
            next[m][c] = m;
        }
        for (int pos = m - 1; pos >= 0; pos--) {
            System.arraycopy(next[pos + 1], 0, next[pos], 0, 26);
            next[pos][t.charAt(pos) - 'a'] = pos;
        }
    }

    public boolean isSubsequence(String s) {
        int m = next.length - 1;
        int pos = 0;
        for (int i = 0; i < s.length(); i++) {
            int found = next[pos][s.charAt(i) - 'a'];
            if (found == m) {
                return false;
            }
            pos = found + 1;
        }
        return true;
    }
}
type SubsequenceMatcher struct {
    next [][26]int
}

func NewSubsequenceMatcher(t string) *SubsequenceMatcher {
    m := len(t)
    next := make([][26]int, m+1)
    for c := 0; c < 26; c++ {
        next[m][c] = m
    }
    for pos := m - 1; pos >= 0; pos-- {
        next[pos] = next[pos+1]
        next[pos][t[pos]-'a'] = pos
    }
    return &SubsequenceMatcher{next: next}
}

func (matcher *SubsequenceMatcher) IsSubsequence(s string) bool {
    m := len(matcher.next) - 1
    pos := 0
    for i := 0; i < len(s); i++ {
        found := matcher.next[pos][s[i]-'a']
        if found == m {
            return false
        }
        pos = found + 1
    }
    return true
}

复杂度分析

  • 时间复杂度:$O(26m+q+L)$,其中 $m$ 为文本长度,$q$ 为模式个数,$L$ 为模式总长度。预处理为 $O(26m)$,每个模式字符至多查询一次,查询可以逐条输入。
  • 空间复杂度:$O(26m)$,保存全部查询共享的后继表;每次查询额外只使用常数空间。

关键点总结

[!green]

  • 双指针每次寻找的都是最早后继,可以将这一步提前制表。
  • 第 m 行统一处理文本已用完的情况,避免越界和特殊分支。
  • 查到字符后从下一位继续,保证同一个文本位置最多使用一次。
  • 单次判断用双指针即可,大量模式共用一个文本时才适合这份预处理。

易错点总结

[!yellow]

  • 匹配后只推进 i 而不推进文本位置,会把同一个字符重复使用。
  • 只比较字符频次会丢失相对顺序,无法判断子序列。
  • 成功条件是 s 已匹配完,不是 t 已扫描完。
  • 空模式应返回 true;后继表查询也要允许从位置 m 开始查询,并用哨兵表示找不到。
  • 多次查询只共享文本预处理,每个新模式的匹配位置必须重新从零开始。

相似题目

题目 难度 关联与区别
522. 最长特殊序列 II 中等 该题逐个检查候选字符串是否为其他字符串的子序列,可直接复用本题双指针匹配;只在外层增加候选枚举与长度筛选。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/70984513
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!