LeetCode 71. 简化路径:详细解析 + 代码实战

5 阅读5分钟

LeetCode 上一道经典的字符串处理题目——71. 简化路径,这道题本质是模拟 Unix 风格路径的解析过程,考察对字符串拆分、栈的运用,难度中等但高频,适合巩固基础。

先明确题目核心需求,再一步步拆解思路、分析代码,保证新手也能看懂。

一、题目解读:什么是“简化路径”?

题目给出一个 Unix 风格的绝对路径(以 '/' 开头),要求转化为“规范路径”,核心是遵循 Unix 文件系统的规则,去掉无效的部分(比如 '.'、'..'、连续斜杠),同时满足规范格式。

1. 关键规则(必记)

  • '.' :表示当前目录,无效,需忽略;

  • '.' :表示上一级目录(父目录),需返回上一层(若有上一层);

  • 连续斜杠(//、///...):视为单个斜杠 '/';

  • 其他点格式(...、....):视为有效目录/文件名,不能忽略;

2. 规范路径格式(必满足)

  • 必须以 '/' 开头;

  • 目录之间只能有一个 '/';

  • 最后一个目录(若存在)不能以 '/' 结尾;

  • 不含任何 '.' 或 '..'(已被处理掉)。

3. 示例帮助理解

举两个简单示例,快速get需求:

  • 输入:"/home/" → 输出:"/home"(去掉末尾斜杠);

  • 输入:"/../" → 输出:"/"(根目录的上一级还是根目录);

  • 输入:"/home//foo/" → 输出:"/home/foo"(连续斜杠合并,去掉末尾斜杠);

  • 输入:"/a/./b/../../c/" → 输出:"/c"(处理 '.' 和 '..' 后,最终指向c目录)。

二、解题思路:为什么用“栈”?

这道题的核心逻辑是“路径层级的进退”——进入目录(push)、返回上一级(pop),这种“后进先出”的特性,刚好契合的数据结构(栈:只能在一端插入和删除,最后插入的最先删除)。

核心思路拆解(3步走)

  1. 拆分路径:用 '/' 作为分隔符,将原路径拆分成多个片段(比如 "/home//foo" 拆分后是 ["", "home", "", "foo"]);

  2. 处理每个片段:遍历拆分后的片段,根据规则判断如何操作栈;

  3. 重构路径:将栈中的有效目录,用 '/' 连接,再在开头加 '/',即为规范路径。

这里有个关键细节:拆分连续斜杠时,会出现空字符串(比如 "////" 拆分后是 ["", "", "", "", ""]),这些空字符串和 '.' 一样,都是无效的,直接忽略即可。

三、代码实战 + 逐行解析

先给出完整代码(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个高频易错点:

  1. 忽略连续斜杠拆分后的空字符串:比如忘记处理 part === '',会导致栈中存入空字符串,最终路径出现 "//";

  2. 处理 '..' 时,没有判断栈是否为空:若栈为空仍执行 pop(),会导致栈报错(或出现 undefined);

  3. 重构路径时,多此一举处理末尾斜杠:比如栈join后已经没有末尾斜杠,无需额外判断,开头加 '/' 即可(比如栈为空时,返回 "/",完美符合规范)。

五、测试用例验证

用几个典型测试用例,验证代码是否正确(建议自己动手运行一遍):

输入路径代码输出说明
"/home/""/home"去掉末尾斜杠
"/../""/"根目录无上级,返回根目录
"/home//foo/""/home/foo"连续斜杠合并,去掉末尾斜杠
"/a/./b/../../c/""/c"处理 '.' 和 '..',最终指向c
"/.../a/../b""/.../b""..." 是有效目录,保留

六、总结

这道题的核心是「用栈模拟路径层级的进退」,步骤清晰、逻辑简单,关键在于:

  1. 掌握 split('/') 对连续斜杠的处理;

  2. 准确区分三种片段(无效空串/'.'、上一级'..'、有效目录);

  3. 利用栈的后进先出特性,处理目录的进退;

  4. 简洁重构路径,避免多余操作。

代码整体复杂度:时间复杂度 O(n)(n 是路径长度,拆分、遍历、join 都是线性操作),空间复杂度 O(n)(栈最多存储所有有效目录,最坏情况和路径长度一致),完全符合题目要求。