动态规划之正则表达.md 10.2 KB
Newer Older
L
labuladong 已提交
1 2
---
title: '动态规划之正则表达'
L
labuladong 已提交
3
tags: ['动态规划', '字符串']
L
labuladong 已提交
4
---
L
labuladong 已提交
5

6 7
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
8
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
9 10 11
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>
L
labuladong 已提交
12

L
labuladong 已提交
13
![](https://labuladong.github.io/pictures/souyisou1.png)
L
labuladong 已提交
14

L
labuladong 已提交
15
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.1,[手把手刷二叉树系列课程](https://aep.xet.tech/s/3YGcq3) 上线。[第 18 期每日打卡](https://aep.xet.tech/s/2PLO1n) 开始报名。反馈或修正 chatGPT 翻译的多语言代码 [点击这里](https://github.com/labuladong/fucking-algorithm/issues/1113)。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
L
labuladong 已提交
16 17


L
labuladong 已提交
18 19 20 21 22 23 24

读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [10. Regular Expression Matching](https://leetcode.com/problems/regular-expression-matching/) | [10. 正则表达式匹配](https://leetcode.cn/problems/regular-expression-matching/) | 🔴
| - | [剑指 Offer 19. 正则表达式匹配](https://leetcode.cn/problems/zheng-ze-biao-da-shi-pi-pei-lcof/) | 🔴
L
labuladong 已提交
25

26
**-----------**
L
labuladong 已提交
27

28 29 30 31 32 33 34 35 36
正则表达式是一个非常强力的工具,本文就来具体看一看正则表达式的底层原理是什么。力扣第 10 题「正则表达式匹配」就要求我们实现一个简单的正则匹配算法,包括「.」通配符和「*」通配符。

这两个通配符是最常用的,其中点号「.」可以匹配任意一个字符,星号「*」可以让之前的那个字符重复任意次数(包括 0 次)。

比如说模式串 `".a*b"` 就可以匹配文本 `"zaaab"`,也可以匹配 `"cb"`;模式串 `"a..b"` 可以匹配文本 `"amnb"`;而模式串 `".*"` 就比较牛逼了,它可以匹配任何文本。

题目会给我们输入两个字符串 `s``p``s` 代表文本,`p` 代表模式串,请你判断模式串 `p` 是否可以匹配文本 `s`。我们可以假设模式串只包含小写字母和上述两种通配符且一定合法,不会出现 `*a` 或者 `b**` 这种不合法的模式串,

函数签名如下:
L
labuladong 已提交
37

L
labuladong 已提交
38
<!-- muliti_language -->
L
labuladong 已提交
39
```cpp
40
bool isMatch(string s, string p);
L
labuladong 已提交
41 42
```

43 44 45 46 47 48 49 50 51 52 53
对于我们将要实现的这个正则表达式,难点在那里呢?

点号通配符其实很好实现,`s` 中的任何字符,只要遇到 `.` 通配符,无脑匹配就完事了。主要是这个星号通配符不好实现,一旦遇到 `*` 通配符,前面的那个字符可以选择重复一次,可以重复多次,也可以一次都不出现,这该怎么办?

对于这个问题,答案很简单,对于所有可能出现的情况,全部穷举一遍,只要有一种情况可以完成匹配,就认为 `p` 可以匹配 `s`。那么一旦涉及两个字符串的穷举,我们就应该条件反射地想到动态规划的技巧了。

### 一、思路分析

我们先脑补一下,`s``p` 相互匹配的过程大致是,两个指针 `i, j` 分别在 `s``p` 上移动,如果最后两个指针都能移动到字符串的末尾,那么久匹配成功,反之则匹配失败。

**如果不考虑 `*` 通配符,面对两个待匹配字符 `s[i]` 和 `p[j]`,我们唯一能做的就是看他俩是否匹配**
L
labuladong 已提交
54

L
labuladong 已提交
55
<!-- muliti_language -->
L
labuladong 已提交
56
```cpp
57 58 59 60 61 62 63 64 65
bool isMatch(string s, string p) {
    int i = 0, j = 0;
    while (i < s.size() && j < p.size()) {
        // 「.」通配符就是万金油
        if (s[i] == p[j] || p[j] == '.') {
            // 匹配,接着匹配 s[i+1..] 和 p[j+1..]
            i++; j++;
        } else {
            // 不匹配
L
labuladong 已提交
66
            return false;
67
        }
L
labuladong 已提交
68
    }
69
    return i == j;
L
labuladong 已提交
70 71 72
}
```

73 74 75 76 77 78 79
那么考虑一下,如果加入 `*` 通配符,局面就会稍微复杂一些,不过只要分情况来分析,也不难理解。

**当 `p[j + 1]` 为 `*` 通配符时,我们分情况讨论下**

1、如果 `s[i] == p[j]`,那么有两种情况:

1.1 `p[j]` 有可能会匹配多个字符,比如 `s = "aaa", p = "a*"`,那么 `p[0]` 会通过 `*` 匹配 3 个字符 `"a"`
L
labuladong 已提交
80

81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
1.2 `p[i]` 也有可能匹配 0 个字符,比如 `s = "aa", p = "a*aa"`,由于后面的字符可以匹配 `s`,所以 `p[0]` 只能匹配 0 次。

2、如果 `s[i] != p[j]`,只有一种情况:

`p[j]` 只能匹配 0 次,然后看下一个字符是否能和 `s[i]` 匹配。比如说 `s = "aa", p = "b*aa"`,此时 `p[0]` 只能匹配 0 次。

综上,可以把之前的代码针对 `*` 通配符进行一下改造:

```cpp
if (s[i] == p[j] || p[j] == '.') {
    // 匹配
    if (j < p.size() - 1 && p[j + 1] == '*') {
        // 有 * 通配符,可以匹配 0 次或多次
    } else {
        // 无 * 通配符,老老实实匹配 1 次
        i++; j++;
    }
} else {
    // 不匹配
    if (j < p.size() - 1 && p[j + 1] == '*') {
        // 有 * 通配符,只能匹配 0 次
    } else {
        // 无 * 通配符,匹配无法进行下去了
        return false;
    }
}
L
labuladong 已提交
107 108
```

109
整体的思路已经很清晰了,但现在的问题是,遇到 `*` 通配符时,到底应该匹配 0 次还是匹配多次?多次是几次?
L
labuladong 已提交
110

111
你看,这就是一个做「选择」的问题,要把所有可能的选择都穷举一遍才能得出结果。动态规划算法的核心就是「状态」和「选择」,**「状态」无非就是 `i` 和 `j` 两个指针的位置,「选择」就是 `p[j]` 选择匹配几个字符**
L
labuladong 已提交
112

113
### 二、动态规划解法
L
labuladong 已提交
114

115 116 117 118
根据「状态」,我们可以定义一个 `dp` 函数:

```cpp
bool dp(string& s, int i, string& p, int j);
L
labuladong 已提交
119 120 121 122
```



L
labuladong 已提交
123 124 125
<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>
L
labuladong 已提交
126

L
labuladong 已提交
127 128
 - [最优子结构原理和 dp 数组遍历方向](https://labuladong.github.io/article/fname.html?fname=最优子结构)
 - [经典动态规划:编辑距离](https://labuladong.github.io/article/fname.html?fname=编辑距离)
129

L
labuladong 已提交
130
</details><hr>
131

L
labuladong 已提交
132 133 134



L
labuladong 已提交
135 136 137
<hr>
<details>
<summary><strong>引用本文的题目</strong></summary>
L
labuladong 已提交
138

L
labuladong 已提交
139
<strong>安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:</strong>
L
labuladong 已提交
140

L
labuladong 已提交
141 142 143
| LeetCode | 力扣 |
| :----: | :----: |
| - | [剑指 Offer 19. 正则表达式匹配](https://leetcode.cn/problems/zheng-ze-biao-da-shi-pi-pei-lcof/?show=1) |
L
labuladong 已提交
144

L
labuladong 已提交
145
</details>
L
labuladong 已提交
146 147 148



149
**_____________**
L
labuladong 已提交
150

L
labuladong 已提交
151
应合作方要求,本文不便在此发布,请扫码关注回复关键词「正则」或 [点这里](https://appktavsiei5995.pc.xiaoe-tech.com/detail/i_6298796ae4b01a4852072fb9/1) 查看:
L
labuladong 已提交
152

L
labuladong 已提交
153
![](https://labuladong.github.io/pictures/qrcode.jpg)
L
labuladong 已提交
154

L
labuladong 已提交
155
======其他语言代码======
B
brucecat 已提交
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283

### javascript

[10.正则表达式匹配](https://leetcode-cn.com/problems/regular-expression-matching/)

```js
var isMatch = function (s, p) {
		// 备忘录
    let memo = {}
    let dp = function (s, i, p, j) {
        let m = s.length, n = p.length;

        // base case
        if (j === n) {
            return i === m;
        }

        if (i === m) {
            if ((n - j) % 2 === 1) {
                return false;
            }
            for (; j + 1 < n; j += 2) {
                if (p[j + 1] !== '*') {
                    return false;
                }
            }
            return true;
        }

        // 记录状态(i,j),消除重叠子问题
        let key = i + ',' + j

        if (memo[key] !== undefined) {
            return memo[key];
        }
        let res = false;

        if (s[i] === p[j] || p[j] === '.') {
            // 匹配
            if (j < n - 1 && p[j + 1] === '*') {
                // 1.1 通配符匹配 0 次或多次
                res = dp(s, i, p, j + 2) || dp(s, i + 1, p, j);
            } else {
                // 1.2 常规匹配1次
                res = dp(s, i + 1, p, j + 1);
            }
        } else {
            // 不匹配
            if (j < n - 1 && p[j + 1] === '*') {
                // 2.1 通配符匹配0次
                res = dp(s, i, p, j + 2)
            } else {
                // 2.2 无法继续匹配
                res = false
            }
        }

        // 将当前结果记入备忘录
        memo[key] = res;

        return res;
    }

    // 指针 i,j 从索引 0 开始移动
    return dp(s, 0, p, 0);
};
```



### C++

```c++
class Solution {
public:
    map<string, bool> memo;
    bool isMatch(string s, string p) {
        // 指针 i,j 从索引 0 开始移动
        return dp(s, 0, p, 0);
    }

    /* 计算 p[j..] 是否匹配 s[i..] */
    bool dp(string& s, int i, string& p, int j) {
        int m = s.size(), n = p.size();
        // base case
        if (j == n) {
            return i == m;
        }
        if (i == m) {
            if ((n - j) % 2 == 1) {
                return false;
            }
            for (; j + 1 < n; j += 2) {
                if (p[j + 1] != '*') {
                    return false;
                }
            }
            return true;
        }

        // 记录状态 (i, j),消除重叠子问题
        string key = to_string(i) + "," + to_string(j);
        if (memo.count(key)) return memo[key];
        
        bool res = false;
        
        if (s[i] == p[j] || p[j] == '.') {
            if (j < n - 1 && p[j + 1] == '*') {
                res = dp(s, i, p, j + 2)
                || dp(s, i + 1, p, j);
            } else {
                res = dp(s, i + 1, p, j + 1);
            }
        } else {
            if (j < n - 1 && p[j + 1] == '*') {
                res = dp(s, i, p, j + 2);
            } else {
                res = false;
            }
        }
        // 将当前结果记入备忘录
        memo[key] = res;
        
        return res;
    }
};
```