「前端刷题」241.为运算表达式设计优先级(MEDIUM)

49 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情

题目(Different Ways to Add Parentheses)

链接:https://leetcode-cn.com/problems/different-ways-to-add-parentheses
解决数:662
通过率:75.7%
标签:递归 记忆化搜索 数学 字符串 动态规划 
相关公司:google amazon adobe 

给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。

生成的测试用例满足其对应输出值符合 32 位整数范围,不同结果的数量不超过 104 。

 

示例 1:

输入: expression = "2-1-1"
输出: [0,2]
解释:
((2-1)-1) = 0 
(2-(1-1)) = 2

示例 2:

输入: expression = "2*3-4*5"
输出: [-34,-14,-10,-10,10]
解释:
(2*(3-(4*5))) = -34 
((2*3)-(4*5)) = -14 
((2*(3-4))*5) = -10 
(2*((3-4)*5)) = -10 
(((2*3)-4)*5) = 10

 

提示:

  • 1 <= expression.length <= 20
  • expression 由数字和算符 '+''-' 和 '*' 组成。
  • 输入表达式中的所有整数值在范围 [0, 99] 

思路

  • 分治算法思想:将原问题分解成小规模的子问题,然后根据子问题的结果构造出原问题的答案(大家了解的归并排序其实就是典型的使用分治思想)
  • 简单说一下题目的意思:给你输入一个算式,你可以给它随意加括号,请你穷举出所有可能的加括号方式,并计算出对应的结果
  • 举例来说:现在给出的算式是1 + 2 * 3 - 4 * 5,这个算式有几种加括号的方式?
  • 因为括号可以嵌套,所以可能一时半会很难答出来,如果不让括号嵌套(即只加一层括号),有几种加括号的方式?
  • 显然我们有四种加括号方式:
  1. (1) + (2 * 3 - 4 * 5)
  2. (1 + 2) * (3 - 4 * 5)
  3. (1 + 2 * 3) - (4 * 5)
  4. (1 + 2 * 3 - 4) * (5)
  • 其实就是按照运算符进行分割,给每个运算符的左右两部分加括号
  • 现在再细节说下上面的第3种情况:(1 + 2 * 3) - (4 * 5)
  • 我们用减号-作为分隔,把原算式分解成两个算式1 + 2 * 3和4 * 5
  • 分治分治,分而治之,这一步就是把原问题进行了「分」,我们现在要开始「治」了
  • 1 + 2 * 3可以有两种加括号的方式,分别是:
  1. (1) + (2 * 3) = 7
  2. (1 + 2) * (3) = 9
  • 我们可以假定表述成1 + 2 * 3 = [9, 7]
  • 4 * 5当然只有一种加括号方式,也就是4 * 5 = [20]
  • 通过上述结果推导出(1 + 2 * 3) - (4 * 5)有几种加括号方式,或者说有几种不同的结果
  • 可以推导出来(1 + 2 * 3) - (4 * 5)有两种结果,分别是:
  1. 9 - 20 = -11
  2. 7 - 20 = -13
  • 总结一下:先「分」后「治」,先按照运算符将原问题拆解成多个子问题,然后通过子问题的结果来合成原问题的结果
  • 代码实现中加了备忘录来优化重复计算

代码

/**
 * @param {string} expression
 * @return {number[]}
 */
let memo = new Map();
var diffWaysToCompute = function (expression) {
  // 避免重复计算
  if (memo.has(expression)) {
    return memo.get(expression);
  }
  let res = [];
  for (let i = 0; i < expression.length; i++) {
    let c = expression.charAt(i);
    // 扫描算式 expression 中的运算符
    if (c == "*" || c == "+" || c == "-") {
      /****** 分 ******/
      let left = diffWaysToCompute(expression.substring(0, i));
      let right = diffWaysToCompute(expression.substring(i + 1));
      /****** 治 ******/
      // 通过子问题的结果,合成原问题的结果
      for (let a of left) {
        for (let b of right) {
          switch (c) {
            case "*":
              res.push(a * b);
              break;
            case "+":
              res.push(a + b);
              break;
            case "-":
              res.push(a - b);
              break;
          }
        }
      }
    }
  }
  // base case,递归函数必须有个 base case 用来结束递归,其实这段代码就是我们分治算法的 base case,代表着你「分」到什么时候可以开始「治」
  // 如果 res 为空,说明算式是一个数字,没有运算符(因为当算式中不存在运算符的时候,就不会触发 if 语句,也就不会给res中添加任何元素)
  if (!res.length) {
    res.push(parseInt(expression));
  }
  // 将结果添加进备忘录
  memo.set(expression, res);
  return res;
};