前言
最近在刷 LeetCode 的 227. 基本计算器 II 这道题时,我遇到了一个有趣的问题:我的解法逻辑完全正确,但在提交时却因为超时无法通过。经过一番排查,我发现问题出在 JavaScript 数组操作方法的性能差异上,特别是 shift/unshift 和 push/pop 之间的区别。这篇文章将记录我的探索过程,希望能帮助其他开发者避免类似的性能陷阱。
问题描述
题目要求实现一个计算器,能够处理包含加减乘除的字符串表达式,并遵守运算符优先级规则。例如:
- 输入:"3+2*2",输出:7
- 输入:" 3/2 ",输出:1
- 输入:" 3+5 / 2 ",输出:5
初始解法:使用队列和正则表达式
第一个解法使用了正则表达式来解析字符串,并用两个数组分别存储数字和运算符:
- 如果不是数字,则是操作符,直接压入操作符栈顶
- 如果是数字,在将数字压入栈之前,判断如果操作符栈顶是
*或者/则优先计算,然后将计算结果压入栈顶。 - 最后栈中剩余的则全是加法或者减法,而没有运算符优先级问题了,一次遍历就可以求出结果
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 上提交时会超时。为什么?
性能问题分析
问题出在 shift 和 unshift 操作上。在 JavaScript 中:
push和pop操作的时间复杂度是 O(1)shift和unshift操作的时间复杂度是 O(n)
这是因为 shift/unshift 操作需要移动数组中的所有元素。对于大型数组,这会带来显著的性能开销。
在上述解法中,while (ops.length) 循环里使用了 shift 和 unshift,当输入字符串很长时,这些操作就会成为性能瓶颈。
优化方案:改用 push pop 操作
既然 shift/unshift 是性能问题所在,则可以改用 push/pop 操作来优化尝试。具体做法是:
- 在处理完乘除后,反转数组
- 使用
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 的所有测试用例。
总结
- 避免使用
shift/unshift:在处理大型数组时,这些操作会成为性能瓶颈 - 优先使用
push/pop:这些操作的时间复杂度是常数级
这个经历让我深刻认识到,即使是看似简单的数组操作方法,也可能对程序性能产生重大影响。在日常开发中,我们应该养成关注底层操作性能的习惯,特别是在处理大数据量时。
思考题
- 在你的项目中,是否也遇到过因为数组操作导致的性能问题?
- 除了
shift/unshift,你还知道哪些JavaScript操作有隐藏的性能陷阱? - 在处理大量数据时,你会选择哪些数据结构来优化性能?
欢迎在评论区分享你的经验和想法!