力扣 71 & 72 题解与 JavaScript API 深度解析

16 阅读6分钟

引言

在算法的世界里,掌握高效的解题思路固然重要,但同样关键的是熟悉所用编程语言提供的强大工具(API)。本文将通过解析 LeetCode 上的经典问题——71. 简化路径72. 编辑距离,带您领略 JavaScript 内置 API 在处理字符串、数组和动态规划问题中的妙用。


第一站:力扣 71. 简化路径 (Simplify Path)

题目大意:
给定一个 Unix 风格的绝对路径,将其进行简化。路径中可能包含 . (当前目录), .. (上级目录), 以及多余的斜杠 //。要求返回一个标准的、不含这些冗余部分的路径。

核心思路:
这个问题本质上是一个路径模拟问题。我们可以使用一个“栈”来存放最终路径的有效目录名。遍历路径的各个部分,根据规则入栈或出栈,最后将栈内元素拼接即可。

JavaScript API 详解与应用:

  1. String.prototype.split(separator) :

    • 作用: 将字符串按照指定的 separator 分割成一个字符串数组。
    • 在本题中的应用: 这是解题的第一步。我们使用 path.split('/') 将输入的路径字符串按 / 分割。例如,"/a//b////c/d//././/.." 会被分割成 ["", "a", "", "b", "", "", "c", "d", "", ".", ".", "", ".."]。这极大地简化了后续对路径各部分的处理,因为现在每一项都是一个独立的组件。
  2. Array.prototype.forEach(callback) / for...of 循环:

    • 作用: 遍历数组的每一个元素。
    • 在本题中的应用: 我们需要检查 split 后得到的 parts 数组中的每一个组件 (part)。forEachfor...of 循环是遍历此数组的标准方法。
  3. Array.prototype.push(element)Array.prototype.pop() :

    • 作用: push 将一个元素添加到数组的末尾,pop 移除并返回数组的最后一个元素。
    • 在本题中的应用: 这两个方法完美模拟了栈(LIFO - Last In, First Out)的行为。当遇到一个有效目录名(非 . 且非 ..)时,使用 stack.push(part) 将其压入栈;当遇到 .. 时,使用 stack.pop() 将栈顶元素(即当前目录)弹出,模拟返回上级目录的操作。
  4. Array.prototype.join(separator) :

    • 作用: 将数组中的所有元素连接成一个字符串,并返回这个新字符串。元素之间用指定的 separator 分隔。
    • 在本题中的应用: 这是构建最终结果的关键。当栈 stack 中包含了所有规范路径的目录名后,我们调用 stack.join('/') 将它们用 / 连接起来。最后,再在前面加上一个 / 即可得到最终的规范路径 "/" + stack.join('/')

代码实现:

/**
 * @param {string} path
 * @return {string}
 */
var simplifyPath = function(path) {
    // 1. 使用 split('/') 将路径分割成组件数组
    const parts = path.split('/');

    // 2. 创建一个栈来存储有效的目录名
    const stack = [];

    // 3. 遍历分割后的组件
    for (const part of parts) {
        if (part === '..') {
            // 如果是 '..', 且栈不为空,则弹出栈顶元素 (返回上级)
            if (stack.length > 0) {
                stack.pop();
            }
        } else if (part && part !== '.') {
            // 如果是有效目录名 (非空且不是 '.'),则压入栈
            stack.push(part);
        }
        // 忽略空字符串 (由连续的 '/' 产生) 和 '.' (当前目录)
    }

    // 4. 使用 join('/') 将栈中元素连接,并在前面加上根目录 '/'
    return '/' + stack.join('/');
};

// --- 示例 ---
console.log(simplifyPath("/home/"));         // 输出: "/home"
console.log(simplifyPath("/../"));           // 输出: "/"
console.log(simplifyPath("/home//foo/"));    // 输出: "/home/foo"

第二站:力扣 72. 编辑距离 (Edit Distance)

题目大意:
给定两个单词 word1word2,问最少需要多少次操作(插入、删除、替换一个字符)能把 word1 转换成 word2

核心思路:
这是一个典型的动态规划(Dynamic Programming)问题。我们定义 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。通过分析 word1[i-1]word2[j-1] 是否相等,可以推导出状态转移方程。

JavaScript API 详解与应用:

  1. Array(length).fill(value) :

    • 作用: 创建一个指定长度的数组,并用 value 填充所有元素。
    • 在本题中的应用: 动态规划需要一个二维数组 dp。我们可以先创建一个 m+1 行的数组,然后对每一行再创建一个 n+1 列的数组。Array(m + 1).fill(null) 创建了行,map(() => Array(n + 1).fill(0)) 为每一行填充了 n+10注意: Array(m + 1).fill(Array(n + 1).fill(0)) 是错误的,因为它会用同一个数组对象的引用来填充所有行。
  2. Array.prototype.map(callback) :

    • 作用: 创建一个新数组,其结果是原数组中的每个元素都调用一次提供的函数后的返回值。
    • 在本题中的应用:fill 结合使用,为二维数组的每一行创建一个新的、独立的数组实例。Array(m + 1).fill(null).map(() => Array(n + 1).fill(0)) 是创建并初始化二维 DP 数组的常用模式。
  3. Math.min(...values) :

    • 作用: 返回一组数值中的最小值。
    • 在本题中的应用: 状态转移方程的核心是取三种操作(插入、删除、替换)中成本最低的一个。dp[i][j] = Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + cost) 这一行代码直接利用了 Math.min 来找出最小值,使代码简洁明了。
  4. String.prototype.charAt(index) / 索引访问 string[index] :

    • 作用: 获取字符串中指定索引处的字符。
    • 在本题中的应用: 在比较 word1word2 的字符时,需要访问 word1.charAt(i - 1)word2.charAt(j - 1) (或 word1[i-1]word2[j-1])。API 的存在让字符比较变得非常直接。

代码实现:

/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function(word1, word2) {
    const m = word1.length;
    const n = word2.length;

    // 1. 使用 Array.fill 和 map 创建并初始化二维 DP 数组
    const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));

    // 2. 初始化 DP 数组的边界条件
    // 将 word1 的前 i 个字符变成空字符串,需要 i 次删除
    for (let i = 0; i <= m; i++) {
        dp[i][0] = i;
    }
    // 将空字符串变成 word2 的前 j 个字符,需要 j 次插入
    for (let j = 0; j <= n; j++) {
        dp[0][j] = j;
    }

    // 3. 填充 DP 表
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (word1.charAt(i - 1) === word2.charAt(j - 1)) {
                // 如果字符相同,不需要操作
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 如果不同,取三种操作的最小值
                // dp[i-1][j] + 1: 删除 word1[i-1]
                // dp[i][j-1] + 1: 插入 word2[j-1]
                // dp[i-1][j-1] + 1: 替换 word1[i-1] 为 word2[j-1]
                dp[i][j] = Math.min(
                    dp[i - 1][j] + 1,     // 删除
                    dp[i][j - 1] + 1,     // 插入
                    dp[i - 1][j - 1] + 1  // 替换
                );
            }
        }
    }

    // 4. dp[m][n] 即为最终结果
    return dp[m][n];
};

// --- 示例 ---
console.log(minDistance("horse", "ros"));       // 输出: 3
console.log(minDistance("intention", "execution")); // 输出: 5

总结

通过对这两道经典题目的剖析,我们可以看到 JavaScript 的内置 API 如 split, join, push, pop, map, fill, Math.min 等,是如何将复杂的字符串处理和算法逻辑变得清晰、简洁和易于实现的。熟练掌握这些 API 的功能和适用场景,是提高编码效率和代码质量的关键。在解题时,优先考虑是否有现成的 API 可以使用,往往能让思路更加顺畅。