LeetCode 上一道经典的字符串处理题目——71. 简化路径,这道题本质是模拟 Unix 风格路径的解析过程,考察对字符串拆分、栈的运用,难度中等但高频,适合巩固基础。
先明确题目核心需求,再一步步拆解思路、分析代码,保证新手也能看懂。
一、题目解读:什么是“简化路径”?
题目给出一个 Unix 风格的绝对路径(以 '/' 开头),要求转化为“规范路径”,核心是遵循 Unix 文件系统的规则,去掉无效的部分(比如 '.'、'..'、连续斜杠),同时满足规范格式。
1. 关键规则(必记)
-
'.' :表示当前目录,无效,需忽略;
-
'.' :表示上一级目录(父目录),需返回上一层(若有上一层);
-
连续斜杠(//、///...):视为单个斜杠 '/';
-
其他点格式(...、....):视为有效目录/文件名,不能忽略;
2. 规范路径格式(必满足)
-
必须以 '/' 开头;
-
目录之间只能有一个 '/';
-
最后一个目录(若存在)不能以 '/' 结尾;
-
不含任何 '.' 或 '..'(已被处理掉)。
3. 示例帮助理解
举两个简单示例,快速get需求:
-
输入:"/home/" → 输出:"/home"(去掉末尾斜杠);
-
输入:"/../" → 输出:"/"(根目录的上一级还是根目录);
-
输入:"/home//foo/" → 输出:"/home/foo"(连续斜杠合并,去掉末尾斜杠);
-
输入:"/a/./b/../../c/" → 输出:"/c"(处理 '.' 和 '..' 后,最终指向c目录)。
二、解题思路:为什么用“栈”?
这道题的核心逻辑是“路径层级的进退”——进入目录(push)、返回上一级(pop),这种“后进先出”的特性,刚好契合栈的数据结构(栈:只能在一端插入和删除,最后插入的最先删除)。
核心思路拆解(3步走)
-
拆分路径:用 '/' 作为分隔符,将原路径拆分成多个片段(比如 "/home//foo" 拆分后是 ["", "home", "", "foo"]);
-
处理每个片段:遍历拆分后的片段,根据规则判断如何操作栈;
-
重构路径:将栈中的有效目录,用 '/' 连接,再在开头加 '/',即为规范路径。
这里有个关键细节:拆分连续斜杠时,会出现空字符串(比如 "////" 拆分后是 ["", "", "", "", ""]),这些空字符串和 '.' 一样,都是无效的,直接忽略即可。
三、代码实战 + 逐行解析
先给出完整代码(TypeScript 版本,JavaScript 可直接去掉类型声明),再逐行拆解逻辑,保证每一步都讲清楚。
function simplifyPath(path: string): string {
// 1. 初始化栈,用于存储有效目录(核心数据结构)
const stack: string[] = [];
// 2. 用 '/' 拆分路径,得到所有片段(包含空字符串、'.'、'..'、有效目录)
const parts = path.split('/');
// 3. 遍历所有片段,处理每一种情况
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
// 情况1:片段为空(连续斜杠拆分而来)或为 '.'(当前目录),直接忽略
if (part === '' || part === '.') {
continue;
}
// 情况2:片段为 '..'(上一级目录),若栈不为空,弹出栈顶(返回上一级)
else if (part === '..') {
if (stack.length > 0) {
stack.pop();
}
}
// 情况3:有效目录/文件名,压入栈中
else {
stack.push(part);
}
}
// 4. 重构规范路径:栈中目录用 '/' 连接,开头加 '/'
return '/' + stack.join('/');
};
逐行解析(重点关注)
1. 栈的初始化
const stack: string[] = [];
栈的作用:存储“有效目录”,比如遍历到 "home"、"foo" 这种有效目录,就压入栈;遇到 ".." 且栈不为空,就弹出栈顶(相当于返回上一级目录)。栈最终存储的,就是从根目录到目标路径的所有有效目录。
2. 路径拆分
const parts = path.split('/');
这里要注意:split('/') 对连续斜杠的处理——比如 "/home//foo" 拆分后是 ["", "home", "", "foo"],连续的 '/' 会拆分出空字符串;再比如 "////" 拆分后是 ["", "", "", "", ""](n个 '/' 拆分出 n+1 个空字符串)。
这些空字符串都是无效的,后续会通过 part === '' 判断并忽略。
3. 遍历处理片段(核心逻辑)
-
if (part === '' || part === '.') { continue; } -
空字符串:来自连续斜杠,无效;
-
'.':当前目录,无需切换,无效;
-
两者都直接跳过(continue)。
-
else if (part === '..') { if (stack.length > 0) { stack.pop(); } } -
'..' 表示上一级目录,需要返回上一层;
-
注意:若栈为空(说明当前在根目录),根目录没有上一级,所以不做任何操作(避免弹出空栈)。
-
else { stack.push(part); } -
排除以上两种情况,剩下的都是有效目录/文件名(比如 "a"、"..."、"foo");
-
直接压入栈中,记录当前目录层级。
4. 重构路径
return '/' + stack.join('/');
这一步是关键,也是最简洁的处理方式:
-
stack.join('/'):将栈中的有效目录用 '/' 连接(比如栈是 ["home", "foo"],join后是 "home/foo");
-
开头加 '/':保证路径以 '/' 开头(比如上面的结果变成 "/home/foo");
-
特殊情况:若栈为空(比如输入 "/../"),join后是空字符串,最终返回 "/",刚好符合规范。
四、易错点提醒(避坑必备)
这道题看似简单,但很容易踩坑,分享3个高频易错点:
-
忽略连续斜杠拆分后的空字符串:比如忘记处理
part === '',会导致栈中存入空字符串,最终路径出现 "//"; -
处理 '..' 时,没有判断栈是否为空:若栈为空仍执行 pop(),会导致栈报错(或出现 undefined);
-
重构路径时,多此一举处理末尾斜杠:比如栈join后已经没有末尾斜杠,无需额外判断,开头加 '/' 即可(比如栈为空时,返回 "/",完美符合规范)。
五、测试用例验证
用几个典型测试用例,验证代码是否正确(建议自己动手运行一遍):
| 输入路径 | 代码输出 | 说明 |
|---|---|---|
| "/home/" | "/home" | 去掉末尾斜杠 |
| "/../" | "/" | 根目录无上级,返回根目录 |
| "/home//foo/" | "/home/foo" | 连续斜杠合并,去掉末尾斜杠 |
| "/a/./b/../../c/" | "/c" | 处理 '.' 和 '..',最终指向c |
| "/.../a/../b" | "/.../b" | "..." 是有效目录,保留 |
六、总结
这道题的核心是「用栈模拟路径层级的进退」,步骤清晰、逻辑简单,关键在于:
-
掌握 split('/') 对连续斜杠的处理;
-
准确区分三种片段(无效空串/'.'、上一级'..'、有效目录);
-
利用栈的后进先出特性,处理目录的进退;
-
简洁重构路径,避免多余操作。
代码整体复杂度:时间复杂度 O(n)(n 是路径长度,拆分、遍历、join 都是线性操作),空间复杂度 O(n)(栈最多存储所有有效目录,最坏情况和路径长度一致),完全符合题目要求。