
题目考点
这道题主要考的是两个点:
- 正则表达式基础
- 开头结尾锚点
^和$ - 数字匹配
\d - 重复次数
{5} - 分组
(\d) - 正向预查
(?=...)
- 开头结尾锚点
- 审题能力
- 题目不是让你自己写一堆判断逻辑
- 而是让你提供两个正则表达式
- 再由系统用这两个正则去判断邮编是否合法
也就是说,这题本质上不是“写完整程序”,而是“把规则翻译成正则”。
审题
输入是什么
输入是一个字符串 P,表示邮政编码。
输出是什么
你不用自己 print,平台模板会帮你输出 True 或 False。
题目要求判断什么
一个合法的邮编 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_5和2_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
意思是:
- 先看
P是否符合 6 位合法范围 - 再统计交替重复对有几个
- 少于 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
- 符合
检查交替重复对:
523235356563
都不是首尾相同
所以一对都没有。
结果:
True
样例 3:552523
检查范围:
- 没问题
检查交替重复对:
525里有5...5252里有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}$
就可能只匹配到字符串中的某一部分,而不是整个字符串。
练习
你可以先自己做这道小练习,不要急着看答案。
练习题
请你写两个正则,判断一个字符串是否满足:
- 必须是一个 4 位数字,且第一位不能是 0
- 不能出现超过 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
它的意思是:
- 先记住一个数字
- 再匹配任意一个数字
- 再匹配一次刚才记住的那个数字
比如拿字符串 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)
它是在做:
从当前位置向后检查是否满足某模式
特点是:
- 只检查,不消耗字符
- 每个位置都能作为起点重新检查
- 所以能看见重叠匹配
你下次做类似题时怎么判断要不要用预查
看到下面这种需求时,就要立刻提高警惕:
- 题目要求“统计出现次数”
- 匹配结果可能彼此重叠
- 不能漏掉任何可能的起点
这时就很可能要考虑:
(?=...)
也就是“零宽断言 / 正向预查”。
小练习
你可以自己先试着判断,不要急着写代码。
练习题
对于字符串 1231231,模式仍然是 a b a,也就是:
(\d)\d\1
请你分别思考:
- 用普通模式
(\d)\d\1,findall()大概会找到几次? - 用预查模式
(?=(\d)\d\1),大概会找到几次? - 哪些匹配是重叠的?
提示
你可以把每个位置都写出来:
- 从第 1 位开始看 3 个字符
- 从第 2 位开始看 3 个字符
- 从第 3 位开始看 3 个字符
- …
这样你会非常直观地看出,为什么“每个位置都检查一次”这件事这么重要。



