10. 正则表达式匹配

news2025/1/14 18:25:01

题目链接:https://leetcode.cn/problems/regular-expression-matching/

从暴力递归到动态规划,对于状态转移方程不容易推导出来的可以先从递归进行尝试各种策略,最后再从暴力递归转为动态规划,这种尝试方式容易求解dp初始值以及dp更新方式,由于题目限制了s的长度最大为20,p的长度最大为30,这道题使用暴力递归的解法也过了。暴力递归和动态规划的具体解题思路如下:

  1. 暴力递归:题目是两个字母串匹配的过程,可以使用递归分解成子问题来做

  1. 递归方程的定义:boolean process(char[] s, char[] p, int sL, int pL),参数解释如下:

  1. s:字符串s

  1. p:字符规律p

  1. sL:从字符串s的sL处位置开始匹配

  1. pL:从字符规律p的pL处位置开始匹配

  1. return:如果p[pL,...,N]完全匹配字符串s[sl,...,M]则,返回true,否则返回false

  1. process递归函数的解释:判断字符规律p从索引pL开始一直到结束的正则表达式字符串是否匹配字符串s从索引sL开始到结束的字符串

  1. 递归的结束条件:对于递归函数来说首先需要找到递归的结束条件

  1. 如果sL==s.length:这种情况字符串s已经完全被正则表达式匹配了(s最后一个字符的索引为s.length-1),但是字符规律p可能还没有结束,需要分情况讨论:

  1. pL == p.length:正则表达式也结束了(正则表达式的最后一个字符的索引为p.length-1),显然这种情况正则表达式p完全匹配字符串s,递归函数返回true

  1. 否则:正则表达式还没有使用完,但是这种情况并不意味着正则表达式没有全部匹配字符串s,因为有*(匹配零个或多个前面的哪一个元素,这种情况会把没有使用完的正则字符消耗掉),

  1. 比如字符串s="a",字符规律p="ab*",字符规律p的a匹配字符串s的a,字符串s匹配结束,但是字符规律p还剩下字符"b*",这个时候显然完全匹配的,递归函数应该返回true

  1. 比如字符串s="a",字符规律p="ab",字符规律p的a匹配字符串s的a,字符串s匹配结束,但是字符规律p还剩下字符"b",这个时候显然是不匹配的,递归函数应该返回false

  1. 因此,当字符串s结束,字符规律p还有字符时,需要再次判断,如果p剩余字符是*,让pL++,如果p[pL + 1] == '*',可以让*的含义为匹配零次,把*前面的一个字符消耗掉,让pL=pL+2,最终如果pL大于等于p.length,这个是否显然字符规律p也消耗完,递归函数返回true,否则返回false

  1. 如果,sL!=s.length,但是pL==p.length:这种情况为字符串s还有字符,但是正则表达式全部消耗完了,这种情况递归返回false

  1. 递归的终止条件已经找到,接下来就是递归的转移过程了,此时,字符串s和字符规律p都有剩余字符。需要分三种情况,对于字符归路p,只有三种字符,".","*",“a-z”

  1. p[pL] == '.':字符规律当前的字符为"." ,可以匹配任意一个字符s中的字符,分两种情况

  1. 如果p[pL+1]=="*":表明当前.可以选择匹配零个或者匹配任意多个字符,最终递归转移有两种

  1. p1 = process(s, p, sL, pL + 1):当前的. 选择匹配0个s中的字符,p中的.被消耗掉,然后看s[sL,...M]和p[pL+1,...,N]是否完全匹配

  1. p2 = process(s, p, sL + 1, pL + 1):当前的. 选择匹配多个(>=1)个s中的字符,p中的.和s的当前字符消耗掉,然后看s[sL+1,...,M]和p[pL+1,...N]是否完全匹配,解释一下,这里为什么是pL+1而不是pL(把当前的.消耗掉了),下一个字符为*,当前的.不是可以匹配多次吗?因为下一个字符是*,匹配多次的逻辑在*中判断,这样可以减少递归的层数

  1. 只要a和b两个递归中有一个返回true,那么就表明存在一个正则匹配使字符规律p完全匹配字符串s,因此递归返回值为p1=p1||process(s, p, sL + 1, pL + 1)

  1. 否则,也就是.的下一个字符不是*,当前.只能匹配一个字符,递归返回值为:p1=process(s, p, sL + 1, pL + 1)

  1. 如果p[pL] == '*',那么p的前面一个字符可以匹配多次,分两种情况

  1. 确定使用哪一个字符去匹配:

  1. 如果p[pL - 1] == '.',那么,可以匹配掉任何一个s中的字符即c=s[sL](s中待消耗掉的字符),

  1. 否则,c=p[pL-1],只能使用p中前一个字符取匹配

  1. 递归转移:

  1. 匹配0个,p1 = process(s, p, sL, pL + 1):选择匹配0个,p中的*被消耗掉

  1. c == s[sL]时,p2 = process(s, p, sL + 1, pL):选择匹配多个,s的当前字符被消耗掉,p中的*继续使用

  1. c!==s[sL]时,p2 = process(s, p, sL, pL + 1):p的前一个字符和s当前的字符不匹配,只能选择匹配0个(这个分支在程序中不用写,因为在a中已经判断匹配0个了,这里列出为了逻辑清晰)

  1. 只要p1和p2有一个为true,那么说明存在一个正则匹配方式使字符规律p完全匹配字符串s,因此递归返回p1||p2

  1. 否则,也就是p的当前字符为字母:分两种情况

  1. 如果s[sL] == p[pL]:正确匹配,有两种情况:

  1. 如果p[pL + 1] == '*':p的下一个字符为*,当前p中的字符可以选择匹配0次或者多次

  1. p1 = process(s, p, sL, pL + 1):匹配0次,也就是不用p中的字符消耗s中的当前字符,仅p中的当前字符被消耗掉

  1. p2 = process(s, p, sL + 1, pL + 1):匹配多次,s中的当前字符和p的当前字符被消耗掉

  1. p1 = p1 || process(s, p, sL + 1, pL + 1)

  1. 否则,只能匹配一次,p1 = process(s, p, sL + 1, pL + 1)

  1. 递归返回p1

  1. 如果s[sL] != p[pL]

  1. 如果p[pL + 1] == '*':虽然当前不匹配,但是p的下一个字符是*,可以将当前不匹配的字符消耗掉递归返回值为process(s, p, sL, pL + 1)

  1. 否则,就只能返回false了。

  1. AC代码

class Solution {
    public static boolean isMatch(String s, String p) {
        return process(s.toCharArray(), p.toCharArray(), 0, 0);
    }
   public static boolean process(char[] s, char[] p, int sL, int pL) {
        if (sL == s.length) {
            if (pL == p.length) {
                return true;
            } else {
                while (pL < p.length) {
                    if (p[pL] == '*') {
                        pL++;
                    } else {
                        if (pL + 1 < p.length && p[pL + 1] == '*') {
                            pL += 2;
                        } else {
                            break;
                        }
                    }
                }
                return pL >= p.length;
            }
        }
        if (pL == p.length) {
            return false;
        }
        if (p[pL] == '.') {
            boolean p1;
            if (pL + 1 < p.length && p[pL + 1] == '*') {
                p1 = process(s, p, sL, pL + 1);
                p1 = p1 || process(s, p, sL + 1, pL + 1);
            } else {
                p1 = process(s, p, sL + 1, pL + 1);
            }
            return p1;
        } else if (p[pL] == '*') {
            char c;
            if (p[pL - 1] == '.') {
                c = s[sL];
            } else {
                c = p[pL - 1];
            }
            boolean p1 = process(s, p, sL, pL + 1);
            if (c == s[sL]) {
                p1 = p1 || process(s, p, sL + 1, pL);
            }
            return p1;
        } else {
            if (s[sL] == p[pL]) {
                boolean p1;
                if (pL + 1 < p.length && p[pL + 1] == '*') {
                    p1 = process(s, p, sL, pL + 1);
                    p1 = p1 || process(s, p, sL + 1, pL + 1);
                } else {
                    p1 = process(s, p, sL + 1, pL + 1);
                }
                return p1;
            } else {
                int p1;
                if (pL + 1 < p.length && p[pL + 1] == '*') {
                    return process(s, p, sL, pL + 1);
                }
                return false;
            }
        }
    }
}

虽然过了,但是也是险过,时间花费太大....

  1. 动态规划:可以直接由上述递归求解修改成动态规划求解

  1. dp数组的维度与大小:递归函数process(char[] s, char[] p, int sL, int pL)变化的的参数只有两个sL和pL,因此只需要两个维度,就可以表示所有的递归过程,又因为递归终止条件为sL == s.length或者pL==p.length,即从0到s.length,因此使用dp[s.length+1][p.length+1]就可以保存所有的递归函数,其中dp[sL][pL]=process(char[] s, char[] p, int sL, int pL)

  1. dp数组的初始值:由递归方程的终止条件就是dp数组的初始值,其中dp[s.length][p.length] = true,字符串s和字符规律p都已经到达末尾,递归函数返回true

dp[s.length][p.length] = true;
for (int pL = 0; pL < p.length; pL++) {//sL == s.length
    int j = pL;
    while (j < p.length) {
        if (p[j] == '*') {
            j++;
        } else {
            if (j + 1 < p.length && p[j + 1] == '*') {
                j += 2;
            } else {
                break;
            }
        }
    }
    dp[s.length][pL] = j >= p.length;
}
for (int sL = 0; sL < s.length; sL++) {//pL==p.length
    dp[sL][p.length] = false;
}

经过上述过程我们就可以求出上图中dp数组的最下面一行和最最边一行的初始值

  1. dp的状态转移:对于递归process(char[] s, char[] p, int sL, int pL),在本次递归函数中会进入下面几层递归

  1. process(s, p, sL, pL + 1)

  1. process(s, p, sL + 1, pL + 1)

  1. process(s, p, sL + 1, pL)

  1. 也就是dp[sL][pL]的值会和dp[sL][pL+1],dp[sL+1][pL+1],dp[sL+1][pL]相关

  1. 如果想要求出?处的值,需要直到右边,下边,右下角的值,而最下面一行和最右边一行的值已经求出,所以可以从下往上,从左往右更新dp数组,将所有的process(s,p,sL,pL)递归函数改为响应的dp数组,所有的return部分使用dp数组保存递归函数返回结果(这就是dp的状态转移!),暴力递归求解中,第一层递归为rocess(s.toCharArray(), p.toCharArray(), 0, 0),所以最终返回值为dp[0][0]

AC代码

class Solution {
    public static boolean isMatch(String s, String p) {
        return dp(s.toCharArray(), p.toCharArray());
    }
   public static boolean dp(char[] s, char[] p) {
        boolean[][] dp = new boolean[s.length + 1][p.length + 1];
        dp[s.length][p.length] = true;
        for (int pL = 0; pL < p.length; pL++) {
            int j = pL;
            while (j < p.length) {
                if (p[j] == '*') {
                    j++;
                } else {
                    if (j + 1 < p.length && p[j + 1] == '*') {
                        j += 2;
                    } else {
                        break;
                    }
                }
            }
            dp[s.length][pL] = j >= p.length;
        }

        for (int sL = 0; sL < s.length; sL++) {
            dp[sL][p.length] = false;
        }
        for (int sL = s.length - 1; sL >= 0; sL--) {
            for (int pL = p.length - 1; pL >= 0; pL--) {
                if (p[pL] == '.') {
                    boolean p1;
                    if (pL + 1 < p.length && p[pL + 1] == '*') {
                        p1 = dp[sL][pL + 1];
                        p1 = p1 || dp[sL + 1][pL + 1];
                    } else {
                        p1 = dp[sL + 1][pL + 1];
                    }
                    dp[sL][pL] = p1;
                } else if (p[pL] == '*') {
                    char c;
                    if (p[pL - 1] == '.') {
                        c = s[sL];
                    } else {
                        c = p[pL - 1];
                    }
                    boolean p1 = dp[sL][pL + 1];
                    if (c == s[sL]) {
                        p1 = p1 || dp[sL + 1][pL];
                    }
                    dp[sL][pL] = p1;
                } else {
                    if (s[sL] == p[pL]) {
                        boolean p1;
                        if (pL + 1 < p.length && p[pL + 1] == '*') {
                            p1 = dp[sL][pL + 1];
                            p1 = p1 || dp[sL + 1][pL + 1];
                        } else {
                            p1 = dp[sL + 1][pL + 1];
                        }
                        dp[sL][pL] = p1;
                    } else {
                        if (pL + 1 < p.length && p[pL + 1] == '*') {
                            dp[sL][pL] = dp[sL][pL + 1];
                        } else {
                            dp[sL][pL] = false;
                        }
                    }
                }
            }
        }
        return dp[0][0];
    }
}

时间从暴力递归的1091ms缩短为1ms!

从暴力递归到动态规划完全没有推导出dp的状态转移方程,只是从暴力递归进行尝试,然后根据递归修改为动态规划,这种方式比较简单,对于直接推到状态转移方程比较困难的情况,不妨先从暴力递归进行尝试,然后修改成动态规划。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/143006.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构-考研难点代码突破 (图关键路径完全解析(流程+代码) - C++代码)

考研在关键路径上的考察以流程为主 文章目录1. AOE网2. 关键路径问题解决流程C代码1. AOE网 首先区分AOV网&#xff1a; AOV网∶若用DAG 图&#xff08;有向无环图&#xff09;表示一个工程&#xff0c;其顶点表示活动&#xff0c;用有向边<Vi&#xff0c;Vj>表示活动 V…

【ESP32+freeRTOS学习笔记-(五)队列Queue】

目录1、什么是队列Queue2、队列的多任务特性2.1 多任务的访问&#xff1a;2.2 队列读取阻塞&#xff1a;2.3 写队列阻塞&#xff1a;2.4 阻塞于多个队列&#xff1a;3、队列的使用3.1 创建队列--The xQueueCreate() API3.2 写入队列3.3 从队列中接收数据3.4 删除队列4、队列集4…

ReactDOM.render在react源码中执行之后发生了什么?

ReactDOM.render 通常是如下图使用&#xff0c;在提供的 container 里渲染一个 React 元素&#xff0c;并返回对该组件的引用&#xff08;或者针对无状态组件返回 null&#xff09;。本文主要是将ReactDOM.render的执行流程在后续文章中会对创建更新的细节进行分析&#xff0c…

MATLAB-plot3/ezplot3三维绘图

&#xff08;1&#xff09; plot3是三维绘图的基本函数&#xff0c;调用格式如下。1、plot3( X,Y,Z):绘制简单的三维曲线&#xff0c;当X、Y、Z是长度相同的向量时&#xff0c;plot3命令将绘制以向量X、Y、Z为(x, y,z)坐标值的三维曲线;当X、Y、Z是mn矩阵时,plot3命令将绘制m条…

Android 虚拟分区详解(四) 编译开关

Android Virtual A/B 系统简称 VAB,我将其称为虚拟分区。 本系列文章基于 Android R(11) 进行分析,如果没有特别说明,均基于代码版本 android-11.0.0_r46 请已经购买《Android 虚拟分区》专栏的朋友加我 wx 进 "虚拟分区专栏 VIP 答疑"群,作为本专栏文章的附加服…

(6)元对象系统与信号与槽机制

1. 元对象系统 元对象系统是一个基于标准C的扩展&#xff0c;为Qt提供了信号与槽机制、实时类型信息、动态属性系统。 什么是元对象 在计算机科学中&#xff0c;元对象是这样一个东西&#xff1a;它可以操纵、创建、描述、或执行其他对象。元对象描述的对象称为基对象。元对象可…

记一次搭建备库,使用连接串主库无法连接到备库

主库使用连接串连接备库失败 SQL> conn sys/oracleorcldg as sysdba ERROR: ORA-12528: TNS:listener: all appropriate instances are blocking new connections 备库已经建立了静态监听 # listener.ora Network Configuration File: /u01/app/oracle/product/11.2.0/db_1/…

安全寒假第一堂课

一、状态码 200 – 服务器成功返回网页 404 – 请求的网页不存在 503 – 服务器超时 1xx&#xff08;临时响应&#xff09; 表示临时响应并需要请求者继续执行操作的状态码。 100&#xff08;继续&#xff09; 请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一…

OpenCV实战(5)——图像运算详解

OpenCV实战&#xff08;5&#xff09;——图像运算详解0. 前言1. 图像基本运算2. 重载图像运算符2.1 加法运算符重载2.2 分割图像通道2.3 完整代码3. 图像重映射3.1 OpenCV 重映射函数3.2 完整代码小结系列链接0. 前言 图像可以以不同的方式进行组合&#xff0c;因为它们是正则…

XGBoost论文阅读

XGBoost: A Scalable Tree Boosting System 目录 XGBoost: A Scalable Tree Boosting System 1.摘要 2.方法 2.1 正则化学习目标 2.2 梯度提升树 2.3 收缩率和列采样 2.4分裂点查找算法 1.摘要 提出了一种新的稀疏性感知算法&#xff0c;用于稀疏数据和加权全图草图&a…

Python教程:什么是三级模式和二级映像?

美国国家标准学会(American National Standards Institute,ANSI)所属的标准计划与需求委员会&#xff08;Standards Planning and Requirements Committee,SPARC)在1971年公布的研究报告中提出了ANSI-SPARC体系结构&#xff0c;即三级模式结构&#xff08;或称为三层体系结构&a…

ArcGIS基础实验操作100例--实验53导出线、面要素的坐标值

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验53 导出线、面要素的坐标值 目录 一、实验背景 二、实验数据 三、实验步骤 &#xf…

笔记杂项(一)

都是踩过的坑&#xff0c;趟过的水。 ubuntu虚拟机终端字体太小的设置方法&#xff1a;ubuntu18.04调整终端字体大小 这个方法试试看&#xff1a;https://zhuanlan.zhihu.com/p/139305626 容器里面编译内核代码&#xff0c;进程被杀掉的原因是触发了内核OOM killer&#xff0c…

干货| app自动化测试之Andriod微信小程序的自动化测试

随着微信小程序的功能和生态日益完善&#xff0c;很多公司的小程序项目页面结构越来越多&#xff0c;业务逻辑也越来越复杂。如何做好小程序的自动化测试就成为测试同学普遍面临的一大痛点难题。微信小程序小程序内嵌于微信内部&#xff0c;页面包含 Native 原生元素和 Web 元素…

华为防火墙与二层交换机对接配置VLAN上网设置

拓扑图 一、防火墙设置 1、G1/0/0接口设置IP&#xff0c;G1/0/1接口切换二层口设置VLAN&#xff0c;G1/0/0 桥接了本地无线网卡来模拟公网地址 <USG6000V1>sys [USG6000V1]sys FW1 [FW1]un in en# 设置公网IP [FW1]int g1/0/0 [FW1-GigabitEthernet1/0/0]ip addr 192.1…

package.json配置解读之入门

文章目录前言一、描述配置nameversionrepositorydescriptionkeywordslicenseauthor二、文件配置filestypemainbrowsermoduleexportsworkspaces三、脚本配置scriptsconfig四、结语前言 package.json是每个前端项目都会有的json文件&#xff0c;位于项目的根目录中。很多脚手架在…

RHCE(chrony服务器)

chrony服务器 chrony服务器是一个开源自由的网络时间协议NTP的客户端和服务器的软件&#xff0c;他能让计算机保持系统时钟和时钟服务器保持同步&#xff0c;让计算机保持精确的时间&#xff0c;chrony也可以作为服务端软件为其他计算机提供时间同步服务 chrony由两部分组成&…

openAI--十拳剑助你做AI时代的弄潮儿

AI它厉害&#xff08;diao&#xff09;吗&#xff1f; 最近大家玩chatgpt还好吗&#xff1f; 有被它的恋爱情商暴击到吗&#xff1f; 有没有觉得那在leetcode上所向无敌的技巧都是浮云吗&#xff1f; 今天&#xff0c;我为大家带来十个很好的AI平台。这一篇先介绍一下&…

【远程桌面】nomachine下载安装使用教程、zerotier下载安装使用教程超详细

文章目录一、软件介绍二、NoMachine远程桌面1.Windows下载安装使用2.Linux下载安装使用3.Android下载安装使用4.ARM下载安装使用&#xff08;未实践&#xff09;三、ZeroTier内网穿透0.官网注册账户1.Windows下载安装使用2.Linux下载安装使用3.Android下载安装使用4.ARM下载安装…

Android 学习 - 不完善

SharedPreference 共享参数用法 SharedPreference 是 Android 的一个轻量级存储工具, 采用的存储结构是Key-Value的键值对方式. 共享参数的存储介质是符合XML规范的配置文件. 保存路径是: /data/data/应用包名/shared_prefs/文件名.xml 利用元数据配置快捷菜单 (1)元数据的met…