对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎
LeetCode 763. 划分字母区间
1. 题目描述
LeetCode 763. 划分字母区间要求:给定一个字符串 S,将字符串划分为尽可能多的片段,使得每个字母最多出现在一个片段中。返回一个表示每个片段长度的列表。
1.1 示例说明
- 输入 :
S = "ababcbacadefegdehijhklij" - 输出 :
[9,7,8] - 解释 :
划分结果为"ababcbaca"、"defegde"、"hijhklij"。每个字母(如a、b、c等)在同一个片段中出现,且不会跨片段重复。
1.2 注意点
- 字符串
S只包含小写英文字母。 - 划分需要最大化片段数量,即每个片段尽可能短,但前提是满足字母不跨片段的条件。
- 输出是片段长度的列表,顺序与划分顺序一致。
2. 问题分析
对于前端开发者,这个问题可以类比于组件或模块的拆分:在大型前端项目中,我们希望将代码拆分为独立模块,每个模块负责特定功能,且依赖关系清晰(类似字母不跨片段)。这里,字符串中的字母代表"依赖",我们需要找到最小粒度的划分。
关键挑战在于如何高效确定划分点。暴力枚举所有可能的划分点会指数级增长,不可行。因此,我们需要利用字符串特性进行优化。
3. 解题思路
3.1 核心思路:贪心算法
贪心算法通过局部最优选择达到全局最优。对于此题:
- 预处理:记录每个字母在字符串中最后出现的位置。
- 遍历字符串:维护当前片段的开始和结束位置。对于每个字符,更新当前片段的结束位置为当前字符最后出现位置的最大值。
- 划分时机:当遍历索引等于当前片段的结束位置时,表示一个片段结束,记录其长度,并开始下一个片段。
这种方法确保每个片段包含所有必需字母,且不重叠,从而最大化片段数量。
3.2 复杂度分析
- 时间复杂度: O(n),其中 n 是字符串长度。需要两次遍历:一次记录最后位置,一次划分片段。
- 空间复杂度: O(1),因为字符集为小写字母,使用固定大小的数组(长度 26)存储最后位置。
- 最优解: 贪心算法是最优解,因为它在线性时间内解决问题,且无需额外数据结构。
3.3 其他思路对比
- 暴力枚举: 枚举所有可能的划分点组合,检查每个组合是否满足条件。时间复杂度 O(2^n),空间复杂度 O(n),不适用于大规模输入。
- 动态规划: 可设计状态表示划分,但复杂度较高,贪心更简洁高效。
因此,贪心算法是推荐方法。
4. 代码实现
以下用 JavaScript 实现贪心算法,代码包含详细注释和步骤分解。
4.1 JavaScript 实现
javascript
/**
* LeetCode 763. 划分字母区间
* 贪心算法实现:基于字母最后出现位置进行划分
* @param {string} S - 输入字符串,只包含小写字母
* @return {number[]} - 片段长度列表
*/
var partitionLabels = function(S) {
// 步骤1: 预处理,记录每个字母最后出现的位置
const lastOccurrence = new Array(26).fill(-1); // 26个小写字母
const charCodeA = 'a'.charCodeAt(0); // 'a'的ASCII码,用于索引映射
for (let i = 0; i < S.length; i++) {
const charIndex = S.charCodeAt(i) - charCodeA; // 将字符映射到0-25
lastOccurrence[charIndex] = i; // 更新最后出现位置
}
// 步骤2: 遍历字符串,进行贪心划分
const result = []; // 存储片段长度
let start = 0; // 当前片段的开始索引
let end = 0; // 当前片段的结束索引
for (let i = 0; i < S.length; i++) {
const charIndex = S.charCodeAt(i) - charCodeA;
// 更新当前片段的结束位置:取当前字符最后出现位置的最大值
end = Math.max(end, lastOccurrence[charIndex]);
// 当遍历到当前片段的结束位置时,表示一个片段完成
if (i === end) {
// 计算片段长度并添加到结果
result.push(end - start + 1);
// 开始下一个片段,更新start为下一个索引
start = i + 1;
}
}
return result;
};
// 示例测试
console.log(partitionLabels("ababcbacadefegdehijhklij")); // 输出: [9,7,8]
console.log(partitionLabels("eccbbbbdec")); // 输出: [10]
4.2 步骤分解说明
-
预处理阶段:
- 创建一个长度为 26 的数组
lastOccurrence,初始化为 -1。 - 遍历字符串
S,对于每个字符,计算其索引(通过 ASCII 码减去 'a' 的码值),并更新数组对应位置为当前索引。这确保了数组存储每个字母的最后出现位置。
- 创建一个长度为 26 的数组
-
划分阶段:
- 初始化
start和end为 0,分别表示当前片段的开始和结束索引。 - 再次遍历字符串
S:- 对于每个字符,获取其最后出现位置,并更新
end为Math.max(end, lastOccurrence[charIndex])。这保证了当前片段包含所有已遍历字符的完整出现。 - 检查当前索引
i是否等于end。如果相等,说明当前片段已覆盖所有必需字母,可以划分:- 计算片段长度:
end - start + 1,并推入结果数组。 - 更新
start为i + 1,开始下一个片段。
- 计算片段长度:
- 对于每个字符,获取其最后出现位置,并更新
- 返回结果数组。
- 初始化
4.3 前端场景联想
- 此算法类似前端中代码分割(Code Splitting):根据模块依赖关系动态拆分代码块,以优化加载性能。贪心策略帮助找到最小粒度分割点,提升应用效率。
5. 各实现思路的复杂度、优缺点对比表格
| 思路 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| 贪心算法 | O(n) | O(1)(固定26长度数组) | 高效、简洁、最优解 | 假设字符集固定(小写字母) | 大多数情况,尤其是字符集有限 |
| 暴力枚举 | O(2^n) | O(n) | 简单直观,易实现 | 指数级复杂度,不适用于长字符串 | 仅用于教学或极小输入 |
| 动态规划 | O(n^2) 或更高 | O(n^2) | 可处理更复杂约束 | 过度设计,贪心更优 | 当贪心不适用时(但此题贪心有效) |
- 推荐: 贪心算法是此题的标准解法,兼顾性能和可读性。
6. 总结
6.1 通用解题模板
此类划分问题通常遵循贪心模板:
- 预处理: 收集关键信息(如最后出现位置、频率等)。
- 遍历维护: 使用指针或变量维护当前区间状态(如开始和结束)。
- 决策点: 在特定条件(如索引等于结束位置)下进行划分或更新。
- 累积结果: 记录每次划分的输出。
6.2 LeetCode 类似题目
- 56. 合并区间: 给定区间列表,合并重叠区间。类似贪心思路,按起始排序后合并。
- 435. 无重叠区间: 选择不重叠区间的最大数量,贪心按结束时间排序。
- 452. 用最少数量的箭引爆气球: 区间问题,贪心求最小交集点。
- 122. 买卖股票的最佳时机 II: 贪心累积利润,类似片段划分思想。