re 正则表达式2

题目考点

这道题主要考的是两个点:

  1. 正则表达式基础
    • 开头结尾锚点 ^$
    • 数字匹配 \d
    • 重复次数 {5}
    • 分组 (\d)
    • 正向预查 (?=...)
  2. 审题能力
    • 题目不是让你自己写一堆判断逻辑
    • 而是让你提供两个正则表达式
    • 再由系统用这两个正则去判断邮编是否合法

也就是说,这题本质上不是“写完整程序”,而是“把规则翻译成正则”。


审题

输入是什么

输入是一个字符串 P,表示邮政编码。

输出是什么

你不用自己 print,平台模板会帮你输出 TrueFalse

题目要求判断什么

一个合法的邮编 P 必须同时满足两个条件:

条件 1:必须在 100000 到 999999 之间

这句话翻译一下,其实就是:

  • 必须是 6 位数字
  • 第一位不能是 0

因为:

  • 100000 是 6 位
  • 999999 也是 6 位
  • 所以范围内的数一定都是 6 位数
  • 并且首位必须是 1~9

所以这一条,本质上就是:

匹配一个 6 位数,且第一位是 1~9


条件 2:不能有超过 1 对“交替重复数字对”

题目里说的 alternating repetitive digit pair,意思是:

两个相同数字中间正好隔了一个数字。

例如:

  • 121426 里,1_1 成立,所以有 1 对
  • 523563 里,没有这种结构
  • 552523 里,5_52_2 都成立,所以有 2 对

注意这里最容易忽略的点:

交替重复对是可以“重叠”统计的

例如样例 110000

  • 第 3、5 位:0 0
  • 第 4、6 位:0 0

所以这里其实有 2 对

这正是这题最难的地方。


思路提示

先不要急着写代码,我们先把题目拆成两个独立任务。

第一步:写“范围检查”的正则

你先想:

  • 一共 6 位
  • 第一位不能是 0
  • 后面 5 位可以是任意数字

这个正则其实不难。


第二步:写“交替重复对”的正则

你要找的是这种结构:

数字  任意一个数字  同样的数字

也就是像:

1 2 1
5 3 5
0 0 0

这里要注意,不是找“连续重复”,而是找“隔一个字符后重复”。

所以你会想到写成:

(\d)\d\1

这个方向是对的,但还不够,因为它会漏掉重叠匹配

所以这一步的关键是:

要用一种“当前位置看一眼是否满足,但不真正吃掉字符”的写法。

这就是 正向预查 (?=...)


完整设计思路

现在把整个思路整理成完整步骤。

第 1 步:处理第一个条件

范围 100000 ~ 999999 可以直接转成:

首位:1 到 9
后面:任意 5 个数字
总长度:必须刚好 6 位

所以正则应该长这样:

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

解释:

  • ^:从字符串开头开始
  • [1-9]:第一位是 1 到 9
  • \d{5}:后面跟 5 个数字
  • $:到字符串结尾结束

这样就保证了它既是 6 位,又不会以 0 开头。


第 2 步:处理第二个条件

交替重复数字对的结构是:

a b a

如果只写:

(\d)\d\1

它能找到一个这样的片段,但有一个问题:

它不能正确处理重叠匹配

比如 0000

  • 前三个字符 000 是一个 a b a
  • 后三个字符 000 也是一个 a b a

这两个是重叠的。

普通匹配在找到第一个 000 后,会把指针跳过去,就可能漏掉第二个。

所以正确写法是:

(?=(\d)\d\1)

它的意思是:

  • 在当前位置,往后看一眼
  • 看看是不是存在 (\d)\d\1 这样的结构
  • 不消耗字符
  • 这样下一位还能继续检查
  • 所以重叠情况也能被统计出来

这就是为什么这题必须用预查。


第 3 步:配合模板理解整个判断过程

题目模板会这样判断:

bool(re.match(regex_integer_in_range, P))
and len(re.findall(regex_alternating_repetitive_digit_pair, P)) < 2

意思是:

  1. 先看 P 是否符合 6 位合法范围
  2. 再统计交替重复对有几个
  3. 少于 2 个才算合法

也就是:

  • 0 对:合法
  • 1 对:合法
  • 2 对及以上:不合法

代码实现

这题你只需要补两个正则:

regex_integer_in_range = r"^[1-9]\d{5}$"
regex_alternating_repetitive_digit_pair = r"(?=(\d)\d\1)"

如果放进完整模板里,大致是这样:

import re

P = input().strip()

regex_integer_in_range = r"^[1-9]\d{5}$"
regex_alternating_repetitive_digit_pair = r"(?=(\d)\d\1)"

print(
    bool(re.match(regex_integer_in_range, P))
    and len(re.findall(regex_alternating_repetitive_digit_pair, P)) < 2
)

运行演示

样例 1:121426

先检查范围:

  • 6 位
  • 首位是 1
  • 符合

再找交替重复对:

  • 121:有 1 对
  • 其他位置没有新的

所以总数是 1,对数 < 2

结果:

True

样例 2:523563

检查范围:

  • 6 位,首位不是 0
  • 符合

检查交替重复对:

  • 523
  • 235
  • 356
  • 563

都不是首尾相同

所以一对都没有。

结果:

True

样例 3:552523

检查范围:

  • 没问题

检查交替重复对:

  • 525 里有 5...5
  • 252 里有 2...2

一共 2 对。

结果:

False

样例 4:110000

检查范围:

  • 是 6 位,首位不是 0
  • 符合

检查交替重复对:

字符串是:

1 1 0 0 0 0

从每个位置往后看:

  • 110:不是
  • 100:不是
  • 000:是 1 对
  • 下一个位置开始的 000:又是 1 对

共 2 对。

结果:

False

这也说明了为什么必须用:

(?=(\d)\d\1)

而不能只写普通的:

(\d)\d\1

方法总结

这类题下次怎么下手,可以记住这个顺序。

第一类:范围型规则

看到“某个数字范围”时,先别急着想大小比较,先想能不能转换成:

  • 固定长度
  • 首位限制
  • 每一位的字符范围

这题就是:

  • 6 位
  • 第一位 1~9
  • 后 5 位 0~9

第二类:模式识别型规则

看到“某种重复结构”时,先把它翻译成字符模式。

这题的“交替重复”就是:

a b a

所以自然对应:

(\d)\d\1

第三类:重叠匹配

只要题目里可能出现这种情况:

0000
aaaa
12121

就要警惕:

普通匹配会不会漏掉“重叠出现”的情况?

如果会漏,就要考虑 正向预查 (?=...)


易错点回顾

这题最容易错的地方有三个:

1. 把范围写成普通数字判断

题目不让你写 if 来判断大小,而是要写正则,所以要把数值范围转成字符串规则。


2. 第二个正则写成 (\d)\d\1

这个写法只能找普通匹配,可能漏掉重叠情况。

正确应该写:

(?=(\d)\d\1)

3. 忘记加 ^$

如果不加:

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

就可能只匹配到字符串中的某一部分,而不是整个字符串。


练习

你可以先自己做这道小练习,不要急着看答案。

练习题

请你写两个正则,判断一个字符串是否满足:

  1. 必须是一个 4 位数字,且第一位不能是 0
  2. 不能出现超过 1 对“首尾相同、中间隔 1 位”的数字模式

例如:

  • 1213 有 1 对
  • 3434 有 2 对
  • 1001 没有这种对

提示

你可以直接模仿今天这道题的思路:

  • 第一个正则先写“4 位且首位非 0”
  • 第二个正则先写出 a b a 的结构
  • 再思考是否需要处理“重叠匹配”

补充说明:为什么 (?=(\d)\d\1) 能看见重叠匹配,而普通 (\d)\d\1 看不见

这一节是这道题里最关键、也最容易让初学者“看着懂,实际上没真懂”的地方。

很多人第一次看到这两个正则,会觉得它们长得几乎一样:

(\d)\d\1

(?=(\d)\d\1)

里面核心结构明明都是“一个数字 + 任意一个数字 + 前面那个数字”,为什么前者会漏,后者却能把重叠情况也找出来?

真正的区别,不在“匹配的内容”本身,而在于:

它们匹配成功之后,正则引擎的当前位置会不会往前走。

这才是根本。


先把“重叠匹配”到底是什么说清楚

所谓重叠匹配,就是两个满足条件的片段,它们会共享一部分字符。

例如字符串:

0000

如果我们要找的模式是:

a b a

那么在 0000 里其实有两段:

  • 第 1~3 个字符:000
  • 第 2~4 个字符:000

你会发现,这两段不是彼此分开的,它们共用了中间两个 0

这就叫重叠


普通匹配为什么看不见重叠

先看普通正则:

(\d)\d\1

它的意思是:

  1. 先记住一个数字
  2. 再匹配任意一个数字
  3. 再匹配一次刚才记住的那个数字

比如拿字符串 0000 来看。


第一步:从第 1 个字符开始尝试

字符串位置可以这样标出来:

位置:  1 2 3 4
字符:  0 0 0 0

从位置 1 开始看:

  • 第一个 (\d) 匹配到第 1 个 0
  • 中间那个 \d 匹配到第 2 个 0
  • 最后 \1 匹配到第 3 个 0

于是,正则成功匹配到了这一段:

000

也就是第 1~3 位。


第二步:问题来了——匹配完之后,指针往哪走?

普通匹配的特点是:

它一旦匹配成功,就把这一整段“吃掉了”。

这里“吃掉”不是说字符串被删除,而是说:

  • 正则引擎会认为“这一段已经匹配完成了”
  • 下一次继续找时,会从这段后面继续找

也就是说,刚才匹配了第 1~3 位,接下来就从第 4 位后面继续。

但第 4 位后面已经没东西了。

所以它只找到 1 次

它不会再回头去试“第 2 位开始的那一段 000”。

这就是为什么普通匹配看不见重叠。


你可以把普通匹配想成“真的拿走了一块”

打个比方,字符串像一排砖块:

0 0 0 0

普通模式 (\d)\d\1 找到第一个 000 时,相当于一下子拿走了前面三块砖来组成一个匹配结果:

[0 0 0] 0

接下来它不会只挪一步重新试,而是直接跳到这块后面继续找。

所以,第二个从位置 2 开始的 000,就被跳过去了。


(?=(\d)\d\1) 为什么可以?

这里就要理解 正向预查

(?=...) 的意思不是“真正匹配一段内容”,而是:

在当前位置往后看一眼,检查这里后面是不是满足某个模式。

注意这句话里最重要的四个字:

往后看一眼。

它的本质是“检查”,不是“消耗”。


什么叫“不消耗字符”

普通正则匹配成功后,会把那部分字符当成已经走过了。

而预查 (?=...) 虽然也会判断成功或失败,但它有一个非常重要的特性:

它本身不占用字符位置。

也就是说:

  • 它只负责判断“从这里开始往后看,能不能组成目标模式”
  • 但判断完以后,当前位置还停在原地
  • 它不会把字符“吃掉”

这就是它能处理重叠匹配的根本原因。


0000 具体模拟一遍

现在我们来看:

(?=(\d)\d\1)

还是字符串:

位置:  1 2 3 4
字符:  0 0 0 0

从位置 1 开始

预查会问:

从位置 1 开始往后看,能不能看到一个 (\d)\d\1

答案是能:

  • 第 1 位 0
  • 第 2 位 0
  • 第 3 位 0

所以,位置 1 这里算发现了一个匹配。

但是注意:

预查只是看到了,并没有把第 1~3 位吃掉。

所以接下来,正则引擎还可以继续从下一个位置再试。


再从位置 2 开始

它又问:

从位置 2 开始往后看,能不能看到一个 (\d)\d\1

答案还是能:

  • 第 2 位 0
  • 第 3 位 0
  • 第 4 位 0

所以,位置 2 又发现了一个匹配。

于是总共就找到了 2 次。

这正好对应题目要统计的两对交替重复数字。


一句话总结两者差别

可以先记住这句非常核心的话:

普通模式 (\d)\d\1 是“真正匹配并吃掉一段字符”,而预查模式 (?=(\d)\d\1) 是“站在每个位置往后看一眼,但不吃掉字符”。

所以:

  • 会“吃掉字符”的普通匹配,容易漏掉重叠部分
  • 不“吃掉字符”的预查,可以让每个位置都被检查到

再看一个更直观的例子:12121

字符串:

位置:  1 2 3 4 5
字符:  1 2 1 2 1

我们要找 a b a


用普通匹配 (\d)\d\1

从位置 1 开始:

  • 121 成功

然后它会跳到第 4 位继续。

第 4 位后面只剩 21,不够 3 个字符了。

所以它只找到一次。

但实际上,字符串里还有另一段:

  • 第 3~5 位 121

这就是一个被漏掉的重叠匹配。


用预查 (?=(\d)\d\1)

它会在每个位置都看:

  • 位置 1 开始:121,成功
  • 位置 2 开始:212,成功
  • 位置 3 开始:121,成功

所以它能发现多个重叠出现的结构。

当然,在这道邮编题里,12121 只是帮助你理解原理,实际邮编长度固定为 6。


为什么 re.findall() 配合预查特别合适

这题平台用的是:

re.findall(regex_alternating_repetitive_digit_pair, P)

如果正则写成:

(\d)\d\1

那么 findall() 会找“普通匹配到的各段”,匹配完一段后继续往后找,因此会漏掉重叠。

但如果写成:

(?=(\d)\d\1)

那么 findall() 实际上是在:

  • 从每个位置都触发一次“预查”
  • 只要当前位置后面能组成 a b a
  • 就算找到一次

于是所有可能的起点都被检查了,重叠也就不会漏。


你可以把预查看成“站桩扫描”

这个比喻很适合初学者记忆。

普通匹配

像一个人在路上走,看到一段符合条件的字符,就把这一整段拿走,然后跳到后面继续。

所以中间重叠的部分,可能没机会再看。

预查匹配

像一个人在每一个位置都站一下:

  • 站在这里,往前看?不对,是往后看
  • 看一眼后面三格是不是符合 a b a
  • 看完不动,再挪到下一个位置继续看

所以不会漏。


为什么这题必须用预查,而不是普通分组

因为题目要的不是“有没有某一段匹配”,而是:

到底有多少对 alternating repetitive digit pair。

而这些 pair 可能是重叠出现的。

比如 110000

1 1 0 0 0 0
    |___|   第 3,5 位:0...0
      |___| 第 4,6 位:0...0

这两对是重叠的。

如果你用普通的:

(\d)\d\1

就容易少算。

而题目明确要求不能超过 1 对,所以少算就会直接判错。


本节小结

这一节你真正要抓住的,不是“预查很高级”,而是下面这组本质区别:

普通模式 (\d)\d\1

它是在做:

真正匹配一段字符

特点是:

  • 匹配成功后会向前推进
  • 已经匹配过的那段不会重新作为新起点去试
  • 因此可能漏掉重叠匹配

预查模式 (?=(\d)\d\1)

它是在做:

从当前位置向后检查是否满足某模式

特点是:

  • 只检查,不消耗字符
  • 每个位置都能作为起点重新检查
  • 所以能看见重叠匹配

你下次做类似题时怎么判断要不要用预查

看到下面这种需求时,就要立刻提高警惕:

  1. 题目要求“统计出现次数”
  2. 匹配结果可能彼此重叠
  3. 不能漏掉任何可能的起点

这时就很可能要考虑:

(?=...)

也就是“零宽断言 / 正向预查”。


小练习

你可以自己先试着判断,不要急着写代码。

练习题

对于字符串 1231231,模式仍然是 a b a,也就是:

(\d)\d\1

请你分别思考:

  1. 用普通模式 (\d)\d\1findall() 大概会找到几次?
  2. 用预查模式 (?=(\d)\d\1),大概会找到几次?
  3. 哪些匹配是重叠的?

提示

你可以把每个位置都写出来:

  • 从第 1 位开始看 3 个字符
  • 从第 2 位开始看 3 个字符
  • 从第 3 位开始看 3 个字符

这样你会非常直观地看出,为什么“每个位置都检查一次”这件事这么重要。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇