正则表达式深度解析:从LeetCode 3136题说起

正则表达式深度解析:从LeetCode 3136题说起

引言

正则表达式(Regular Expression,简称RegEx)是一种强大的字符串匹配工具,在字符串处理、数据验证、文本搜索等场景中有着广泛的应用。本文将以LeetCode 3136题"有效单词"为例,深入探讨正则表达式的各种用法和最佳实践。

问题回顾

在LeetCode 3136题中,我们需要验证一个字符串是否为"有效单词",条件如下:

  1. 至少包含3个字符
  2. 只包含字母和数字
  3. 至少包含一个元音字母
  4. 至少包含一个辅音字母

传统的解法需要遍历字符串,逐个检查字符。而正则表达式可以用一行代码解决:

^(?=.*[aeiouAEIOU])(?=.*[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])[a-zA-Z0-9]{3,}$等信息。

元字符详解

元字符含义示例
.匹配任意字符(除换行符)a.c 匹配 “abc”, “adc”
*匹配前面的元素0次或多次ab* 匹配 “a”, “ab”, “abb”
+匹配前面的元素1次或多次ab+ 匹配 “ab”, “abb”
?匹配前面的元素0次或1次ab? 匹配 “a”, “ab”
{n}匹配前面的元素恰好n次a{3} 匹配 “aaa”
{n,}匹配前面的元素至少n次a{2,} 匹配 “aa”, “aaa”
{n,m}匹配前面的元素n到m次a{2,4} 匹配 “aa”, “aaa”, “aaaa”
^匹配字符串开始^hello 匹配以"hello"开始的字符串
$匹配字符串结束world$ 匹配以"world"结束的字符串
[]字符集合[abc] 匹配 “a”, “b”, “c”
[^]字符集合的否定[^abc] 匹配除了"a", “b”, "c"之外的字符
|逻辑或cat|dog 匹配 “cat” 或 “dog”
()分组(ab)+ 匹配 “ab”, “abab”

字符类

字符类含义等价写法
\d数字字符[0-9]
\D非数字字符[^0-9]
\w单词字符[a-zA-Z0-9_]
\W非单词字符[^a-zA-Z0-9_]
\s空白字符[ \t\n\r\f\v]
\S非空白字符[^ \t\n\r\f\v]

先行断言(Lookahead)详解

先行断言是正则表达式中的高级特性,用于检查某个位置之后的内容,但不消耗字符。

正向先行断言(Positive Lookahead)

语法:(?=pattern)

表示当前位置后面必须匹配pattern,但不会消耗字符。

# 匹配后面跟着数字的字母
[a-zA-Z](?=\d)# 示例:
# "a1" 中的 "a" 会匹配
# "ab" 中的 "a" 不会匹配

负向先行断言(Negative Lookahead)

语法:(?!pattern)

表示当前位置后面不能匹配pattern。

# 匹配后面不跟着数字的字母
[a-zA-Z](?!\d)# 示例:
# "ab" 中的 "a" 会匹配
# "a1" 中的 "a" 不会匹配

LeetCode 3136题正则表达式解析

让我们逐步分析这个正则表达式:

^(?=.*[aeiouAEIOU])(?=.*[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])[a-zA-Z0-9]{3,}$

1. 字符串边界

  • ^ - 匹配字符串开始
  • $ - 匹配字符串结束

确保整个字符串都被匹配,不允许有额外的字符。

2. 元音字母检查

(?=.*[aeiouAEIOU])
  • (?=...) - 正向先行断言
  • .* - 任意字符0次或多次
  • [aeiouAEIOU] - 元音字母字符集

这个断言检查字符串中是否至少包含一个元音字母,但不消耗字符。

3. 辅音字母检查

(?=.*[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])
  • 类似于元音检查,但检查辅音字母
  • 包含所有英文字母中的辅音

4. 主体匹配

[a-zA-Z0-9]{3,}
  • [a-zA-Z0-9] - 字母和数字字符集
  • {3,} - 至少3个字符

各语言中的正则表达式实现

JavaScript

class Solution {isValid(word) {const regex = /^(?=.*[aeiouAEIOU])(?=.*[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])[a-zA-Z0-9]{3,}$/;return regex.test(word);}
}

Python

import reclass Solution:def isValid(self, word: str) -> bool:pattern = r"^(?=.*[aeiouAEIOU])(?=.*[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])[a-zA-Z0-9]{3,}$"return bool(re.match(pattern, word))

Java

import java.util.regex.Pattern;class Solution {private static final Pattern PATTERN = Pattern.compile("^(?=.*[aeiouAEIOU])(?=.*[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])[a-zA-Z0-9]{3,}$");public boolean isValid(String word) {return PATTERN.matcher(word).matches();}
}

C++

#include <regex>class Solution {
private:static const std::regex pattern;public:bool isValid(string word) {return std::regex_match(word, pattern);}
};const std::regex Solution::pattern("^(?=.*[aeiouAEIOU])(?=.*[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])[a-zA-Z0-9]{3,}$"
);

性能优化技巧

1. 预编译正则表达式

// 好的做法:预编译
private static final Pattern PATTERN = Pattern.compile("regex");// 避免每次都编译
public boolean check(String input) {return PATTERN.matcher(input).matches();
}

2. 使用非捕获组

# 使用非捕获组 (?:...)
(?:abc|def)+# 而不是捕获组 (...)
(abc|def)+

3. 避免回溯

# 容易引起回溯的模式
(a+)+b# 优化后的模式
a+b

实际应用场景

1. 邮箱验证

^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

2. 手机号码验证(中国)

^1[3-9]\d{9}$

3. 密码强度检查

# 至少8位,包含大小写字母、数字和特殊字符
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$

4. URL验证

^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$

5. 日期格式验证(YYYY-MM-DD)

^\d{4}-\d{2}-\d{2}$

常见陷阱与错误

1. 贪婪匹配 vs 非贪婪匹配

# 贪婪匹配(默认)
.*# 非贪婪匹配
.*?

2. 字符转义

# 需要转义的特殊字符
\. \* \+ \? \^ \$ \| \\ \( \) \[ \] \{ \}

3. 多行模式

# 默认情况下,^ 和 $ 匹配整个字符串的开始和结束
# 多行模式下,它们匹配每行的开始和结束

性能对比:正则表达式 vs 传统方法

时间复杂度

  • 正则表达式: O(n),其中n是字符串长度
  • 传统遍历: O(n),同样需要遍历字符串

空间复杂度

  • 正则表达式: O(1),编译后的模式占用常数空间
  • 传统遍历: O(1),只需要几个变量

实际性能测试

// 测试代码示例
public class PerformanceTest {public static void main(String[] args) {String[] testCases = generateTestCases(10000);// 测试正则表达式方法long start = System.nanoTime();for (String word : testCases) {isValidRegex(word);}long regexTime = System.nanoTime() - start;// 测试传统方法start = System.nanoTime();for (String word : testCases) {isValidTraditional(word);}long traditionalTime = System.nanoTime() - start;System.out.println("正则表达式方法: " + regexTime / 1_000_000 + "ms");System.out.println("传统方法: " + traditionalTime / 1_000_000 + "ms");}
}

一般来说,对于简单的验证逻辑,传统方法可能略快一些,但正则表达式的优势在于:

  1. 代码更简洁
  2. 表达力更强
  3. 易于维护和修改

最佳实践建议

1. 何时使用正则表达式

  • 适用场景

    • 复杂的字符串匹配模式
    • 需要频繁修改验证规则
    • 字符串替换和提取
    • 数据清洗和格式化
  • 不适用场景

    • 简单的字符串操作
    • 性能要求极高的场景
    • 复杂的上下文相关语法

2. 代码组织

public class Validators {// 将正则表达式定义为常量private static final Pattern VALID_WORD_PATTERN = Pattern.compile("^(?=.*[aeiouAEIOU])(?=.*[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])[a-zA-Z0-9]{3,}$");private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");public static boolean isValidWord(String word) {return VALID_WORD_PATTERN.matcher(word).matches();}public static boolean isValidEmail(String email) {return EMAIL_PATTERN.matcher(email).matches();}
}

3. 文档和注释

/*** 验证单词是否有效* 规则:* 1. 至少3个字符* 2. 只包含字母和数字* 3. 至少包含一个元音字母 [aeiouAEIOU]* 4. 至少包含一个辅音字母 [bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ]*/
private static final Pattern VALID_WORD_PATTERN = Pattern.compile("^"                                                    // 字符串开始+ "(?=.*[aeiouAEIOU])"                                // 先行断言:包含元音+ "(?=.*[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ])" // 先行断言:包含辅音+ "[a-zA-Z0-9]{3,}"                                   // 字母数字,至少3个+ "$"                                                  // 字符串结束
);

进阶技巧

1. 条件匹配

# 如果前面是数字,则匹配字母;否则匹配数字
(?(?=\d)[a-zA-Z]|\d)

2. 平衡组(仅.NET支持)

# 匹配平衡的括号
\((?:[^()]|(?<open>\()|(?<-open>\)))*(?(open)(?!))\)

3. 递归匹配(部分引擎支持)

# 匹配嵌套结构
\((?:[^()]|(?R))*\)

总结

正则表达式是一个强大的文本处理工具,虽然学习曲线较陡峭,但掌握后能大大提高开发效率。通过LeetCode 3136题的例子,我们深入了解了:

  1. 基础语法:元字符、字符类、量词等
  2. 高级特性:先行断言、分组、条件匹配
  3. 实际应用:数据验证、文本处理、格式化
  4. 性能优化:预编译、避免回溯、合理使用断言
  5. 最佳实践:何时使用、如何组织代码、文档化

在实际开发中,建议根据具体场景选择合适的方法。对于简单的验证逻辑,传统方法可能更直观;对于复杂的模式匹配,正则表达式则是不二选择。

记住:正则表达式是一门艺术,需要在简洁性和可读性之间找到平衡

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

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

相关文章

映美打印机-URL页面打印

<?php /** 打印 - 映美云 https://open.jolimark.com/doc/ */ namespace Home\Controller; use Admin\Logic\OrderLogic;class PrintController extends BaseController {private $appid "";private $appkey "";//打印机编号private $deviceIds &qu…

机器学习算法 ——XGBoost 的介绍和使用

前言&#xff1a; 最近在工作中遇到一个结构化数据回归预测的问题&#xff0c;用到了很多回归算法&#xff08;如多元线性回归等&#xff09;都没有很好的效果&#xff0c;于是使用了XGBoost&#xff0c;自己也冲三个特征参数人为的增加来几个&#xff0c;训练出来的效果还是很…

Linux操作系统之信号:保存与处理信号

目录 前言&#xff1a; 前文回顾与补充&#xff1a; 信号的保存 PCB里的信号保存 sigset_t 信号集操作函数 信号的处理 信号捕捉的流程&#xff1a;​编辑 操作系统的运行原理 硬件中断 时钟中断 死循环 软中断 总结&#xff1a; 前言&#xff1a; 在上一篇文…

Spring Boot 设置滚动日志logback

Spring Boot 的 logback 框架 Spring Boot 默认内置了 Logback 作为日志实现框架&#xff0c;只需要在resources文件夹下添加一个logback-spring.xml&#xff0c;springboot会按照你的设置自动开启logback日志功能。 配置 logback-spring.xml 实现每天产生一个日志文件&#xf…

如何定义一个只能在堆上或栈上生成对象的类

在C中&#xff0c;可以通过特定的技术手段来控制对象只能在堆(heap)或栈(stack)上创建。只能在堆上创建对象的类要实现这一点&#xff0c;我们需要阻止用户直接实例化对象&#xff0c;而只能通过new操作符创建。class HeapOnly { public:static HeapOnly* create() {return new…

1.1 前端-vue3项目的创建

构建工具先搭好vue3框架 vue2的vue-cli脚手架基于webpack构建工具创建vue的框架. 而在vue3&#xff0c;可以通过vite构建工具创建vue3项目&#xff0c;性能更优。 两者创建方式的区别&#xff1a;cmd命令基于的构建工具vue2/vue3vue create 项目名称&#xff08;或 vue ui图形化…

PHP password_get_info() 函数

password_get_info() 函数用于返回指定散列&#xff08;hash&#xff09;的相关信息。 PHP 版本要求: PHP 5 > 5.5.0, PHP 7 语法 array password_get_info ( string $hash ) 参数说明&#xff1a; $hash: 一个由 password_hash() 创建的散列值。 返回值 返回三个元素…

mac上的app如何自动分类

使用文件夹进行手动分类在Finder中创建文件夹&#xff0c;将同类应用拖入同一文件夹。右键点击Dock上的应用图标&#xff0c;选择「选项」→「在Finder中显示」&#xff0c;可快速定位应用安装位置。利用Launchpad自动分组打开Launchpad&#xff08;触控板四指捏合或按F4键&…

LLM面试题目 3

LLM面试题目 3 什么是自注意力机制(Self-Attention)?为什么它在LLM中很重要?如何评估LLM的性能?LLM面临的挑战有哪些?Transformer和RNN的区别是什么?LLM如何处理多轮对话? 题目讲解 什么是自注意力机制(Self-Attention)?为什么它在LLM中很重要? 自注意力机制是一种…

linux上的软挂载操作方法

针对linux上的软挂载 可以查看linux已经挂载和存储的磁盘分区 df -hfdisk 命令是检索相同信息的另一种方法&#xff0c;可以看到所有的磁盘分区 sudo fdisk -l 要将磁盘分区 /dev/sda1 挂载到 /home/visionx/EXD1 目录 步骤 1&#xff1a;准备工作 1.创建挂载目录&#xff08;如…

SecretFlow 隐语 (2) --- 隐语架构概览

在前边两篇文章中&#xff0c;介绍了数据要素和可信流通相关的内容&#xff0c;以及基于p2p模式的安装方法 SecretFlow 隐语 (1) --- 快速入门 关于在Linux上部署 SecretFlow --- P2P部署模式 由于安装过程中出现意外报错&#xff0c;现已提交issue等待官方技术人员查阅&#x…

PHP语言基础知识(超详细)第二节

二十七. 数组的遍历 1)通过函数进行遍历:(例:demo07) (此方式不能完全遍历数组,需要借助其他功能辅助)(不推荐,了解即可) key():返回数组中当前指针所在位置的键。 current():返回数组中当前指针所在位置的值。 例如:demo07: <?php/*key():返回数组中…

网络--OSPF实验

目录 OSPF实验报告 一、实验拓扑 二、实验要求 三、实验思路 1.IP地址划分 2. OSPF 部署 3. 其它配置 4. 验证测试 四、实验步骤 1.IP 地址配置 2.OSPF 部署 3.其它配置 4.验证测试 OSPF实验报告 一、实验拓扑 二、实验要求 1、R1-R3为区域0&#xff0c;R3-R4为…

Go语言第一个程序--hello world!

文章目录一、Go 语言程序安装二、运行程序三、go mod tidy 命令四、遇到的问题五、VS Code 调试 go 程序的相关配置说明一、Go 语言程序安装 Go语言下载链接&#xff1a;https://studygolang.com/dl 双击打开下一步下一步即可。 验证安装&#xff1a;go version 二、运行程序 创…

【MCU控制 初级手札】1.1 电阻

作者&#xff1a;电控工程手札 本博文内容著作权归作者所有&#xff0c;转载请务必保留本文链接 目录1. 定义2. 电导3. 电阻率4. 电导率5. 伏安特性6. 开路与短路7. 功率8. 应用元件特性&#xff08;端子特性&#xff09;&#xff1a;元件的两个端子的电路物理量之间的代数函数…

JS中async/await功能介绍和使用演示

JS 中 async/await 功能介绍与使用演示 一、功能介绍基本概念 async&#xff1a;用于声明异步函数&#xff0c;返回一个 Promise 对象。即使函数内没有显式返回 Promise&#xff0c;也会隐式将返回值封装为 Promise.resolve()。await&#xff1a;仅能在 async 函数内部使用&…

系统调用入口机制:多架构对比理解(以 ARM64 为主)

&#x1f4d6; 推荐阅读&#xff1a;《Yocto项目实战教程:高效定制嵌入式Linux系统》 &#x1f3a5; 更多学习视频请关注 B 站&#xff1a;嵌入式Jerry 系统调用入口机制&#xff1a;多架构对比理解&#xff08;以 ARM64 为主&#xff09; 本篇内容聚焦于系统调用的入口实现机…

java MultipartFile初始化

在Java中&#xff0c;MultipartFile 是Spring框架中用于处理文件上传的接口。​开发者通常不会直接初始化MultipartFile对象&#xff0c;而是通过Spring MVC的控制器方法参数接收上传的文件。如果需要在测试或模拟场景中创建其实例&#xff0c;可以使用Spring的MockMultipartFi…

Linux C IO多路复用

在上一节利用管道实现了一个简单的聊天室&#xff0c;但这个聊天室有一个很明显的问题就是&#xff0c;当A处于读阻塞情况下是不能向B发送消息的&#xff0c;只有收到B的消息才能发送。如何实现同时既能接受B的消息&#xff0c;又能向其发送消息&#xff1f;很遗憾&#xff0c;…

day21——特殊文件:XML、Properties、以及日志框架

文章目录一、特殊文件概述二、Properties属性文件2.1 文件特点2.2 Properties类解析2.3 写入属性文件三、XML文件详解3.1 XML核心特性3.2 XML解析&#xff08;Dom4J&#xff09;3.3 XML写入3.4 XML约束&#xff08;了解&#xff09;四、日志技术&#xff08;Logback&#xff09…