【算法-LeetCode】227. 基本计算器 II(栈;Array.split();正则)

本文介绍了如何使用JavaScript解决LeetCode的基本计算器II问题,主要通过两种方法实现:一是暴力解法,利用数组的split()和map()函数处理输入字符串,然后通过两次while循环完成运算;二是优化解法,借助栈数据结构,通过遍历处理输入字符串,动态计算结果。虽然暴力解法在处理超长数据时会超时,但优化后的栈解法成功通过了所有测试用例,时间复杂度得到改善。
摘要由CSDN通过智能技术生成

227. 基本计算器 II - 力扣(LeetCode)

发布:2021年9月28日22:46:27

问题描述及示例

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
整数除法仅保留整数部分。

示例 1:
输入:s = “3+2*2”
输出:7

示例 2:
输入:s = " 3/2 "
输出:1

示例 3:
输入:s = " 3+5 / 2 "
输出:5

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/basic-calculator-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

提示:
1 <= s.length <= 3 * 105
s 由整数和算符 (’+’, ‘-’, ‘*’, ‘/’) 组成,中间由一些空格隔开
s 表示一个 有效表达式
表达式中的所有整数都是非负整数,且在范围 [0, 231 - 1] 内
题目数据保证答案是一个 32-bit 整数

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/basic-calculator-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

我的题解

成功前的尝试(暴力解法)

首先是利用 Array.split() 函数和 map() 函数来生成一个数组类型的对象 splitedStr,该数组中存储了经过修剪整理的 s 数据。大体的效果如下图所示:

在这里插入图片描述

【图1】split和map的效果演示

在这里插入图片描述

【图2】利用 `Array.split()` 函数的上述特性

注意,如果某个数字字符串的前后有空格,parseInt() 函数也能正确地将其转换为有效的整数。如下图所示:
在这里插入图片描述

接下来的第一个 while 循环就是执行 splitedStr 中的所有的 */ 运算。

第二个 while 循环则是完成剩下的所有 +- 运算,最后 splitedStr 中将只剩下一个元素,该元素即为 s 表达式所得到的结果,将其返回即可。

/**
 * @param {string} s
 * @return {number}
 */
var calculate = function(s) {
  // 生成一个过滤后的数组,该数组中的元素大致就是上面图1的结构
  let splitedStr = s.split(/(\+|-|\*|\/)/).map(
    cur => isNaN(parseInt(cur)) ? cur : parseInt(cur)
  );
  // temp是临时变量,用于存储某两个数字直接的运算结果
  let temp = 0;
  // index 用于第一个while循环中标识当前遍历元素的下标
  let index = 0;
  // 第一个while循环用于完成 splitedStr 中所有乘法或除法的运算
  while(index < splitedStr.length) {
    // 如果当前元素是乘或除,则将该元素的前驱元素和后继元素先做相应的运算,
    // 并将结果覆盖这三个元素,注意这里用到了 splice 函数,所以数组的长度会改变
    // 所以下方有一个 index-- 的操作以防止数组长度塌陷
    if(["*", "/"].includes(splitedStr[index])) {
      temp = splitedStr[index] === "*" ? splitedStr[index-1] * splitedStr[index+1] : parseInt(splitedStr[index-1] / splitedStr[index+1]);
      splitedStr.splice(index-1, 3, temp);
      // index-- 用于防止出现数组长度塌陷
      index--;
    }
    index++;
  }
  // 第二个while循环用于完成 splitedStr 中所有加法或减法的运算
  while(splitedStr.length > 1) {
    // 注意由于此时 splitedStr 中只剩下加法和减法,所以直接针对前三位进行操作即可
    temp = splitedStr[1] === "+" ? splitedStr[0] + splitedStr[2] : splitedStr[0] - splitedStr[2];
    splitedStr.splice(0, 3, temp);
  }
  // 最终结果都将累积到一个元素身上,将其返回即可
  return splitedStr[0];
};


提交记录
108 / 109 个通过测试用例
执行结果:超出时间限制
最后执行的输入:"1+7-7+3+3+6-3+1-3...6-9-9+0+6+4+2+7+1-4-6-6-0+6+3-7"
时间:2021/09/28 22:51

在这里插入图片描述

遇到超长数据时,运行超时

如果不计算创建 splitedStr 时的那次遍历,那么在最坏的情况下(即:s 中所有的运算符都是 +-),上面的程序将会完整地遍历 splitedStr 两次,而且上面的 splice() 函数是直接操作 splitedStr,所以会移动大量的元素,这就导致当遇到非常长的 s 时,程序的时间花费非常大。

我的题解1(栈)

由于上面的程序性能太差,没法儿通过提交,所以我想到了改进的方法:用栈来累积存储 splitedStr 中的计算结果。

其实一开始我想到的就是用栈结构来做辅助,但是用的是两个栈:用 numbers 栈用于存储 s 中的数据,而用 operators 栈来存储运算符。但是后来发现这种解法会遇到大量的判断情况从而导致程序非常臃肿,所以就中途舍弃了,然后转而想出了上面那种超时的做法,所以我现在也不确定利用双栈的做法是否可行。

首先和上面一样,先生成 splitedStr 数组,然后逐位遍历 splitedStr 中的元素。

  • 如果当前元素是数字,则直接压入 result 栈中;
  • 如果当前元素是运算符 */ 中的一个,则将 result 的栈顶元素 resultTop 弹出,然后用栈顶元素 resultTop 和当前遍历元素的下一个元素 splitedStr[i+1](这个元素一定是数字)做相应的 */ 运算,然后将运算结果作为新的栈顶元素压入 result 栈;
  • 如果当前元素是运算符 +- 中的一个,先将当前元素压入 result 栈中,然后再判断:
    • 如果此时 result 栈中没有 +- 运算符,那么就继续往下遍历;
    • 如果此时 result 栈中已经有了一个 +- 运算符,那么就要将 result 栈中存储的前三位(此时这前三位一定是 [数字, 加或减运算符, 数字]的顺序)进行一次运算,并且把运算结果覆盖掉这前三位。

      我是通过 count 这个专门的计数变量来判断 result 栈中是否已经有了一个 +- 运算符的,当然还可以通过 indexOf()includes() 等函数来判断。

需要特别注意到的是,因为 result 栈中的结果会动态累积,所以其栈长度最大为 4

/**
 * @param {string} s
 * @return {number}
 */
var calculate = function(s) {
  // splitedStr的作用和上面一样
  let splitedStr = s.split(/(\+|-|\*|\/)/).map(
    cur => isNaN(parseInt(cur)) ? cur : parseInt(cur)
  );
  // result用于存储累积的结果和运算符,其长度最大为4
  let result = [];
  // resultTop用于存储result的栈顶的有效累积数字和
  let resultTop = 0;
  // count用于记录当前result栈中有几个加号或减号
  let count = 0;
  // 开始遍历 splitedStr
  for(let i = 0; i < splitedStr.length; i++) {
    // 如果当前元素是数字,直接压入result栈即可,并结束本次循环
    if(typeof splitedStr[i] === 'number') {
      result.push(splitedStr[i]);
      continue;
    }
    // 如果当前元素是乘号或除号,
    if(["*", "/"].includes(splitedStr[i])) {
      // 则取出result栈顶元素,
      resultTop = result.pop();
      // 并与后一个数字进行相应的乘或除运算
      resultTop = splitedStr[i] === "*" ? resultTop * splitedStr[i+1] : parseInt(resultTop / splitedStr[i+1]);
      // 并将重新计算的结果作为栈顶元素压入result
      result.push(resultTop);
      // 同时将i指针往后移动一位,因为上面的操作相当于已经遍历了当前元素的后一位
      i++;
      // 结束本次循环
      continue;
    }
    // 如果当前元素既不是数字,也不是乘号或除号,则一定是加号或减号,将其压入result栈
    result.push(splitedStr[i]);
    // 此时计数器加一,表示当前result栈中的加号或减号数量加了1
    count++;
    // 如果此时count累加到了2,则说明此前result栈中已经有了一个加号或减号
    if(count === 2) {
      // 将result中的前三位进行相应的运算
      resultTop = result[1] === "+" ? result[0] + result[2] : result[0] - result[2];
      // 将上面获得的结果覆盖掉result的前三位
      result.splice(0, 3, resultTop);
      // 此时result中只有一个加号或减号
      count = 1;
    }
  }
  // 最后这里和上面的有点不同,因为这种解法下,result最后可能会有两种情况:
  // 1、result中只有一个元素,则该元素即为 s 表达式的最终值(所有运算的累积值)
  if(result.length === 1) {
    return result[0];
  }
  // 2、result中有三个元素,分别是[数字, 加或减运算符, 数字],此时还要将其整合为最后结果
  return result[1] === "+" ? result[0] + result[2] : result[0] - result[2];
};


提交记录
109 / 109 个通过测试用例
状态:通过
执行用时:104 ms, 在所有 JavaScript 提交中击败了24.08%的用户
内存消耗:49.5 MB, 在所有 JavaScript 提交中击败了5.14%的用户
时间:2021/09/29 00:21

折腾了好久,总算是通过了,但是空间表现似乎不怎么好……

官方题解

更新:2021年7月29日18:43:21

因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。

更新:2021年9月28日15:03:57

参考:两数相加 - 两数相加 - 力扣(LeetCode)

【更新结束】

有关参考

更新:2021年9月28日22:54:13
参考:String.prototype.split() - JavaScript | MDN
参考:Array.prototype.map() - JavaScript | MDN

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值