LeetCode 热题 HOT 100(05,正则表达式匹配)

不够优秀,发量尚多,千锤百炼,方可成佛。

算法的重要性不言而喻,无论你是研究者,还是最近比较火热的IT 打工人,都理应需要一定的算法能力,这也是面试的必备环节,算法功底的展示往往能让面试官眼前一亮,这也是在大多数竞争者中脱颖而出的重要影响因素。

然而往往大多数人比较注重自身的实操能力,着重于对功能的实现,却忽视了对算法能力的提高。有的时候采用不同的算法来解决同一个问题,运行效率相差还是挺大的,毕竟我们最终还是需要站在客户的角度思考问题嘛,能给用户带来更加极致的体验当然再好不过了。

万法皆空,因果不空。Taoye之前也不怎么情愿花费太多的时间放在算法上,算法功底也是相当的薄弱。这不,进入到了一个新的学习阶段,面对导师的各种“严刑拷打”和与身边人的对比,才开始意识到自己“菜”的事实。

这次的题目是LeeTCode 热题 HOT 100的第六题,难度属于困难,主要考查的是正则匹配问题。

感觉这道题还是有点东西的,也是花费了不少时间才搞懂。

我们都知道正则表达式主要用来进行字符匹配的,在爬虫中,我们会在向服务器发出一个请求并得到相应结果之后,通过特定的方式在响应结果中提取我们所需要的目标数据,而其中一种方式就可以通过正则表表达式来进行解析。

这道题的难度还是有点的,让我更加体会到了算法“孰能生巧”这一特性,这就像刷数学题一样,题目见过就有思路,没见过根本完全无法动手。这道题也是一样,主要可以通过动态规划算法来进行求解,当然也有其他可供用的算法,本文主要使用的是动态规划算法。

下面,我们就来看看这道题吧。

题目:正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

  • '.' 匹配任意单个字符
  • '*' 匹配零个或多个前面的那一个元素
    所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/regular-expression-matching

示例

  • 示例1

输入:s = "aa" p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。

  • 示例2

输入:s = "aa" p = "a"
输出:true
解释:因为 '
' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

  • 示例3

输入:s = "ab" p = "."
输出:true
解释:".
" 表示可匹配零个或多个('*')任意字符('.')。

  • 示例4

输入:s = "aab" p = "cab"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

  • 示例5

输入:s = "mississippi" p = "misisp*."
输出:false

思路

前面也提到了,爬虫中会经常会使用到正则表达式,既然如此,肯定是有特定的模块可供调用的,而我们可以通过re模块来实现这一功能:

class Solution(object):
    def isMatch(self, s, p):
        return True if re.match(p+'$',s) else False

需求虽已实现,但显然我们需要手动来实现这一功能,而非调用第三方模块。

在正式通过动态规划解决该题之前,我们有必要先了解下动态规划,其能解决哪些问题。

  • 计数问题,比如在一个矩阵当中,有多少种方式能从左上角走到右下角。(每次行走方向只能往下或往右)
  • 求最值问题,比如有三种硬币,面值分别是2元、3元、7元,则27元最少能用多少枚硬币组成
  • 存在性问题,比如说我们的这道题,是否能够在目标字符串中匹配成功

当然了,我们肯定是不能一概而论的。具体问题,具体分析,还是要根据实际情况来判断目标问题是否能够通过动态规划来解决。

要在问题中使用动态规划,我们一般需要四个步骤:

  1. 确定状态(最后一步、化为子问题)。解动态规划时需要定义一个数组,而确定状态就是要明白数组的每个元素dp[i]或者dp[i][j]代表什么意思(读到这里,可能会有点糊涂,没事,下面会有示例来具体解释)
  2. 转移方程,就是通过上一步骤当中的子问题来得到目标问题的子问题
  3. 初始条件和边界情况,动态规划一般是通过前一个节点来获取下一节点的值,所以我们需要明确其初始条件和数组dp的边界条件。就像数学归纳法,或是数列当中递推公式。
  4. 计算顺序,就是明确实现需求之前的上一个步骤要获得什么

读到这里,可能会有点糊涂,没事,下面会用示例来具体解释:

示例1:有三种硬币,面值分别是2元、3元、7元,则27元最少能用多少枚硬币组成

确定状态(最后一步、化为子问题):我们知道,题目需要的是最少能用多少枚硬币组成,我们需要定义一个长度为27的数组dp,而dp[i]就代表了对应总额所需要的最少硬币数目。我们假设最后一枚硬币面值为

转移方程:通过如上分析,我们可以得到如下递推公式(转移方程),代表的意思就是除去最后一枚硬币的面值总额所需要的最少硬币数 + 1

初始条件和边界情况:初始条件就是f(0),表示的就是0元可以通过0枚硬币组成,而边界情况就是每当面值总额减2、5、7的时候都应该大于0,否则的话无法组成目标面额总值(根据题意自行理解)

计算顺序:要想获得f(x),就必须得到f(x-2)、f(x-5)、f(x-7)的值,也就是除去一枚硬币面额所需要的最少硬币数目

通过如上分析,可以得到如下Java算法;

public int coinChange(int[] coin_list, int coin_value) {
    int[] f = new int[coin_value + 1];
    f[0] = 0;
    for (int i = 1; i <= coin_value; i++) {
        f[i] = Integer.MAX_VALUE;
        for (int j = 0; j < coin_list.length; j++) {
            if (i >= coin_list[j] && f[i - coin_list[j]] != Integer.MAX_VALUE) {
                f[i] = Math.min(f[i], f[i - coin_list[j]] + 1);
            }
        }
    }
    if (f[coin_value] == Integer.MAX_VALUE) { f[coin_value] = -1; }
    return f[coin_value];
}

示例2:在一个矩阵当中,有多少种方式能从左上角走到右下角。(每次行走方向只能往下或往右),比如从(0, 0)出发,有多少种路线走到(4, 5)

时间关系,这里简单分析下。

走到(4, 5)的上一格有两种,一个是(3, 5),另一个是(4, 4),所以走到(4, 5)的路线总数就等于上述两种的总和,对此,我们可以得到如下关系,其中f[i][j]表示的是走到(i, j)的路线种数

Python实现的代码如下:

def calc_run_number(m, n):
    import numpy as np
    f = np.zeros([m, n])
    for i in range(m):
        for j in range(n):
            if (i ==0 or j == 0): f[i][j] = 1
            else: f[i, j] = f[i-1][j] + f[i][j-1]
    return f[m-1][n-1

像类似的案例其实还有很多的,主要还是要多多联系才行。

现在,我们回归到正则匹配这道题,再次看下题面:

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
. 匹配任意单个字符

  • 匹配零个或多个前面的那一个元素
    所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

前面也有说到,使用动态规划之前需要定义一个数组来存储数据状态,这道题中涉及s和p两个字符串,所以我们需要定义二维数组,也就是矩阵dp。假设s和p的长度分别为s_len和p_len,则dp数组的shape值应该为(s_len + 1, p_len + 1),其中+1主要是的因为需要额外引入一行一列,用于表示空字符""的匹配情况,这里可以理解成初始值的引入。

dp[i][j]表示的就是s[:i]能否通过dp[:j]正则表达式来进行匹配,其值为True或False。True表示的是能匹配成功,比如s:abc p:a.c,而False表示的是不能匹配,比如s:abc p:ab*,这一点一定要理解清楚,非常重要,也是解这道题的关键之一。

我们需要对dp数组进行初始化,不如通过如下方式将内部所有元素初始化为False:,且dp[0][0]=True,因为空字符s自然能用空字符p来进行匹配:

dp = [[False] * (p_len + 1) for temp in range(s_len + 1)]; dp[0][0] = True

随后,需要将dp第一行进行额外处理,也就是初始状态的定义。这里的有个条件来对*进行判断,假如说*前面的一个字符存在,则需要将该位置上的dp值定义为与前两个值相同,因为在正则表达式中,*表示的是能匹配零个或多个前面的那一个元素(注意:这是在第一行进行定义,也就是说此时s可以看做""空字符,通过p[:j]正则来对其进行匹配):

for one_row_item in range(p_len):
    if p[one_row_item] == '*' and dp[0][one_row_item - 1]:
        dp[0][one_row_item + 1] = True

第一行处理完成,随后就需要对其他行的dp值进行填充,而填充的依据就是根据上一行来进行分类判断:假设遍历条件满足:p[j - 1] == s[i - 1] or p[j - 1] == '.',也就是说s和p对应的字符相同,或能通过.匹配任意字符,则将设置dp[i][j] = dp[i - 1][j - 1],因为此时dp[i][j]的匹配情况是由dp[i-1][j-1]来决定的;假如说此时的正则字符为*,则dp[i][j]需要根据dp[i][j - 2]、dp[i][j - 1]、dp[i - 1][j]共同决定,完整算法思想如下:

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        s_len, p_len = len(s), len(p)
        dp = [[False] * (p_len + 1) for temp in range(s_len + 1)]; dp[0][0] = True
        for one_row_item in range(p_len):
            if p[one_row_item] == '*' and dp[0][one_row_item - 1]:
                dp[0][one_row_item + 1] = True
        for i in range(1, s_len + 1):
            for j in range(1, p_len + 1):
                if p[j - 1] == s[i - 1] or p[j - 1] == '.': dp[i][j] = dp[i - 1][j - 1]
                elif p[j - 1] == '*':
                    if p[j - 2] != s[i - 1]: dp[i][j] = dp[i][j - 2]
                    if p[j - 2] == s[i - 1] or p[j - 2] == '.':
                        dp[i][j] = dp[i][j - 2] or dp[i][j - 1] or dp[i - 1][j]
        return dp[s_len][p_len]

总的来说,这道题还是有难度的,至少是目前刷到最难的一题了,也是花费了不少时间来理解,理解了也很难通过文字来进行描述,主要还是要通过阅读算法代码来理解其核心思想。

像这种通过动态规划算法来解决的问题还是要多做多练,才能孰能生巧。

我是Taoye,爱专研,爱分享,热衷于各种技术,学习之余喜欢下象棋、听音乐、聊动漫,希望借此一亩三分地记录自己的成长过程以及生活点滴,也希望能结实更多志同道合的圈内朋友,更多内容欢迎来访微信公主号:玩世不恭的Coder

推荐阅读:

LeetCode 热题 HOT 100(00,两数之和)
LeetCode 热题 HOT 100(01,两数相加)
LeetCode 热题 HOT 100(02,无重复字符的最长子串)
LeetCode 热题 HOT 100(03,寻找两个正序数组的中位数)
LeetCode 热题 HOT 100(04,最长回文子串))

参考资料:

[1] LeetCode官方:https://leetcode-cn.com/problems/regular-expression-matching/solution/zheng-ze-biao-da-shi-pi-pei-by-leetcode-solution/
[2] 动态规划算法:https://www.bilibili.com/video/BV1xb411e7ww?from=search&seid=9800329040911246241

LeetCode 热题 HOT 100(05,正则表达式匹配)的更多相关文章

  1. 🔥 LeetCode 热题 HOT 100(1-10)

    1. 两数之和 思路一:暴力遍历所有组合 class Solution { public int[] twoSum(int[] nums, int target) { for (int i = 0; ...

  2. 🔥 LeetCode 热题 HOT 100(81-90)

    337. 打家劫舍 III 思路:后序遍历 + 动态规划 推荐题解:树形 dp 入门问题(理解「无后效性」和「后序遍历」) /** * Definition for a binary tree nod ...

  3. 🔥 LeetCode 热题 HOT 100(71-80)

    253. 会议室 II(NO) 279. 完全平方数 class Solution { public int numSquares(int n) { // dp[i] : 组成和为 i 的最少完全平方 ...

  4. 🔥 LeetCode 热题 HOT 100(31-40)

    75. 颜色分类 思路:将 2 往后放,0 往前放,剩余的1自然就放好了. 使用双指针:left.right 分别指向待插入的 0 和 2 的位置,初始 left 指向数组头,right 指向数组尾部 ...

  5. 🔥 LeetCode 热题 HOT 100(51-60)

    142. 环形链表 II 思路:快慢指针,快慢指针相遇后,慢指针回到头,快慢指针步伐一致一起移动,相遇点即为入环点 /** * Definition for singly-linked list. * ...

  6. 🔥 LeetCode 热题 HOT 100(21-30)

    46. 全排列 思路:典型回溯法 class Solution { public List<List<Integer>> permute(int[] nums) { Linke ...

  7. 🔥 LeetCode 热题 HOT 100(61-70)

    207. 课程表 思路:根据题意可知:当课程之间不存在 环状 循环依赖时,便能完成所有课程的学习,反之则不能.因此可以将问题转换成: 判断有向图中是否存在环.使用 拓扑排序法 : 构建 入度表:记录每 ...

  8. 🔥 LeetCode 热题 HOT 100(41-50)

    102. 二叉树的层序遍历 思路:使用队列. /** * Definition for a binary tree node. * public class TreeNode { * int val; ...

  9. 🔥 LeetCode 热题 HOT 100(11-20)

    20. 有效的括号 class Solution { public boolean isValid(String s) { Map<Character, Character> map = ...

随机推荐

  1. 带着好奇心去探索IDEA

    带着好奇心去探索IDEA 工欲善其事必先利其器 软件是提高工作效率的工具.所以了解工具的特性,操作方式,能更好地使用它.一般使用掌握逻辑: 第一步:了解菜单栏-工具栏-其他窗口: 第二步:实战,真正利 ...

  2. Flink实例(五十): Operators(十)多流转换算子(五)coGroup 与union

    参考链接:https://mp.weixin.qq.com/s/BOCFavYgvNPSXSRpBMQzBw 需求场景分析 需求场景 需求诱诱诱来了...数据产品妹妹想要统计单个短视频粒度的「点赞,播 ...

  3. ls: 显示目下的内容及相关属性信息

    ls: 显示目下的内容及相关属性信息 [功能说明] ls 命令可以理解为英文单词 "list" 的缩写,其功能是列出目录的内容及其内容属性信息(list directory con ...

  4. 多测师讲解python_os模块_高级讲师肖sir

    #os.path.isfile()#:判断当前是否为文件,返回布尔值是文件则True否者Falsea_path='F:\cms搭建.rar' #lesson包b_path=r'D:\bao\kk '# ...

  5. 如何修改或新增visual studio 的模板

    在 visual studio 中添加模板,我这里是新增mvc.net的模板 vs2017在文件夹=>(举例说明,请替换为相应的安装目录) D:\Program Files (x86)\Micr ...

  6. 正式班D8

    2020.10.15星期四 正式班D8 一.上节课复习 OSI七层协议 socket socket是对传输层以下的封装 IP+port标识唯一一个基于网络通讯的软件 TCP与UDP TCP:因为在通信 ...

  7. 【博弈论】CF 1215D Ticket Game

    题目大意 洛谷链接 给出一个长度为\(n\)的由数字组成的字符串(\(n\)是偶数).但可能有偶数个位上的数字为?. 现在有两个人\(A\)和\(B\),在?的位置上填\(0\)~\(9\)的数,一直 ...

  8. 正式班D11

    2020.10.20星期二 正式班D11 bash解释器交互式环境特性 命令和文件自动补全(Tab只能补全命令和文件) 快捷键 CTRL+C ==>终止前台运行的程序 CTRL+D ==> ...

  9. Linux命令之date +%F

    date命令显示当前日期 date +%F显示当前日期 [10:02:52 root@C8[ 2020-06-16DIR]#touch `hostname`_`date +%F`.log [10:03 ...

  10. 联赛模拟测试20 C. Weed

    题目描述 \(duyege\) 的电脑上面已经长草了,经过辨认上面有金坷垃的痕迹. 为了查出真相,\(duyege\) 准备修好电脑之后再进行一次金坷垃的模拟实验. 电脑上面有若干层金坷垃,每次只能在 ...