本文始发于个人公众号:TechFlow

这是LeetCode的第10题,题目关于字符串的正则匹配,我们先来看题目相关信息:

Link

Regular Expression Matching

Difficulty

Hard

Description

Given an input string (s) and a pattern (p), implement regular expression

matching with support for '.' and '*'.

  1. '.' Matches any single character.
  2. '*' Matches zero or more of the preceding element.

The matching should cover the entire input string (not partial).

Note:

  • s could be empty and contains only lowercase letters a-z.
  • p could be empty and contains only lowercase letters a-z, and characters like . or *.

题意

这道题属于典型的人狠话不多的问题,让我们动手实现一个简单的正则匹配算法。不过为了降低难度,这里需要匹配的只有两个特殊符号,一个符号是'.',表示可以匹配任意的单个字符。还有一个特殊符号是'*',它表示它前面的符号可以是任意个,可以是0个。

Example 1:

  1. Input:
  2. s = "aa"
  3. p = "a"
  4. Output: false
  5. ## Explanation: "a" does not match the entire string "aa".

Example 2:

  1. Input:
  2. s = "aa"
  3. p = "a*"
  4. Output: true
  5. ## Explanation: '*' means zero or more of the precedeng element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".

Example 3:

  1. Input:
  2. s = "ab"
  3. p = ".*"
  4. Output: true
  5. ## Explanation: ".*" means "zero or more (*) of any character (.)".

Example 4:

  1. Input:
  2. s = "aab"
  3. p = "c*a*b"
  4. Output: true
  5. ## Explanation: c can be repeated 0 times, a can be repeated 1 time. Therefore it matches "aab".

Example 5:

  1. Input:
  2. s = "mississippi"
  3. p = "mis*is*p*."
  4. Output: false

题解

题目要求是输入一个母串和一个模式串,请问是否能够达成匹配。

这题要求的是完全匹配,而不是包含匹配。也就是说s串匹配完p串之后不能有剩余,比如刚好完全匹配才行。明确了这点之后,我们先来简化操作,假设不存在'*'这个特殊字符,只存在'.',那么显然,这个简化过后的问题非常简单,我们随便就可以写出代码:

  1. def match(s, p):
  2. n = len(s)
  3. for i in range(n):
  4. if s[i] == p[i] or p[i] == '.':
  5. continue
  6. return False
  7. return True

我们下面考虑加入'*'的情况,其实加入'*'只会有一个问题,就是'*'可以匹配任意长度,如果当前位置出现了'*',我们并不知道它应该匹配到哪里为止。我们不知道需要匹配到哪里为止,那么就需要进行搜索了。也就是说,我们需要将它转换成一个搜索问题来进行求解。我们试着用递归来写一下:

  1. def match(s, p, i, j):
  2. # 当前位置是否匹配
  3. flag = s[i] == p[j] or p[j] == '.'
  4. # 判断p[j+1]是否是*,如果是那么说明p[j]可以跳过匹配
  5. if j+1 < len(p) and p[j+1] == '*':
  6. # 两种情况,一种是跳过p[j],另一种是p[j]继续匹配
  7. return match(s, p, i, j+2) or flag and match(s, p, i+1, j)
  8. else:
  9. # 如果没有*,只有一种可能
  10. return flag and match(s, p, i+1, j+1)

这段代码的精髓在于,由于'*'之前的符号也可以是0个,所以我们不能判断当前位置是否会是'*',而要判断后面一个位置是否是'*'。

以出否出现'*'为基点,分情况进行递归即可。

从代码上来看算上注释才12行,可是将这里面的关系都梳理清楚,并不容易。还是非常考验基本功的,需要对递归有较深入的理解才行。不过,这并不是最好的方法,因为你会发现有很多状态被重复计算了很多次。这也是递归算法经常遇到的问题之一,要解决倒也不难,我们很容易发现,对于固定的i和j,答案是固定的。那么,我们可以用一个数组来存储所有的i和j的情况。如果当前的i和j处理过了,那么直接返回结果,否则再去计算。

这种方法称作记忆化搜索,说起来复杂,但是实现起来只需要加几行代码:

  1. memory = {}
  2. def match(s, p, i, j):
  3. if (i, j) in memory:
  4. return memory[(i, j)]
  5. # 当前位置是否匹配
  6. flag = s[i] == p[j] or p[j] == '.'
  7. # 判断p[j+1]是否是*,如果是那么说明p[j]可以跳过匹配
  8. if j+1 < len(p) and p[j+1] == '*':
  9. # 两种情况,一种是跳过p[j],另一种是p[j]继续匹配
  10. ret = match(s, p, i, j+2) or flag and match(s, p, i+1, j)
  11. else:
  12. # 如果没有*,只有一种可能
  13. ret = flag and match(s, p, i+1, j+1)
  14. memory[(i, j)] = ret
  15. return ret

如果你对动态规划足够熟悉的话,想必也应该知道,记忆化搜索本质也是动态规划的一种实现方式。但同样,我们也可以选择其他的方式实现动态规划,就可以摆脱递归了,相比于递归,使用数组存储状态的递推形式更容易理解。

我们用dp[i][j]存储s[:i]与p[:j]是否匹配,那么根据我们之前的结论,如果p[j-1]是'*',那么dp[i][j]可能由dp[i][j-2]或者是dp[i-1][j]转移得到。dp[i][j-2]比较容易想到,就是'*'前面的字符作废,为什么是dp[i-1][j]呢?这种情况是代表'*'连续匹配,因为可能匹配任意个,所以必须要匹配在'*'这个位置。

举个例子:

s = 'aaaaa'

p = '.*'

在上面这个例子里,'.'能匹配所有字符,但是问题是s中只有一个a能匹配上。如果我们不用dp[i-1][j]而用dp[i-1][j-1]的话,那么是无法匹配aa或者aaa这种情况的。因为这几种情况都是通过'*'的多匹配能力实现的。如果还不理解的同学, 建议仔细梳理一下它们之间的关系。

我们用数组的形式写出代码:

  1. def is_match(s, p):
  2. # 为了防止超界,我们从下标1开始
  3. s = '$' + s
  4. p = '$' + p
  5. n, m = len(s), len(p)
  6. dp = [[False for _ in range(m)] for _ in range(n)]
  7. dp[0][0] = True
  8. # 需要考虑s空串匹配的情况
  9. for i in range(n):
  10. for j in range(1, m):
  11. # 标记当前位置是否匹配,主要考虑s为空串的情况
  12. match = True if i > 0 and (s[i] == p[j] or p[j] == '.') else False
  13. # 判断j位置是否为'*'
  14. if j > 1 and p[j] == '*':
  15. # 如果是,只有两种转移的情况,第一种表示略过前一个字符,第二种表示重复匹配
  16. dp[i][j] = dp[i][j-2] or ((s[i] == p[j-1] or p[j-1] == '.') and dp[i-1][j])
  17. else:
  18. # 如果不是,只有一种转移的可能
  19. dp[i][j] = dp[i-1][j-1] and match
  20. return dp[n-1][m-1]

这题的代码并不长,算上注释也不过20行左右。但是当中对于题意的思考,以及对算法的运用很高,想要AC难度还是不低的。希望大家能够多多揣摩,理解其中的精髓,尤其是最后的动态规划解法,非常经典,推荐一定要弄懂。当然如果实在看不明白也没有关系,在以后的文章,我会给大家详细讲解动态规划算法。

今天的文章就到这里,如果觉得有所收获,请顺手点个关注或者转发吧,你们的支持是我最大的动力。

LeetCode10 Hard,带你实现字符串的正则匹配的更多相关文章

  1. ruby 把字符串转为正则匹配表达式

    需求 函数,需要通过参数传递字符串,用来做正则匹配 reg = '[0-9]+' def func(str, reg) str.scan(reg) end 由于 reg 在其它地方定义, reg 是字 ...

  2. js字符串与正则匹配

    这里就说一下具体的使用方法,不做过多的解释. 字符串匹配正则的方法:str.方法(reg) 1.str.search() 参数是正则,将会从开始查找字符串中与正则匹配的字符,并返回该字符的第一次出现的 ...

  3. Linux shell中的一个问题 ${}带正则匹配的表达式

    目前在准备龙芯项目的PMON,在研究其编译过程的时候,看到一些make 语句,百思不得其解.后来在shell编程中看到一点资料,牵扯到Shell中的正则表达式.故记录下来,以备后来查阅. 问题: 在某 ...

  4. REGEXP 正则的实现两个字符串组的匹配。(regexp)

    主要懂3个mysql的方法:replace[替换]   regexp[正则匹配]    concat[连接]   由于某些原因,有时候我们没有按照范式的设计准则而把一些属性放到同一个字符串字段中.比如 ...

  5. java 正则匹配空格字符串 正则表达式截取字符串

    java 正则匹配空格字符串 正则表达式截取字符串 需求:从一堆sql中取出某些特定字符串: 比如配置的sql语句为:"company_code = @cc and project_id = ...

  6. IOS开发-UI学习-NSMutableAttributedString(带属性的字符串)的使用

    带属性的字符串: NSString *aa = @"hellochinaIloveYou!"; NSMutableAttributedString *mas = [[NSMutab ...

  7. php中的正则函数:正则匹配,正则替换,正则分割 所有的操作都不会影响原来的字符串.

    有一个长期的误解, 如果要分组, 必须用 小括号 和 |, 而不能用 中括号 和 |. [ab|AB]表示的不是 匹配 ab或 AB, 而是表示 匹配 a,b, |, A, B 这5个字符中 的任意 ...

  8. js格式化文件大小, 输出成带单位的字符串工具

    /** * 格式化文件大小, 输出成带单位的字符串 * @method formatSize * @grammar formatSize( size ) => String * @grammar ...

  9. PHP用正则匹配字符串中的特殊字符防SQL注入

    本文出至:新太潮流网络博客 /** * [用正则匹配字符串中的特殊字符] * @E-mial wuliqiang_aa@163.com * @TIME 2017-04-07 * @WEB http:/ ...

随机推荐

  1. [转]【转】大型高性能ASP.NET系统架构设计

    大型高性能ASP.NET系统架构设计 大型动态应用系统平台主要是针对于大流量.高并发网站建立的底层系统架构.大型网站的运行需要一个可靠.安全.可扩展.易维护的应用系统平台做为支撑,以保证网站应用的平稳 ...

  2. MockMvc control层单元测试 参数传递问题

    GET: 1.路径参数@PathVariable 2.表单参数@RequestParam POST: 1.JSON请求体参数 @RequestBody 放: 1.路径参数@PathVariable 2 ...

  3. C# 发送电子邮件(smtp)

    相关享目托管在github: https://github.com/devgis/CSharpCodes

  4. Oracle Net Manager 的使用方法(监听的配置方法)

    一,在服务端配置oracle端口 win+R  输入netca 弹出如下窗口后 选择监听程序配置,点击下一步 二.配置端口后使用Telnet工具调试端口是否联通 在命令行输入telnet 服务器ip ...

  5. 谈谈IC、ASIC、SoC、MPU、MCU、CPU、GPU、DSP、FPGA、CPLD

    IC (integrated circuit) 集成电路:微电路.微芯片.芯片:集成电路又分成:模拟集成电路(线性电路).数字集成电路.数/模混合集成电路: 模拟集成电路:产生.放大.处理各种模拟信号 ...

  6. MySQL 命令行(转)

    1.登录mysql 本地:mysql -u root -p, 回车后输入密码; 也可以p后不加空格,直接加密码.回车就登录了 远程:mysql -hxx.xx.xx.xx -u -pxxx 2.查看数 ...

  7. 数据库基础之Mysql

    数据库的简介 数据库 数据库(database,DB)是指长期存储在计算机内的,有组织,可共享的数据的集合.数据库中的数据按一定的数学模型组织.描述和存储,具有较小的冗余,较高的数据独立性和易扩展性, ...

  8. NoSQL入门)(详细)

    NoSQL入门 (原创:黑小子-余) 1.NoSQL是什么 NoSql(NoSQL=Not Only SQL),意即“不仅仅是SQL”,泛指菲关系型数据库.传统的关系数据库在应付web2.0网站,特别 ...

  9. [工具] Git版本管理(一)(基本操作)

    一.版本控制的发展 1.用文件来做版本控制 我们在写论文.做方案等的时候,一般都会同时在文件夹中存在很多版本的文件. 例如: 这种方式很常用,在很多领域都是用这种方式来进行版本控制的. 2.本地版本控 ...

  10. 洛谷$1156$ 垃圾陷阱 $dp$

    \(Sol\) \(f_{i,j}\)前\(i\)个垃圾,能活到时间\(j\)的最高垃圾高度.\(t_i\)表示第\(i\)个垃圾掉落的时间,\(g_i\)表示吃垃圾\(i\)能维持的时间,\(h_i ...