题目描述
思路解析动画文字版
记牢这套「逐字压栈、栈顶凑齐 abc 就整段回收、最后看栈空不空」,下面每一帧都在套它。
总览 · 把字符排成一排:上方一排是要从左到右处理的 6 个字符 a、a、b、c、b、c。下方竖着的格子就是栈,现在空空的。我们一个字符一个字符地往栈里压,盯紧栈顶,只要顶上凑出 a、b、c 三个,就把它们一起回收。
预判 · 长度是 3 的倍数:先做个便宜的剪枝:合法串每段 abc 贡献 3 个字符,所以总长一定是 3 的倍数。这里长度 6,6 除以 3 余 0,过关,值得继续扫。如果长度除以 3 不为 0,可以直接判 false,省得白扫。
第 0 个 · 压入 a:处理 第 0 个 字符 a。它不是 c,不会触发回收,直接压进栈。现在栈里是 a,栈顶就一个字符,远没凑齐 abc,继续往后走。
第 1 个 · 压入 a:处理 第 1 个 字符,又是 a,照样压栈。现在栈里是两个 a。栈顶三个里没有 b、c,凑不齐,接着扫。
第 2 个 · 压入 b:处理 第 2 个 字符 b,压栈。现在栈是 a、a、b。栈顶是 b,只差一个 c 就能凑出 abc 了,但 b 自己不触发回收,得等下一个真的是 c 才行。
第 3 个 · 关键字符 c:处理 第 3 个 字符,是 c。c 是唯一能凑齐 abc 的收口字符,所以一遇到 c 就要警觉:先把它压进去,再看栈顶三个能不能恰好拼成 a、b、c。
第 3 个 · 栈顶凑齐 abc:把 c 压进栈,现在栈是 a、a、b、c。看栈顶最上面三个,自底向上正好是 a、b、c,恰好是一段完整的 abc。凑齐了,下一帧就把这三个一起回收掉。
第 3 个 · 整段回收 abc:把栈顶的 a、b、c 三个一起弹掉,相当于抹掉了刚拼出的一段 abc。栈从 a、a、b、c 缩回成单个 a。你看,c 一来就触发了一次回收,这正是栈干的活。
第 4 个 · 压入 b:处理 第 4 个 字符 b,压栈。现在栈是 a、b,栈顶又只差一个 c 就能收口。继续看下一个。
第 5 个 · 关键字符 c:处理 第 5 个 字符,又是 c。老规矩,先压进去,再检查栈顶三个是不是 abc。
第 5 个 · 栈顶凑齐 abc:压入 c,栈变成 a、b、c。栈顶三个自底向上正好是 a、b、c,又凑齐一段 abc,马上回收。
第 5 个 · 整段回收,栈清空:弹掉栈顶的 a、b、c,这一段 abc 也抹掉了。栈现在彻底空了。全部 6 个字符都处理完,栈里一干二净。
结论 · 栈空,有效:6 个字符全部扫完,栈是空的,说明 "aabcbc" 能被一层层 abc 完整抹空,它就是合法串,返回 true。和开头说的答案对上了。
对照 · 无效串怎么卡住:再看一个会失败的例子 "abccba",体会无效是怎么暴露的。栈清空重来,还是逐字压栈、栈顶凑齐 abc 就回收。注意看后半段它在哪一步卡住。
压入 a:第一个字符 a,压栈,栈是 a。
压入 b:第二个字符 b,压栈,栈是 a、b,等一个 c 收口。
压入 c,凑齐 abc:第三个字符 c,压栈,栈是 a、b、c。栈顶三个正好是一段 abc,凑齐了,下一帧回收。
回收第一段 abc:弹掉 a、b、c,栈又空了。前半截 abc 抹得很干净,目前一切正常。关键看第四个字符。
第二个 c 卡住了:第四个字符又是 c,压进去,栈里只有一个 c。问题来了:栈顶这个 c 下面既没有 b 也没有 a,凑不出 abc,回收触发不了,这个 c 只能孤零零留在栈里。无效的苗头出现了。
压入 b,顺序已乱:第五个字符 b,压栈,栈是 c、b。注意顺序乱了:正常一段是底 a、中 b、顶 c,这里底下却是个 c,永远拼不回 abc。
压入 a,栈顶是 cba:最后一个字符 a,压栈,栈是 c、b、a。栈顶三个自底向上是 c、b、a,顺序正好反了,不是 abc,回收不了。字符扫完了,栈里还堵着三个。
结论 · 栈有残余,无效:6 个字符扫完,栈里还剩 c、b、a 三个抹不掉。栈不空,说明 "abccba" 没法被一段段 abc 拼出来,返回 false。一个孤立的 c 把后面全带乱了,这就是无效的根源。
边界先想清:单段 abc 为真;长度对但凑不出 abc(如 aaa、cab)一律假。
面试重点:它是括号匹配的三字符变体;顺序敏感所以纯计数不行,必须用栈。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class Solution: def isValid(self, s: str) -> bool: if len(s) % 3: return False t = [] for c in s: t.append(c) if ''.join(t[-3:]) == 'abc': t[-3:] = [] return not t复杂度
- 时间:O(n),n 为字符串长度,每个字符压入一次、最多被回收一次,栈顶三字符比对是常数,合起来线性
- 空间:O(n),栈最坏要存下几乎所有字符(如全是 a),规模和字符串长度同级
易错点
面试追问把动画讲成自己的话
追问这题和经典的括号匹配(LC20)是不是一回事?
追问能不能不用栈,改用计数或者别的招?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
不同字符的最小子序列
LeetCode 1081 · 中等 · 沿着 栈套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题