一、题目描述
给定一个仅包含数字 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
。 -
存储的表达式:在最坏的情况下,我们可能需要存储所有可能的表达式,而表达式的数量是指数级的。因此,空间复杂度也受到存储所有可能表达式所需空间的影响。
-
递归调用中的局部变量:在每次递归调用中,我们都会有一些局部变量(如
number
、currentValue
、lastNumber
等),它们的大小是常数。
综上所述,空间复杂度主要由递归调用栈的深度和存储所有可能表达式的空间决定:
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
语句进行条件判断,例如检查前导零和递归终止条件。
- 使用
-
数学运算:
- 在回溯过程中,计算加法、减法和乘法的当前值。
-
局部变量与参数传递:
- 定义局部变量来存储中间结果,如当前值、上一个操作数等,并在递归调用中传递这些值。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。