news 2026/10/20 8:40:47

LeetCode 71简化路径:用栈模型攻克路径字符串规范化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 71简化路径:用栈模型攻克路径字符串规范化

刷 LeetCode 的人迟早会在字符串和栈的分水岭碰到这道leetcode71:简化路径。说实话我第一次做这道题的时候,觉得它不过是个字符串处理的小题,结果用正则、replace 一顿操作,最后被连续斜杠和..的组合拳打蒙了。这道题表面是路径字符串规范化,实际考的是对栈的理解,以及把自然语言规则翻译成代码的严谨度。无论你是准备面试、在学数据结构,还是在项目中写文件路径处理,这题都值得花半小时彻底吃透。今天我就以刷透这道题的视角,把题目规则、解题思路、完整实现和坑位都拆开说清楚。

1. 题目到底在问什么:先看懂“简化路径”的规则

1.1 从 Unix 路径说起:为什么要做“简化”

在 Unix 系统里,路径就是定位文件或目录的一串地址。比如/home/project/app,意思是从根目录/出发,依次进入home、project、app这三个目录。用户在实际操作中很少会打得那么规矩,经常会多敲几个斜杠,或者用.、..表示“当前目录”和“上一级目录”。

这时候操作系统就需要把用户输入的路径“规范化”,也就是把多余的、绕路的部分去掉,得到一个干净、没有歧义的标准路径。这道算法题模拟的正是这个规范化过程。所以题目里的“简化”不是简单压缩字符串长度,而是把语义上等价的路径统一成同一种写法。

我把这类题目当作“带着规则的字符串解析题”,它和单纯的反转字符串、子串匹配不一样,因为每个字符(尤其是.)的意义是由上下文决定的。你光看单个字符看不出结果,必须结合前面的目录层级才能判断,这天然就适合用栈来处理。

1.2 规则拆解:四个特殊符号的语义

原题给的规则其实可以收紧成四条:

  • 连续多个斜杠,比如//或///,全部等价于一个斜杠。
  • .表示当前目录,在路径里可以整体删掉。
  • ..表示退回上一级目录,需要把当前路径的最后一层目录删掉。
  • 普通目录名(包括...、..hidden这类奇怪的字符串)都当成正常目录保留下来。

举个例子,输入/a/./b/../../c/,按照语义走一遍:/a进入 a,.留在 a,/b进入 b,..回到 a,另一个..回到根目录,最后/c进入 c。所以结果就是/c。

这里我特别提醒一下:...不是返回上级,它是合法目录名。很多人会下意识把一切带点的都按“特殊符号”处理,这是这道题最大的误判点。...就是三个点组成的普通名字,和abc没有区别。同理..hidden、.git也都是普通目录名,不能因为开头有点就当成.处理。

1.3 边界条件:比想象中多

我一开始以为边界就是“多几个斜杠”,后来做测试才发现坑不少:

  • 输入是"/"或者纯斜杠如"///",结果应该是"/"。
  • 在根目录还遇到..,比如/../,没有上一级可退,结果仍然是/。
  • 路径末尾是.或..,比如/a/.结果/a,/a/..结果/。
  • 路径中间或末尾有多余斜杠,比如/home//foo/,结果/home/foo。

这些边界条件看似细碎,却决定了解法稳不稳。面试或者笔试的时候,能把边界情况主动说出来,比上来闷头写完代码更加分。

2. 解题思路对比:为什么“用栈”是最优解

2.1 暴力字符串处理为什么容易翻车

拿到题最容易想到的就是字符串替换:遇到./删掉,遇到/../就删除前面的目录。逻辑听起来简单,但你真去实现会发现事情没那么美好。

举个例子,如果无脑用replace("./", ""),输入/a/./b确实能变成/a/b,看起来没问题。可要是目录名本身是./或者包含类似模式呢?比如/a/..hidden/b,..hidden是一个普通目录,但一个不够严谨的工作人员可能把它当成..加hidden然后乱处理。

更麻烦的是..需要知道“上一级目录是谁”。字符串替换是独立处理每一处的,它没法理解上下文。比如/a/b/..,你需要知道..前面是b,然后把b删掉。如果路径很长,每遇到一个..都要重新往前找最近的有效目录,很容易出现删错、删多、删少的情况。用正则虽然能写,但表达式又长又难读,面试时也不好解释清楚。

所以暴力处理不是“不能做”,而是“不容易做对”。它的本质问题在于:你试图用“局部替换”来解决一个需要“前后文状态”的问题。

2.2 栈模型:目录切换本来就是后进先出

我个人的理解是:当你沿着路径往下走,其实是在维护一个“当前目录栈”。进入home,栈底是/,栈顶是home;再进入project,栈里就多了一个project。当你遇到..,就是从project退回home,对应 “把栈顶弹出去”。

这种“最后进入的目录最先被回退”的顺序,正是栈(Stack)的核心特征。所以这道题与其说是在考路径处理,不如说是在考你能不能把一个现实场景抽象成数据结构。

举个例子:路径/a/b/c对应栈[a, b, c]。如果某个位置出现..,就弹出c;如果栈已经空了,说明已经在根目录,此时..不产生任何效果。这么一想,实现起来就非常有画面感,也不需要再去死记硬背规则。

2.3 复杂度与为什么 O(n) 已经到极限

用栈的解法只需要从头到尾遍历一遍路径字符串,每个字符至多被处理几次,时间复杂度是 O(n)。空间上,栈里最多存放 n 个目录名,空间复杂度也是 O(n)。对于这种字符串处理题,O(n) 已经是复杂度下限,因为至少要看一遍输入才能决定结果。

可能有人会问:能不能做到 O(1) 额外空间?理论上可以,但需要原地修改字符串,还得处理不同语言里字符串不可变的问题,代码会复杂很多,而且没有必要。在面试中,O(n) 时间、O(n) 空间的解法足够清晰,也最容易向面试官讲明白。真正值得花时间的是把边界情况考虑全面,而不是去扣那一点常数空间。

3. 核心实现:手把手写出干净解法

3.1 先切分再判断:处理连续斜杠最稳妥

我的习惯做法是:先按/把整个路径切分成一个个“部件”,再逐个判断每个部件应该做什么。切分是把复杂问题变简单的重要手段。

比如"/home//foo/"用/切分,会得到["", "home", "", "foo", ""]。注意这里是 Python 的split('/')结果,里面会有空字符串。连续斜杠导致空串,开头和结尾的斜杠也会导致空串。空串对应的是“没有内容”,也就是分隔符本身,所以在后续逻辑里可以跳过。

现在每个非空部件就只有四种情况:.、..、普通目录名、以及那些带点但不是特殊符号的普通名。处理规则很清晰:

  • 如果部件是""或者".",忽略。
  • 如果部件是"..",尝试弹出栈顶目录。
  • 其他所有情况,直接入栈。

先切分再判断的好处是,你不用逐个字符去判断当前是不是在目录名中间、斜杠后面是什么,这些麻烦事都被split挡掉了。你只需要关注切出来的“词法单元”。

3.2 用栈模拟目录切换:Python 和 Java 对照

先看 Python 版本,核心代码很短:

class Solution: def simplifyPath(self, path: str) -> str: stack = [] for part in path.split('/'): if part == '' or part == '.': continue if part == '..': if stack: stack.pop() else: stack.append(part) return '/' + '/'.join(stack)

这段代码的思路和你刚才看的规则完全对应。path.split('/')把所有部件切出来;循环里先处理空串和.;遇到..就判断栈非空再弹出;其余的都当作目录名加入栈。最后用'/'拼接,并在最前面补一个根目录斜杠。

再看 Java 版本,逻辑一样,只是字符串处理方式不同:

class Solution { public String simplifyPath(String path) { Deque<String> stack = new ArrayDeque<>(); for (String part : path.split("/")) { if (part.isEmpty() || part.equals(".")) { continue; } if (part.equals("..")) { if (!stack.isEmpty()) { stack.pollLast(); } } else { stack.offerLast(part); } } return "/" + String.join("/", stack); } }

这里用Deque而不是Stack,是我个人一直推荐的写法。Java 的Stack类继承自Vector,带锁而且是不太被推荐的遗留集合。用ArrayDeque做栈,性能更好,语义也更清楚。offerLast和pollLast分别表示从尾部加入和从尾部取出。

3.3 拼接结果:顺序、斜杠和根目录

最后一步是把栈里的目录拼回路径。很多第一次写的人会在拼接时翻车,核心有两个点。

第一,顺序不能反。栈底是最上层的目录,栈顶是当前目录,所以要从栈底到栈顶拼接。用'/'.join(stack)就是按列表的自然顺序,正好是从底到顶。如果自己用for循环从末尾往前拼,顺序很容易反过来。

第二,根目录的表示。如果栈是空的,说明最终结果就是根目录,返回值应该是"/"。按照'/' + '/'.join([])这个写法,空列表拼接后是空字符串,加上/正好得到"/",所以统一处理即可。

有时候我也会用StringBuilder来拼接,但要注意顺序:

StringBuilder sb = new StringBuilder(); for (String dir : stack) { sb.append("/").append(dir); } return sb.length() == 0 ? "/" : sb.toString();

这种写法更直观,但需要额外处理空栈的情况。两种方式都可以,关键是你得清楚每一步在干什么。

4. 踩坑实录:这些细节让代码从“能跑”到“正确”

4.1 连续斜杠和末尾斜杠:split 出的空串不能乱丢

我在第一次实现时犯过非常典型的错误:直接用path.split("/"),然后对每个非空部分做入栈出栈,看似没问题,但我在判断时写成了if part != ""就入栈,结果把.和..混进去,导致结果完全不对。后来才意识到,切分出来的空串代表“多余的分隔符”,它们本身不是目录名,必须在业务逻辑里过滤掉。

同样值得警惕的是不同语言split的行为差异。Java 的" /a/ ".trim()先不谈,单说/home//foo/这个字符串,用 Java 默认的split("/")会得到["", "home", "foo"],末尾空串被丢弃了;Python 的split('/')则会保留末尾空串。如果你在 Java 里写part.isEmpty()过滤,那没问题。如果你用 C++ 的getline按/分割,也能自然跳过空的部分。理解语言差异是避免玄学报错的关键。

我建议你写完之后,专门用一个"/a//b///c/"这样的用例跑一遍,看看切分结果到底是什么,然后再确认过滤逻辑。这样你就不依赖“我猜 split 会怎样”这种模糊认知。

4.2 根目录上回退..:栈空时不能乱 pop

题目里有一个很经典的输入:"/../"。你从根目录出发,结果上来就是一个..,意思是退到上一级,可是根目录的上一级还是根目录。这个..应该被忽略,所以最终结果不是"/..",也不是"//",而是干净的"/"。

代码里对应的是:执行stack.pop()之前必须先判断stack是否为空。如果不判断,在栈空时调用pop,Python 会直接抛IndexError,Java 会抛NoSuchElementException,结果就是运行时崩溃。

我见过一些同学在面试时写出“先弹出,如果异常就忽略”的方式,这当然可行,但不够优雅。更好的做法是显式判断非空再弹出,逻辑一目了然。

4.3 合法目录名...:别一刀切

这道题最容易错、也最容易被面试官追问的地方,就是...应该怎么处理。先说结论:...是一个普通的目录名,不是..的特殊形态。

为什么?因为文件系统里的目录名可以是任何不以/结尾的合法字符串,包括...。如果我们简单地把“包含两个点”的都当作..,就会把/a/.../b错误地改成/b。正确结果应该是/a/.../b。

我在写代码时用了一个很直白的策略:除了""、"."、".."这三个精确匹配之外,其余所有字符串都按普通目录名入栈。也就是说,我不去判断“是不是包含点”,只做精确匹配。这样"..."、"..foo"、".bar"都会老老实实进栈。这个策略简单又不会出错。

5. 变种题与面试加分点

5.1 相关题目:栈的套路是一通百通

简化路径不是孤立题目。LeetCode 上很多经典题目都用到了“后进先出”这个模型。比如:

  • 有效的括号:左括号入栈,右括号出栈并匹配。
  • 逆波兰表达式求值:数字入栈,遇到运算符弹两个出来算完再压回去。
  • 最长有效括号:用栈记录括号的下标,从而计算长度。
  • 文件的最长绝对路径:模拟目录深度,也需要栈维护层级关系。

把这些题目放到一起看,你会发现核心都是“遇到某种信号就入栈,遇到另一种信号就出栈,最后从栈里整理出答案”。所以刷题时不要只背代码,要提炼出这种“信号-动作”的对应关系。这道题里的信号就是目录名和.、..,动作就是入栈和出栈。

5.2 面试现场怎么讲这道题更加分

如果面试官让你写这道题,我建议按下面顺序来:

先问清楚输入约束。比如路径是否一定以/开头?空字符串会不会出现?目录名是否包含空格?这一步既能体现严谨性,也避免写完后才发现假设错了。

然后讲思路。我会说:路径本身有层级关系,进入目录相当于入栈,..相当于出栈,.和连续斜杠需要过滤。这个模型很简单,也很容易让面试官跟上。

接着写代码。写的时候我会边写边解释:为什么跳过空串,为什么..前要判断栈非空,为什么不把...当特殊符号处理。解释动作比默写代码更重要。

最后主动跑几个测试用例,比如/home/、/../、/a/./b/../../c/。不是机械地过一遍,而是在过的时候说明每个用例验证了什么边界。

如果你能做到这个完整闭环,哪怕最终代码有个别小瑕疵,面试官给你的评价也会明显不一样。

5.3 一套可复用的测试用例清单

我在本地维护了一个测试用例表,每次改完代码就跑一遍。你可以直接拿去用:

输入路径期望输出主要验证点
/home//home去掉末尾斜杠
/..//根目录上的..被忽略
/home//foo//home/foo连续斜杠的处理
/a/./b/../../c//c.、..混合处理
/a/.../b/a/.../b...是普通目录名
////全是斜杠时返回根目录
/..hidden/..hidden开头带点的普通目录名
/a/b/../../../多次回退到根目录

我第一次用这套用例跑的时候,还真抓到了一个 bug:我在处理..的时候没有判断栈空,导致"/../"直接报错。现在每次刷题或写路径处理相关功能,都会先把这些用例过一遍。

6. 我刷这道题的真实体会

6.1 当初是怎么翻车的

我第一次做这道题时,完全没意识到要切分,而是想用正则替换来解决。写了半天,大概长这样:先替换重复斜杠,再处理./,再处理..,结果一到/a/b/../..这种需要连续回退的场景就崩了。因为正则替换是“一次性扫描”,它没法记住当前位置深度。

后来我看了别人的栈解法,恍然大悟:原来不需要在字符串里找来找去,只需要把目录名按顺序放进栈里,..弹出栈顶即可。从此之后,我再遇到任何带层级关系的字符串处理,都会下意识地想“能不能用栈建模”。这个思维转换,比背住代码重要得多。

6.2 给后来者的建议

如果你现在正被这道题折磨,我的建议是:先不要立刻看题解,拿张草稿纸把一个复杂样例比如/a/./b/../../c/手工走一遍,把每一步的“当前路径”写出来。等你走完一遍,你自己就能感觉到栈的存在。然后再打开编辑器自己写,写错也没关系,对照测试用例调试一遍,收获会非常大。

另外,不要只满足于通过这道题。把它和“有效的括号”这类题放一起总结,提炼出“入栈信号”和“出栈信号”的通用框架。刷题到最后,拼的就是这些抽象能力。

最后分享一个小技巧:做字符串路径类题目时,先打印出切分后的列表,看看每个语言到底把/切成了什么。你只要亲眼看过一次空串和普通目录的分布,以后就再也不会对split行为产生误解了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 8:40:38

从语音识别到会议纪要:用腾讯云API搭建自动转写工具

1. 为什么我盯上了"会议小帮手"这个需求先说个真实场景。我平时要参加不少技术评审会和需求对齐会&#xff0c;一场会短则四十分钟&#xff0c;长则一个多小时。会上大家你一句我一句&#xff0c;信息密度其实很高&#xff0c;但真正落到"谁负责什么、什么时候交…

作者头像 李华
网站建设 2026/10/20 8:40:26

PMSM转子初始位置检测:脉冲注入法原理与C代码实现

伺服驱动器上电的那一瞬&#xff0c;MCU面对的第一个问题不是PI参数整定&#xff0c;而是&#xff1a;电机的转子现在朝哪个方向。这个问题答错了&#xff0c;后面的一切都是白做——矢量控制的电流方向会错、力矩方向会反、严重时直接飞车或堵转。尤其在高精度场合&#xff0c…

作者头像 李华
网站建设 2026/10/20 8:39:42

HTML转Word技术方案全解析:保真度、兼容性与工程落地

1. 项目概述&#xff1a;为什么“HTML转Word”不是个简单按钮&#xff0c;而是一场格式、语义与兼容性的三方博弈 你有没有遇到过这样的场景&#xff1a;市场同事发来一份用Markdown写好的产品文案&#xff0c;前端团队用Vue渲染成精美的HTML页面&#xff0c;领导却突然要求“…

作者头像 李华
网站建设 2026/10/20 8:39:12

pytest参数化策略优化:从数据驱动到接口自动化测试的工程实践

做测试开发的朋友应该都有体会&#xff0c;pytest 的参数化策略优化是日常绕不开的话题。pytest.mark.parametrize 作为最常用的功能&#xff0c;几乎每个测试类里都挂着那么一两个装饰器。但很多团队用着用着就会发现&#xff0c;参数化用例一旦上了规模&#xff0c;维护成本就…

作者头像 李华
网站建设 2026/10/20 8:38:46

多模态LoRA微调一杯咖啡速成:DevPod+Llama-Factory实战

多模态微调这个词最近出现频率很高&#xff0c;但我一直觉得它被包装得有点吓人。不少朋友一听到要微调一个能看图、能读文字的大模型&#xff0c;脑子里浮现的画面就是&#xff1a;一堆显卡、几十个小时的等待、还有调不完的bug。实际上&#xff0c;在目前已经相当成熟的工具链…

作者头像 李华
网站建设 2026/10/20 8:37:30

Dart Lambda运行时鸿蒙化实践:端云协同与冷启动优化

1. 为什么要把 Dart Lambda 运行时搬进 OpenHarmony 设备这阵子一直在折腾一件事&#xff1a;把 aws_lambda_dart_runtime_ns 这一套 Dart 实现的 Lambda 运行时组件&#xff0c;从纯 Dart 环境迁移到 OpenHarmony 上&#xff0c;让 Flutter 应用在鸿蒙设备里也能直接承载 Lamb…

作者头像 李华