引言
在算法的世界里,掌握高效的解题思路固然重要,但同样关键的是熟悉所用编程语言提供的强大工具(API)。本文将通过解析 LeetCode 上的经典问题——71. 简化路径 和 72. 编辑距离,带您领略 JavaScript 内置 API 在处理字符串、数组和动态规划问题中的妙用。
第一站:力扣 71. 简化路径 (Simplify Path)
题目大意:
给定一个 Unix 风格的绝对路径,将其进行简化。路径中可能包含 . (当前目录), .. (上级目录), 以及多余的斜杠 //。要求返回一个标准的、不含这些冗余部分的路径。
核心思路:
这个问题本质上是一个路径模拟问题。我们可以使用一个“栈”来存放最终路径的有效目录名。遍历路径的各个部分,根据规则入栈或出栈,最后将栈内元素拼接即可。
JavaScript API 详解与应用:
-
String.prototype.split(separator):- 作用: 将字符串按照指定的
separator分割成一个字符串数组。 - 在本题中的应用: 这是解题的第一步。我们使用
path.split('/')将输入的路径字符串按/分割。例如,"/a//b////c/d//././/.."会被分割成["", "a", "", "b", "", "", "c", "d", "", ".", ".", "", ".."]。这极大地简化了后续对路径各部分的处理,因为现在每一项都是一个独立的组件。
- 作用: 将字符串按照指定的
-
Array.prototype.forEach(callback)/for...of循环:- 作用: 遍历数组的每一个元素。
- 在本题中的应用: 我们需要检查
split后得到的parts数组中的每一个组件 (part)。forEach或for...of循环是遍历此数组的标准方法。
-
Array.prototype.push(element)和Array.prototype.pop():- 作用:
push将一个元素添加到数组的末尾,pop移除并返回数组的最后一个元素。 - 在本题中的应用: 这两个方法完美模拟了栈(LIFO - Last In, First Out)的行为。当遇到一个有效目录名(非
.且非..)时,使用stack.push(part)将其压入栈;当遇到..时,使用stack.pop()将栈顶元素(即当前目录)弹出,模拟返回上级目录的操作。
- 作用:
-
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)
题目大意:
给定两个单词 word1 和 word2,问最少需要多少次操作(插入、删除、替换一个字符)能把 word1 转换成 word2。
核心思路:
这是一个典型的动态规划(Dynamic Programming)问题。我们定义 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。通过分析 word1[i-1] 和 word2[j-1] 是否相等,可以推导出状态转移方程。
JavaScript API 详解与应用:
-
Array(length).fill(value):- 作用: 创建一个指定长度的数组,并用
value填充所有元素。 - 在本题中的应用: 动态规划需要一个二维数组
dp。我们可以先创建一个m+1行的数组,然后对每一行再创建一个n+1列的数组。Array(m + 1).fill(null)创建了行,map(() => Array(n + 1).fill(0))为每一行填充了n+1个0。注意:Array(m + 1).fill(Array(n + 1).fill(0))是错误的,因为它会用同一个数组对象的引用来填充所有行。
- 作用: 创建一个指定长度的数组,并用
-
Array.prototype.map(callback):- 作用: 创建一个新数组,其结果是原数组中的每个元素都调用一次提供的函数后的返回值。
- 在本题中的应用: 与
fill结合使用,为二维数组的每一行创建一个新的、独立的数组实例。Array(m + 1).fill(null).map(() => Array(n + 1).fill(0))是创建并初始化二维 DP 数组的常用模式。
-
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来找出最小值,使代码简洁明了。
-
String.prototype.charAt(index)/ 索引访问string[index]:- 作用: 获取字符串中指定索引处的字符。
- 在本题中的应用: 在比较
word1和word2的字符时,需要访问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 可以使用,往往能让思路更加顺畅。