详解「字符串匹配」的通用思路和技巧| Java 刷题打卡

简介: 详解「字符串匹配」的通用思路和技巧| Java 刷题打卡

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的 115. 不同的子序列 ,难度为 困难


Tag : 「线性 DP」


给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。


字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。


示例 1:


输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
复制代码


示例 2:


输入:s = "babgbag", t = "bag"
输出:5
解释:
如下图所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
(上箭头符号 ^ 表示选取的字母)
babgbag
^^ ^
babgbag
^^    ^
babgbag
^    ^^
babgbag
  ^  ^^
babgbag
    ^^^
复制代码


提示:


  • 0 <= s.length, t.length <= 1000
  • s 和 t 由英文字母组成


基本思路



有两个字符串 st,长度数量级都为 10^3103


一个朴素的想法是,找出所有 s 的子序列,与 t 进行比较,找所有子序列的复杂度是 O(2^n)O(2n),肯定会超时。


因此,我们放弃这种朴素思路。


字符串匹配也不具有二段性质,不可能有 loglog 级别的算法,那么复杂度再往下优化就是 O(n * m)O(nm) 的递推 DP 做法了。


动态规划



DP 的状态定义猜测通常是一门经验学科。


但是,对于两个字符串匹配,一个非常通用的状态定义如下:


定义 f[i][j]f[i][j] 为考虑 s[0,i][0,i] 个字符,t[0,j][0,j] 个字符的匹配个数。


那么显然对于某个 f[i][j]f[i][j] 而言,从「最后一步」的匹配进行分析,包含两类决策:


  • 不让 s[i] 参与匹配,也就是需要让 s[0,i-1][0,i1] 个字符去匹配 t 中的 [0,j][0,j] 字符。此时匹配值为 f[i-1][j]f[i1][j]
  • s[i] 参与匹配,这时候只需要让 s[0,i-1][0,i1] 个字符去匹配 t 中的 [0,j-1][0,j1] 字符即可,同时满足 s[i]=t[j]。此时匹配值为 f[i-1][j-1]f[i1][j1]


最终 f[i][j]f[i][j] 就是两者之和。


代码:


class Solution {
    public int numDistinct(String s, String t) {
        // 技巧:往原字符头部插入空格,这样得到 char 数组是从 1 开始
        // 同时由于往头部插入相同的(不存在的)字符,不会对结果造成影响,而且可以使得 f[i][0] = 1,可以将 1 这个结果滚动下去
        int n = s.length(), m = t.length();
        s = " " + s;
        t = " " + t;
        char[] cs = s.toCharArray(), ct = t.toCharArray();
        // f(i,j) 代表考虑「s 中的下标为 0~i 字符」和「t 中下标为 0~j 字符」是否匹配
        int[][] f = new int[n + 1][m + 1];
        // 原字符只有小写字符,当往两个字符插入空格之后,f[i][0] = 1 是一个显而易见的初始化条件
        for (int i = 0; i <= n; i++) f[i][0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                // 包含两种决策:
                // 不使用 cs[i] 进行匹配,则有 f[i][j] = f[i - 1][j]
                f[i][j] = f[i - 1][j];
                // 使用 cs[i] 进行匹配,则要求 cs[i] == ct[j],然后有  f[i][j] += f[i - 1][j - 1]
                if (cs[i] == ct[j]) {
                    f[i][j] += f[i - 1][j - 1];
                } 
            }
        }
        return f[n][m];
    }
}
复制代码


  • 时间复杂度:O(n * m)O(nm)
  • 空间复杂度:O(n * m)O(nm)


PS. 需要说明的是,由于中间结果会溢出,CPP 中必须使用 long long,而 Java 不用。由于 Java 中 int 的存储机制,只要在运算过程中只要不涉及取 min、取 max 或者其他比较操作的话,中间结果溢出不会影响最终结果。


总结



  1. 关于字符串匹配,通常有两种(你也可以理解为一种)通用的状态定义:
  • f[i][j]f[i][j] 表示「第一个字符 s[0,i][0,i] 个字符」与「第二个字符 t[0,j][0,j] 个字符」的匹配结果
  • f[i][j]f[i][j] 表示「第一个字符 s[0,i][0,i] 个字符」与「第二个字符 t[0,j][0,j] 个字符」且 「最后一个字符为 t[j]」的匹配结果
  1. 往两个字符串的头部追加「不存在」的字符,目的是为了能够构造出可以滚动(被累加)下去的初始化值


进阶



事实上,关于字符串匹配问题,还有一道稍稍不同的变形的题目。


也是利用了类似的「通用思路」(状态定义) &「技巧」,然后对匹配过程中的字符进行分情况讨论,学有余力的同学可以看看:


10. 正则表达式匹配 : 如何利用的「等差」性质降低「正则字符串匹配」算法复杂度 ...


最后



这是我们「刷穿 LeetCode」系列文章的第 No.115 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
2月前
|
SQL JSON Java
告别拼接噩梦:Java文本块让多行字符串更优雅
告别拼接噩梦:Java文本块让多行字符串更优雅
361 82
|
2月前
|
自然语言处理 Java Apache
在Java中将String字符串转换为算术表达式并计算
具体的实现逻辑需要填写在 `Tokenizer`和 `ExpressionParser`类中,这里只提供了大概的框架。在实际实现时 `Tokenizer`应该提供分词逻辑,把输入的字符串转换成Token序列。而 `ExpressionParser`应当通过递归下降的方式依次解析
193 14
|
6月前
|
存储 缓存 安全
Java 字符串详解
本文介绍了 Java 中的三种字符串类型:String、StringBuffer 和 StringBuilder,详细讲解了它们的区别与使用场景。String 是不可变的字符串常量,线程安全但操作效率较低;StringBuffer 是可变的字符串缓冲区,线程安全但性能稍逊;StringBuilder 同样是可变的字符串缓冲区,但非线程安全,性能更高。文章还列举了三者的常用方法,并总结了它们在不同环境下的适用情况及执行速度对比。
153 17
|
6月前
|
存储 缓存 安全
Java字符串缓冲区
字符串缓冲区是用于处理可变字符串的容器,Java中提供了`StringBuffer`和`StringBuilder`两种实现。由于`String`类不可变,当需要频繁修改字符串时,使用缓冲区更高效。`StringBuffer`是一个线程安全的容器,支持动态扩展、任意类型数据转为字符串存储,并提供多种操作方法(如`append`、`insert`、`delete`等)。通过这些方法,可以方便地对字符串进行添加、插入、删除等操作,最终将结果转换为字符串。示例代码展示了如何创建缓冲区对象并调用相关方法完成字符串操作。
137 13
|
10月前
|
SQL Java 索引
java小工具util系列2:字符串工具
java小工具util系列2:字符串工具
242 83
|
安全 Java API
【Java字符串操作秘籍】StringBuffer与StringBuilder的终极对决!
【8月更文挑战第25天】在Java中处理字符串时,经常需要修改字符串,但由于`String`对象的不可变性,频繁修改会导致内存浪费和性能下降。为此,Java提供了`StringBuffer`和`StringBuilder`两个类来操作可变字符串序列。`StringBuffer`是线程安全的,适用于多线程环境,但性能略低;`StringBuilder`非线程安全,但在单线程环境中性能更优。两者基本用法相似,通过`append`等方法构建和修改字符串。
203 1
|
10月前
|
存储 安全 Java
Java零基础-字符串详解
【10月更文挑战第18天】Java零基础教学篇,手把手实践教学!
171 60
|
10月前
|
Java 数据库
java小工具util系列1:日期和字符串转换工具
java小工具util系列1:日期和字符串转换工具
172 26
|
10月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
282 8

热门文章

最新文章