LeetCode题练习与总结:给表达式添加运算符--282

299 篇文章 0 订阅
57 篇文章 0 订阅

一、题目描述

给定一个仅包含数字 0-9 的字符串 num 和一个目标值整数 target ,在 num 的数字之间添加 二元 运算符(不是一元)+- 或 * ,返回 所有 能够得到 target 的表达式。

注意,返回表达式中的操作数 不应该 包含前导零。

示例 1:

输入: num = "123", target = 6
输出: ["1+2+3", "1*2*3"] 
解释: “1*2*3” 和 “1+2+3” 的值都是6。

示例 2:

输入: num = "232", target = 8
输出: ["2*3+2", "2+3*2"]
解释: “2*3+2” 和 “2+3*2” 的值都是8。

示例 3:

输入: num = "3456237490", target = 9191
输出: []
解释: 表达式 “3456237490” 无法得到 9191 。

提示:

  • 1 <= num.length <= 10
  • num 仅含数字
  • -2^31 <= target <= 2^31 - 1

二、解题思路

这个问题可以通过回溯法解决。回溯法是一种试探性的算法,通过尝试所有可能的组合来找到问题的解。以下是解题思路:

  • 初始化:创建一个空的结果列表 result 来存放最终结果,以及一个空的字符串 path 来构建当前的表达式。
  • 递归函数:定义一个递归函数 backtrack,该函数接受当前构建的表达式 path,当前位置 index,当前的累计值 currentValue,上一个操作数 lastNumber,以及 num 和 target

  • 递归终止条件:当 index 等于 num 的长度时,检查 currentValue 是否等于 target。如果等于,将 path 添加到 result 中。

  • 递归过程

    • 对于当前位置 index,尝试从当前位置到字符串末尾的所有可能的数字。
    • 对于每个可能的数字,如果它是一个较长的数字,且以0开头(除了数字0本身),则跳过这个数字,因为操作数不应该包含前导零。
    • 对于每个有效的数字,计算它的值,并考虑以下三种情况:
      • 加号:将当前数字添加到 path,并递归调用 backtrack,将 currentValue 更新为 currentValue + number
      • 减号:将当前数字和减号添加到 path,并递归调用 backtrack,将 currentValue 更新为 currentValue - number
      • 乘号:由于乘法的优先级高于加法和减法,需要回退到上一个操作数的位置,并更新 currentValue 为 currentValue - lastNumber + lastNumber * number

三、具体代码

import java.util.ArrayList;
import java.util.List;

public class Solution {
    public List<String> addOperators(String num, int target) {
        List<String> result = new ArrayList<>();
        if (num == null || num.length() == 0) return result;
        backtrack(num, target, 0, 0, 0, "", result);
        return result;
    }

    private void backtrack(String num, int target, int index, long currentValue, long lastNumber, String path, List<String> result) {
        if (index == num.length()) {
            if (currentValue == target) {
                result.add(path);
            }
            return;
        }
        
        for (int i = index; i < num.length(); i++) {
            // 跳过前导零
            if (i != index && num.charAt(index) == '0') break;
            
            long number = Long.parseLong(num.substring(index, i + 1));
            
            // 第一个数字前面不需要运算符
            if (index == 0) {
                backtrack(num, target, i + 1, number, number, path + number, result);
            } else {
                // 加法
                backtrack(num, target, i + 1, currentValue + number, number, path + "+" + number, result);
                // 减法
                backtrack(num, target, i + 1, currentValue - number, -number, path + "-" + number, result);
                // 乘法
                backtrack(num, target, i + 1, currentValue - lastNumber + lastNumber * number, lastNumber * number, path + "*" + number, result);
            }
        }
    }
}

四、时间复杂度和空间复杂度

1. 时间复杂度

时间复杂度通常取决于递归调用的次数以及每次递归中的操作数。以下是分析:

  • 递归深度:递归的深度取决于输入字符串 num 的长度 n。在最坏的情况下,递归会一直进行到字符串的末尾,所以递归的深度是 n

  • 递归中的操作:在每次递归调用中,我们需要遍历从当前位置 index 到字符串末尾的所有可能的数字。在最坏的情况下,每次递归调用中,我们都会尝试 n - index 种可能的数字,其中 index 是当前递归调用的起始位置。

  • 递归调用次数:在每次递归中,我们都有三种选择(加法、减法和乘法),除了最后一次递归调用(因为字符串已经结束)。这意味着除了最后一次递归调用,每个递归调用都会产生最多3个子递归调用。

将这些因素结合起来,我们可以得到以下递归关系:

T(n) = 3 * T(n-1) + O(n)

这里,T(n) 是处理长度为 n 的字符串所需的时间,O(n) 是在每次递归中处理字符串的常数时间操作。

这个递归关系可以简化为:

T(n) = 3^n * O(n)

这是因为每次递归调用都会生成3个子递归调用,而递归的深度是 n。因此,时间复杂度是指数级的。

2. 空间复杂度

空间复杂度取决于递归调用的最大深度以及递归调用栈上存储的信息。

  • 递归调用栈:递归调用栈的深度与递归的深度相同,即 n

  • 存储的表达式:在最坏的情况下,我们可能需要存储所有可能的表达式,而表达式的数量是指数级的。因此,空间复杂度也受到存储所有可能表达式所需空间的影响。

  • 递归调用中的局部变量:在每次递归调用中,我们都会有一些局部变量(如 numbercurrentValuelastNumber 等),它们的大小是常数。

综上所述,空间复杂度主要由递归调用栈的深度和存储所有可能表达式的空间决定:

S(n) = O(n) + O(3^n)

由于 O(3^n) 是支配项,空间复杂度可以简化为:

S(n) = O(3^n)

因此,空间复杂度也是指数级的。

五、总结知识点

  • Java 类定义

    • public class Solution 定义了一个公共类 Solution
  • 方法定义

    • public List<String> addOperators(String num, int target) 定义了一个公共方法 addOperators,它接受一个字符串和一个整数作为参数,并返回一个字符串列表。
  • 异常处理

    • 检查输入字符串是否为 null 或空,如果是,则直接返回空列表,这是一种简单的异常处理。
  • 回溯算法

    • 使用递归方法 backtrack 实现回溯算法,该方法用于生成所有可能的算术表达式。
  • 字符串操作

    • 使用 substring 方法从字符串中提取子串。
    • 使用 + 运算符连接字符串。
  • 数字转换

    • 使用 Long.parseLong 方法将字符串转换为 long 类型数字。
  • 循环与迭代

    • 使用 for 循环来遍历字符串的不同部分,尝试不同的数字长度。
  • 递归调用

    • backtrack 方法内部调用自身,实现了递归。
  • 逻辑判断

    • 使用 if 语句进行条件判断,例如检查前导零和递归终止条件。
  • 数学运算

    • 在回溯过程中,计算加法、减法和乘法的当前值。
  • 局部变量与参数传递

    • 定义局部变量来存储中间结果,如当前值、上一个操作数等,并在递归调用中传递这些值。

以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一直学习永不止步

谢谢您的鼓励,我会再接再厉的!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值