Appearance
实战:为运算表达式设计优先级
LeetCode 241. 为运算表达式设计优先级 | 难度:中等
分治思想在表达式求值中的巧妙应用:以每个运算符为分界点。
问题描述
给定一个含有数字和运算符的字符串,为表达式添加括号,返回所有可能的计算结果。
示例:
输入:"2-1-1"
输出:[0, 2]
解释:
((2-1)-1) = 0
(2-(1-1)) = 2分治思路
遇到运算符时,将表达式分为左右两部分:
- 左部分的所有可能结果
- 右部分的所有可能结果
- 用运算符组合所有结果
"2*3-4*5"
↓ 遇到 *
左: "2" → [2]
右: "3-4*5" → [3-4*5的所有结果]
组合: 2 * 右边每个结果代码实现
typescript
function diffWaysToCompute(expression: string): number[] {
const result: number[] = [];
// 遍历每个字符
for (let i = 0; i < expression.length; i++) {
const char = expression[i];
// 如果是运算符
if (char === '+' || char === '-' || char === '*') {
// 分治:左右两部分
const left = diffWaysToCompute(expression.slice(0, i));
const right = diffWaysToCompute(expression.slice(i + 1));
// 组合所有可能的结果
for (const l of left) {
for (const r of right) {
if (char === '+') result.push(l + r);
else if (char === '-') result.push(l - r);
else if (char === '*') result.push(l * r);
}
}
}
}
// 基础情况:纯数字
if (result.length === 0) {
result.push(parseInt(expression));
}
return result;
}执行过程详解
以 "2-1-1" 为例:
第一次分割(第一个 -):
左: "2" → [2]
右: "1-1"
第二次分割:
左: "1" → [1]
右: "1" → [1]
组合: 1-1 = 0
→ [0]
组合: 2-0 = 2第二次分割(第二个 -):
左: "2-1"
第一次分割:
左: "2" → [2]
右: "1" → [1]
组合: 2-1 = 1
→ [1]
右: "1" → [1]
组合: 1-1 = 0最终结果:[2, 0]
记忆化优化
typescript
function diffWaysToCompute(expression: string): number[] {
const memo = new Map<string, number[]>();
function compute(expr: string): number[] {
if (memo.has(expr)) {
return memo.get(expr)!;
}
const result: number[] = [];
for (let i = 0; i < expr.length; i++) {
const char = expr[i];
if (char === '+' || char === '-' || char === '*') {
const left = compute(expr.slice(0, i));
const right = compute(expr.slice(i + 1));
for (const l of left) {
for (const r of right) {
if (char === '+') result.push(l + r);
else if (char === '-') result.push(l - r);
else if (char === '*') result.push(l * r);
}
}
}
}
if (result.length === 0) {
result.push(parseInt(expr));
}
memo.set(expr, result);
return result;
}
return compute(expression);
}复杂度分析
时间复杂度:O(4ⁿ / √n)
- 这是卡特兰数级别
- 因为不同的括号组合数是卡特兰数
- n 是表达式中运算符的数量
空间复杂度:O(4ⁿ / √n)
- 存储所有可能的结果
- 递归栈深度为 O(n)
分治的本质理解
这道题的分治思想可以用决策树来理解:
表达式:"2*3-4*5"
"2*3-4*5"
/ | \
分割* 分割- 分割*
| | |
"2" * "3-4*5" ...
|
/ | \
分割- ...
|
"3" - "4*5"
|
"4" * "5"每个运算符都可以成为"最后一个计算的":
- 选择
*作为最外层:2 * (3-4*5) - 选择
-作为最外层:(2*3) - (4*5) - 选择第二个
*:(2*3-4) * 5
预处理优化
可以先解析表达式,提取数字和运算符,避免重复切分字符串:
typescript
function diffWaysToCompute(expression: string): number[] {
// 预处理:提取数字和运算符
const nums: number[] = [];
const ops: string[] = [];
let num = 0;
for (const char of expression) {
if (char === '+' || char === '-' || char === '*') {
nums.push(num);
ops.push(char);
num = 0;
} else {
num = num * 10 + parseInt(char);
}
}
nums.push(num); // 最后一个数字
const memo = new Map<string, number[]>();
function compute(lo: number, hi: number): number[] {
const key = `${lo},${hi}`;
if (memo.has(key)) return memo.get(key)!;
const result: number[] = [];
if (lo === hi) {
result.push(nums[lo]);
} else {
// 枚举分割点(运算符位置)
for (let i = lo; i < hi; i++) {
const left = compute(lo, i);
const right = compute(i + 1, hi);
for (const l of left) {
for (const r of right) {
if (ops[i] === '+') result.push(l + r);
else if (ops[i] === '-') result.push(l - r);
else result.push(l * r);
}
}
}
}
memo.set(key, result);
return result;
}
return compute(0, nums.length - 1);
}优化效果:
- 避免了字符串切分和重复解析
- 使用数字索引而非字符串作为缓存键
- 更高效的内存使用
常见错误
错误1:忘记基础情况
typescript
// ❌ 错误:纯数字时result为空,没有返回任何值
for (let i = 0; i < expression.length; i++) {
if (isOperator(expression[i])) {
// ...
}
}
return result; // 纯数字时为空!
// ✅ 正确:显式处理纯数字情况
if (result.length === 0) {
result.push(parseInt(expression));
}错误2:运算符判断遗漏
typescript
// ❌ 容易遗漏某个运算符
if (char === '+' || char === '-') { // 忘了 '*'
// ✅ 完整判断
if (char === '+' || char === '-' || char === '*') {错误3:多位数解析错误
typescript
// ❌ 错误:只能处理个位数
const num = parseInt(char); // "12" 会被拆成 1 和 2
// ✅ 正确:使用slice提取完整子串
parseInt(expression.slice(0, i));与其他题目的联系
相似题目:
- LeetCode 95. 不同的二叉搜索树 II:枚举根节点作为分割点
- LeetCode 96. 不同的二叉搜索树:只需计数,不用生成
共同模式:
for 每个可能的分割点:
左部分所有可能 = 递归(左边)
右部分所有可能 = 递归(右边)
组合所有 (左, 右) 对结果数量:都是卡特兰数级别 C(n) = C(2n, n) / (n+1)
关键要点
- 分治策略:以每个运算符为分割点
- 笛卡尔积组合:左右子问题结果的所有组合
- 基础情况:纯数字字符串直接返回
- 记忆化优化:缓存相同子表达式的结果
- 预处理技巧:提取数字和运算符,避免重复解析
- 卡特兰数:时间复杂度的数学本质