云计算百科
云计算领域专业知识百科平台

LeetCode 224. 基本计算器:手写实现加减+括号运算

LeetCode 上的经典栈应用题——224. 基本计算器,这道题的核心是实现一个支持 加减运算、括号、空格 的简易计算器,并且明确禁止使用 eval() 等内置表达式计算函数,完全需要我们手动解析字符串、处理运算逻辑。

很多同学遇到这道题会头疼,尤其是括号带来的运算优先级问题,今天就结合完整可运行的代码,从题目理解到代码拆解,一步步讲清楚每一步的逻辑,新手也能轻松看懂!

一、题目回顾(LeetCode 224. 基本计算器)

题目描述

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

注意:

  • 表达式 s 只包含数字、‘+’、‘-’、‘(’、‘)’ 和空格 ’ ';

  • 不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval();

  • 示例:输入 “(1+(4+5+2)-3)+(6+8)”,输出 23;输入 " 2-1 + 2 ",输出 3。

题目核心难点

这道题的难点不在于加减运算本身,而在于两个点:

  • 括号的处理:括号会改变运算优先级,需要先计算括号内的子表达式,再将结果与括号外的内容合并;

  • 多位数的解析:字符串中的数字可能是个位数(如 “1”),也可能是多位数(如 “123”),需要正确拼接;

  • 空格的过滤:空格不影响计算,需要跳过不处理。

  • 二、核心解题思路

    解决这道题的最优思路是使用 栈(Stack) 来保存计算过程中的状态,核心逻辑围绕「括号优先级」和「符号处理」展开:

  • 用栈保存括号外的计算状态:遇到左括号 ‘(’ 时,将当前已经计算出的结果、括号前的符号保存到栈中,然后重置状态,专门计算括号内的子表达式;

  • 用变量维护当前计算状态:用 result 记录当前层级(括号内/外)的计算结果,用 sign 记录当前数字的符号(默认是 ‘+’,因为表达式第一个数字隐含正号),用 num 临时拼接多位数;

  • 遇到右括号 ‘)’ 时,弹出栈中保存的状态(括号外的结果和括号前的符号),将括号内的计算结果与括号外的结果合并,继续后续计算;

  • 空格直接跳过,不影响任何计算逻辑。

  • 简单来说:栈的作用就是「暂存括号外的上下文」,让我们可以专注于括号内的计算,计算完成后再回退到之前的上下文继续运算。

    三、完整代码实现(TypeScript)

    先上完整可运行的代码,后面逐行拆解每一步逻辑,确保每一行代码都讲明白:

    function calculate(s: string): number {
    const stack: number[] = [];
    let num = 0;
    let sign = '+'; // 当前数字的符号(+/-)
    let result = 0; // 当前层级(括号内/外)的计算结果

    for (let i = 0; i < s.length; i++) {
    const c = s[i];

    // 1. 解析多位数(比如"123" -> 123)
    if (c >= '0' && c<= '9') {
    num = num * 10 + (c.charCodeAt(0) '0'.charCodeAt(0));
    }

    // 2. 处理左括号:保存当前状态(结果、符号)到栈,重置状态计算括号内的值
    if (c === '(') {
    stack.push(result); // 保存括号外的结果
    stack.push(sign === '+' ? 1 : 1); // 保存括号前的符号(用1/-1代替+/-更方便)
    // 重置状态,开始计算括号内的子表达式
    result = 0;
    sign = '+';
    }

    // 3. 处理运算符(+/-)或右括号:结算当前数字
    if ((c === '+' || c === '-') || i === s.length 1 || c === ')') {
    // 根据当前符号,把数字加到结果中
    result += sign === '+' ? num : num;
    num = 0; // 重置临时数字

    // 更新符号(仅当是+/-时)
    if (c === '+' || c === '-') {
    sign = c;
    }

    // 4. 处理右括号:弹出栈中保存的状态,合并结果
    if (c === ')') {
    const prevSign = stack.pop()!; // 括号前的符号(1/-1)
    const prevResult = stack.pop()!; // 括号外的结果
    result = prevResult + prevSign * result; // 合并括号内和括号外的结果
    }
    }

    // 空格直接跳过,无需处理
    }

    return result;
    }

    四、代码逐行拆解(核心逻辑精讲)

    我们按「初始化变量 → 遍历字符串 → 各场景处理 → 最终返回结果」的顺序,逐块拆解代码,重点讲清楚栈的用法和括号处理逻辑。

    1. 初始化变量(关键变量说明)

    const stack: number[] = []; // 栈:保存括号外的计算状态(结果+符号)
    let num = 0; // 临时变量:拼接多位数(如"123",先算1,再12,最后123)
    let sign = '+'; // 符号变量:记录当前数字的符号(默认+,因为第一个数字隐含正号)
    let result = 0; // 结果变量:记录当前层级(括号内/外)的计算结果

    这里有个小细节:sign 记录的是「当前数字的符号」,而不是「上一个运算符」,这样处理能更方便地对接多位数解析和括号逻辑,后面会看到具体作用。

    2. 遍历字符串(核心循环)

    循环的核心是「逐个处理字符」,根据字符的类型(数字、左括号、右括号、运算符、空格),执行不同的逻辑,我们逐个场景分析:

    场景1:解析多位数(字符是 0-9)

    if (c >= '0' && c <= '9') {
    num = num * 10 + (c.charCodeAt(0) '0'.charCodeAt(0));
    }

    这行代码是「多位数拼接」的关键,举个例子:解析 “123” 时:

    • 遇到 ‘1’:num = 0 * 10 + (49 – 48) = 1(字符 ‘0’ 的 ASCII 码是 48,‘1’ 是 49);

    • 遇到 ‘2’:num = 1 * 10 + (50 – 48) = 12;

    • 遇到 ‘3’:num = 12 * 10 + (51 – 48) = 123;

    这样就完成了多位数的正确拼接,避免把 “123” 解析成 1、2、3 三个单独的数字。

    场景2:处理左括号 ‘(’

    if (c === '(') {
    stack.push(result); // 保存括号外的结果
    stack.push(sign === '+' ? 1 : 1); // 保存括号前的符号(用1/-1代替+/-)
    // 重置状态,开始计算括号内的子表达式
    result = 0;
    sign = '+';
    }

    这是括号处理的核心步骤之一,目的是「暂存括号外的上下文」,举个例子:当遇到表达式 “(1 + 2) – 3” 中的 ‘(’ 时:

  • 此时 result = 0(括号外还没计算),sign = ‘+’(括号前是正号);

  • 把 result(0)压入栈,再把 sign 转换成 1(‘+’ 对应 1,‘-’ 对应 -1)压入栈;

  • 重置 result = 0、sign = ‘+’,开始专注计算括号内的 “1 + 2”。

  • 为什么用 1/-1 代替 ‘+’/‘-’?因为后续合并括号内外结果时,直接用「括号外结果 + 括号前符号 × 括号内结果」就能快速计算,无需再判断符号字符串,更简洁。

    场景3:处理运算符(+/-)、右括号 ‘)’ 或字符串末尾

    这部分是「结算当前数字」的核心,当遇到以下三种情况时,说明当前数字已经解析完成,需要把它加到 result 中:

    • 遇到运算符 ‘+’ 或 ‘-’(下一个数字要开始解析,当前数字需要结算);

    • 遇到右括号 ‘)’(括号内的数字解析完成,需要结算后和括号外合并);

    • 遍历到字符串末尾(最后一个数字没有后续运算符,需要结算)。

    if ((c === '+' || c === '-') || i === s.length 1 || c === ')') {
    // 步骤1:根据当前符号,把数字加到结果中
    result += sign === '+' ? num : num;
    num = 0; // 重置临时数字,准备解析下一个数

    // 步骤2:更新符号(仅当当前字符是+/-时)
    if (c === '+' || c === '-') {
    sign = c;
    }

    // 步骤3:处理右括号,合并括号内外结果
    if (c === ')') {
    const prevSign = stack.pop()!; // 弹出括号前的符号(1/-1)
    const prevResult = stack.pop()!; // 弹出括号外的结果
    result = prevResult + prevSign * result; // 合并结果
    }
    }

    我们分步骤拆解这部分逻辑,还是用例子辅助理解:

    例子1:解析 “1 + 2” 时,遇到 ‘+’ 运算符:

    • 此时 num = 1(已经解析完第一个数字),sign = ‘+’;

    • result += 1 → result = 1;

    • num 重置为 0,sign 更新为 ‘+’(当前运算符);

    • 继续解析下一个数字 2,后续遇到字符串末尾时,再把 2 加到 result 中,最终 result = 3。

    例子2:解析 “(1 + 2) – 3” 时,遇到 ‘)’ 右括号:

    • 括号内已经解析完 “1 + 2”,此时 result = 3,num = 0;

    • 弹出栈顶的 prevSign = 1(括号前是正号),再弹出 prevResult = 0(括号外的结果);

    • 合并结果:result = 0 + 1 × 3 = 3;

    • 继续解析后续的 ‘-’ 和 3,最终 result = 3 – 3 = 0。

    场景4:处理空格(直接跳过)

    代码中没有专门写空格的处理逻辑,因为当 c 是空格时,不满足任何一个 if 条件,会直接进入下一次循环,相当于「自动跳过」,逻辑简洁高效。

    3. 最终返回结果

    循环结束后,result 中就保存了整个表达式的计算结果,直接返回即可:return result;

    五、总结与优化思考

    1. 核心知识点回顾

    这道题的核心是「栈的应用」,用栈暂存括号外的上下文,解决括号优先级问题,同时用三个变量(num、sign、result)维护当前计算状态,高效解析多位数和符号。

    关键亮点:

    • 用 1/-1 代替 ‘+’/‘-’ 符号,简化括号内外结果的合并逻辑;

    • 一次遍历完成所有解析和计算,时间复杂度 O(n)(n 是字符串长度);

    • 栈的空间复杂度最坏 O(n)(嵌套括号层数最多为 n/2),属于最优解法。

    2. 优化方向(可选)

    如果想进一步优化代码,可以考虑:

    • 用正则表达式先过滤掉所有空格,减少循环中的判断(但会增加一次正则遍历,整体效率影响不大);

    • 对于嵌套括号极深的场景,栈的空间开销无法避免,这是该思路的固有特性,也是最优选择。

    3. 刷题启示

    遇到「优先级处理」「上下文暂存」类的算法题,优先考虑栈这种数据结构(比如括号匹配、表达式求值等)。这道题虽然是中等难度,但覆盖了栈的核心用法、多位数解析、符号处理等多个细节,吃透这道题,能轻松应对同类的表达式求值问题(如 LeetCode 227. 基本计算器 II,支持乘除运算)。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » LeetCode 224. 基本计算器:手写实现加减+括号运算
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!