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

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

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

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

题目描述

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

注意:

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

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

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

题目核心难点

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

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

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

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

二、核心解题思路

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

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

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

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

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

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

三、完整代码实现(TypeScript)

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

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. 初始化变量(关键变量说明)

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

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

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

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

场景1:解析多位数(字符是 0-9)
typescript 复制代码
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:处理左括号 '('
typescript 复制代码
if (c === '(') {
  stack.push(result); // 保存括号外的结果
  stack.push(sign === '+' ? 1 : -1); // 保存括号前的符号(用1/-1代替+/-)
  // 重置状态,开始计算括号内的子表达式
  result = 0;
  sign = '+';
}

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

  1. 此时 result = 0(括号外还没计算),sign = '+'(括号前是正号);

  2. 把 result(0)压入栈,再把 sign 转换成 1('+' 对应 1,'-' 对应 -1)压入栈;

  3. 重置 result = 0、sign = '+',开始专注计算括号内的 "1 + 2"。

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

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

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

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

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

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

typescript 复制代码
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,支持乘除运算)。

相关推荐
飘尘6 分钟前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆15 分钟前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师1 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆1 小时前
VSCode自动格式化三要素
前端
爱勇宝2 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen3 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518135 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode5 小时前
Redis 在生产项目的使用
前端·后端
LiaCode5 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战5 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github