从 LeetCode 227 题看 JavaScript 数组操作的性能陷阱

108 阅读3分钟

前言

最近在刷 LeetCode 的 227. 基本计算器 II 这道题时,我遇到了一个有趣的问题:我的解法逻辑完全正确,但在提交时却因为超时无法通过。经过一番排查,我发现问题出在 JavaScript 数组操作方法的性能差异上,特别是 shift/unshiftpush/pop 之间的区别。这篇文章将记录我的探索过程,希望能帮助其他开发者避免类似的性能陷阱。

问题描述

题目要求实现一个计算器,能够处理包含加减乘除的字符串表达式,并遵守运算符优先级规则。例如:

  • 输入:"3+2*2",输出:7
  • 输入:" 3/2 ",输出:1
  • 输入:" 3+5 / 2 ",输出:5

初始解法:使用队列和正则表达式

第一个解法使用了正则表达式来解析字符串,并用两个数组分别存储数字和运算符:

  1. 如果不是数字,则是操作符,直接压入操作符栈顶
  2. 如果是数字,在将数字压入栈之前,判断如果操作符栈顶是*或者/则优先计算,然后将计算结果压入栈顶。
  3. 最后栈中剩余的则全是加法或者减法,而没有运算符优先级问题了,一次遍历就可以求出结果
var calculate = function (s) {
  const queue = [];
  const ops = [];
  const regex = /(\d+)|(\+)|(\-)|(\*)|(\/)/g;
  let match;
  while ((match = regex.exec(s))) {
    switch (match[0]) {
      case "+":
      case "-":
      case "*":
      case "/":
        ops.push(match[0]);
        break;
      default:
        const op = ops.at(-1);
        if (op === "*") {
          const num = queue.pop() * match[0];
          queue.push(num);
          ops.pop();
        } else if (op === "/") {
          const num = Math.floor(queue.pop() / match[0]);
          queue.push(num);
          ops.pop();
        } else {
          queue.push(match[0]);
        }
        break;
    }
  }

  while (ops.length) {
    const num1 = queue.shift();
    const num2 = queue.shift();
    const op = ops.shift();
    const num = op === "+" ? num1 / 1 + num2 / 1 : num1 / 1 - num2 / 1;
    queue.unshift(num);
  }

  return Number(queue[0]);
};

这个解法在逻辑上是正确的,但在 LeetCode 上提交时会超时。为什么?

性能问题分析

问题出在 shiftunshift 操作上。在 JavaScript 中:

  • pushpop 操作的时间复杂度是 O(1)
  • shiftunshift 操作的时间复杂度是 O(n)

这是因为 shift/unshift 操作需要移动数组中的所有元素。对于大型数组,这会带来显著的性能开销。

在上述解法中,while (ops.length) 循环里使用了 shiftunshift,当输入字符串很长时,这些操作就会成为性能瓶颈。

优化方案:改用 push pop 操作

既然 shift/unshift 是性能问题所在,则可以改用 push/pop 操作来优化尝试。具体做法是:

  1. 在处理完乘除后,反转数组
  2. 使用 push/pop 代替 shift/unshift

优化后的代码如下:

var calculate = function (s) {
  const queue = [];
  const ops = [];
  const regex = /(\d+)|(\+)|(\\-)|(\*)|(\/)/g;
  let match;
  while ((match = regex.exec(s))) {
    switch (match[0]) {
      case "+":
      case "-":
      case "*":
      case "/":
        ops.push(match[0]);
        break;
      default:
        const op = ops.at(-1);
        if (op === "*") {
          const num = queue.pop() * match[0];
          queue.push(num);
          ops.pop();
        } else if (op === "/") {
          const num = Math.floor(queue.pop() / match[0]);
          queue.push(num);
          ops.pop();
        } else {
          queue.push(match[0]);
        }
        break;
    }
  }

  // 优化:反转数组后使用 push/pop
  ops.reverse();
  queue.reverse();
  while (ops.length) {
    const num1 = queue.pop();
    const num2 = queue.pop();
    const op = ops.pop();
    const num = op === "+" ? num1 / 1 + num2 / 1 : num1 / 1 - num2 / 1;
    queue.push(num);
  }

  return Number(queue[0]);
};

这个优化将时间复杂度从 O(n²) 降到了 O(n),成功通过了 LeetCode 的所有测试用例。

总结

  1. 避免使用 shift/unshift:在处理大型数组时,这些操作会成为性能瓶颈
  2. 优先使用 push/pop:这些操作的时间复杂度是常数级

这个经历让我深刻认识到,即使是看似简单的数组操作方法,也可能对程序性能产生重大影响。在日常开发中,我们应该养成关注底层操作性能的习惯,特别是在处理大数据量时。

思考题

  1. 在你的项目中,是否也遇到过因为数组操作导致的性能问题?
  2. 除了 shift/unshift,你还知道哪些JavaScript操作有隐藏的性能陷阱?
  3. 在处理大量数据时,你会选择哪些数据结构来优化性能?

欢迎在评论区分享你的经验和想法!