LeetCode10 Hard,带你实现字符串的正则匹配

本文始发于个人公众号: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 ‘*‘.

'.' Matches any single character.
'*' 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:

Input:
s =  "aa"
p = "a"
Output: false
## Explanation:  "a" does not match the entire string "aa".

Example 2:

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

Example 3:

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

Example 4:

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

Example 5:

Input:
s =  "mississippi"
p = "mis*is*p*."
Output: false

题解

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

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

def match(s, p):
  n = len(s)
  for i in range(n):
    if s[i] == p[i] or p[i] == '.':
      continue
    return False
  return True

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

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

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

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

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

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

memory = {}
def match(s, p, i, j):
    if (i, j) in memory:
      return memory[(i, j)]
    # 当前位置是否匹配
    flag = s[i] == p[j] or p[j] == '.'
    # 判断p[j+1]是否是*,如果是那么说明p[j]可以跳过匹配
    if j+1 < len(p) and p[j+1] == '*':
        # 两种情况,一种是跳过p[j],另一种是p[j]继续匹配
        ret = match(s, p, i, j+2) or flag and match(s, p, i+1, j)
    else:
        # 如果没有*,只有一种可能
        ret = flag and match(s, p, i+1, j+1)
    memory[(i, j)] = ret
    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这种情况的。因为这几种情况都是通过‘*‘的多匹配能力实现的。如果还不理解的同学, 建议仔细梳理一下它们之间的关系。

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

def is_match(s, p):
    # 为了防止超界,我们从下标1开始
    s = '$' + s
    p = '$' + p
    n, m = len(s), len(p)
    dp = [[False for _ in range(m)] for _ in range(n)]

    dp[0][0] = True

    # 需要考虑s空串匹配的情况
    for i in range(n):
        for j in range(1, m):
            # 标记当前位置是否匹配,主要考虑s为空串的情况
            match = True if i > 0 and (s[i] == p[j] or p[j] == '.') else False
            # 判断j位置是否为'*'
            if j > 1 and p[j] == '*':
                # 如果是,只有两种转移的情况,第一种表示略过前一个字符,第二种表示重复匹配
                dp[i][j] = dp[i][j-2] or ((s[i] == p[j-1] or p[j-1] == '.') and dp[i-1][j])
            else:
                # 如果不是,只有一种转移的可能
                dp[i][j] = dp[i-1][j-1] and match
    return dp[n-1][m-1]

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

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

原文地址:https://www.cnblogs.com/techflow/p/12216544.html

时间: 2024-10-10 16:14:22

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

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

需求 函数,需要通过参数传递字符串,用来做正则匹配 reg = '[0-9]+' def func(str, reg) str.scan(reg) end 由于 reg 在其它地方定义, reg 是字符串, 传进来后发现没法直接用于正则匹配 返回的结果总是 [] 空字符串. 解决 通过 Regexp 来通过 string 生成正则匹配的条件 def func)(str, reg) reg = Regexp.new("#{reg}"[0..-1]) "#{str}"

带属性的字符串 NSMutableAttributedString/NSAttributedString

由于iOS7新出的NSTextStorge是NSMutableAttributedString的子类,所以要用好NSTextStorage,首先要学好NSMutableAttributedString和NSAttributedString. 按个人的理解,NSAttributedString是一个带有属性的字符串,通过该类可以灵活地操作和呈现多种样式的文字数据. alignment //对齐方式 firstLineHeadIndent //首行缩进 headIndent //缩进 tailInd

批处理命令:带参数的字符串替换

批处理命令:带参数的字符串替换 @echo off setlocal enabledelayedexpansion set main_str=hello world set src=hello set dst=hi echo %main_str% set sub_str=!main_str:%src%=%dst%! echo %sub_str%

c语言中读入带空格的字符串

http://blog.csdn.net/pipisorry/article/details/37073023 问题: scanf("%s", a); 运行输入hello world 回车 则输入到a的只是空格之前的部分,怎样把空格之后的部分也输出? 1. scanf( "%[^\n]", str ); #include <stdio.h> int main(){ char str[50]; scanf( "%[^\n]", str

关于Boost,C Regex对短目标字符串正则匹配的性能分析

昨天对长目标字符串下的各种正则匹配库性能进行了总结,得出结论是Boost regex性能最佳.今天将其应用到项目当中,果不其然,长字符串匹配带来的性能损失基本没有了,当然,目前规模并不算太大,但是在可预计规模内Boost可以完全达到要求. 不过有一点,在Boost,C同时去除长字符串匹配的影响后,剩下都是短字符串匹配,发现Boost比C好的并不是好很多,例如10000+次短字符匹配中,其中包含匹配成功和不成功的,Boost regex+系统其他模块用时130ms左右,而C regex+系统其他模

ES6随笔--各数据类型的扩展(1) --字符串和正则

ES6随笔--基本数据类型的扩展 字符串 Unicode表示法 \uxxxx表示的Unicode如果码数超过了0xFFFF的范围,只要放进大括号内就能识别: codePointAt() 用codePointAt()可以准确识别码数超出0xFFFF范围的Unicode字符: for...of循环能够正确识别32位的UTF-16字符: String.fromCodePoint() 与codePointAt()作用刚好相反:根据传入的Unicede码数返回对应的字符: 遍历 for...of循环可以循

Nginx 正则匹配配置

location表达式类型 ~ 表示执行一个正则匹配,区分大小写 ~* 表示执行一个正则匹配,不区分大小写 ^~ 表示普通字符匹配.使用前缀匹配.如果匹配成功,则不再匹配其他location. = 进行普通字符精确匹配.也就是完全匹配. @ 它定义一个命名的 location,使用在内部定向时,例如 error_page, try_files location优先级说明 在nginx的location和配置中location的顺序没有太大关系.正location表达式的类型有关.相同类型的表达式

re模块 正则匹配

import re re.M 多行模式 位或的意思 parrterm就是正则表达式的字符串,flags是选项,表达式需要被编译,通过语法.策划.分析后卫其编译为一种格式,与字符串之间进行转换 re模块 主要为了提速,re的其他方法为了提高效率都调用了编译方法,就是为了提速 re的方法 单次匹配 re.compile 和 re.match def compile(pattern, flags=0): return _compile(pattern, flags) 可看到,re最后返回的是_comp

常用的re模块的正则匹配的表达式

07.01自我总结 常用的re模块的正则匹配的表达式 一.校验数字的表达式 1.数字 ^[0-9]\*$ 2.n位的数字 ^\d{n}$ 3.至少n位的数字 ^\d{n,}$ 4.m-n位的数字 ^\d{m,n}$ 5.零和非零开头的数字 ^(0|[1-9][0-9]\*)$ 6.非零开头的最多带两位小数的数字 ^([1-9][0-9]\*)+(\.[0-9]{1,2})?$ 7.带1-2位小数的正数或负数 ^(\-)?\d+(\.\d{1,2})$ 8.正数.负数.和小数 ^(\-|\+)?\