携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情
算法场景描述
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
什么意思呢?举个例子吧,现在有一个整数数组nums = [1,1,1,1,1],还有一个整数target = 3
我现在希望让nums里的元素全部用上后能够计算出target出来,运算的规则可以是加法也可以是减法,那么就有下面这几种方案:
- +1 + 1 + 1 - 1 + 1
- +1 + 1 + 1 + 1 - 1
- +1 + 1 - 1 + 1 + 1
- +1 - 1 + 1 + 1 + 1
- -1 + 1 + 1 + 1 + 1
一共五种方式,现在我们要求的就是如何求出这个5,也就是有几种方式可以计算出目标和target
方法一:暴力递归
首先我们定义一个递归函数,只要明确并相信递归函数的定义后,按照定义去走一定能够得到我们想要的结果
定义函数traverse(nums: number[], idx: number, target: number),它的定义是:
从 nums[idx...] 开始,求出目标和为
target有多少种方式
明确了定义后,我们先考虑一下base case是什么,很明显,当idx来到数组的末尾的时候,此时算法可访问的数组元素为空,那么就不可能凑出任何一种方式去得出target,除非target是0,那也就是有一种方法凑出0,因为什么也不做,空元素求和就是0
因此我们可以写出base case的代码
if (idx === nums.length) {
return target === 0 ? 1 : 0
}
之后再按照我们的定义,去求出目标和,那么就要思考一下有多少种方式可以求出目标和
既然每个元素都可以加上正号或者负号,那至少也会有两种途径
假设前两次我们都取了正 1,那么此时的总和已经是 2 了,由于总的目标和是3,那么我们现在要求的目标和就应当是1
所以递归调用traverse([1,1,1,1,1], 2, 1),也就是说我们可访问的数组为[1,1,1],现在要在这三个里面求出目标和为1
- 假设
nums[idx]取正数,那么总的目标和现在就是3,已经达到target了,于是递归调用traverse([1,1,1,1,1], 3, 0) - 假设
nums[idx]取负数,那么总的目标和现在就是1,尚未达到target,于是递归调用traverse([1,1,1,1,1], 3, 2)
可以看到,元素取正取负正好就是目标和的两种途径来源,所以traverse([1,1,1,1,1], 2, 1)等价于traverse([1,1,1,1,1], 3, 0) + traverse([1,1,1,1,1], 3, 2)
也就是说,nums[idx]取正数的时候,我们的目标和变成了1 - 1 === 0,而nums[idx]取负数的时候,我们的目标和则变成了1 + 1 === 2
除此之外就没有别的途径能够得到当前需要的目标和了,所以我们此时就可以开始写出最终代码了,不要再进一步往下递归,只需要按照递归函数定义去尝试递归一次就行了
/**
* @description 方法一 -- 暴力递归
*/
export const solution1 = (nums: number[], target: number): number => {
// 定义递归函数:从 nums[idx...] 开始,求出 target 有多少种方式
const traverse = (nums: number[], idx: number, target: number): number => {
// base case
if (idx === nums.length) {
return target === 0 ? 1 : 0
}
// 两种途径得到目标和 -- nums[idx] 取正和取负
return (
traverse(nums, idx + 1, target - nums[idx]) +
traverse(nums, idx + 1, target + nums[idx])
)
}
// 从 nums[0...] 开始,求出目标和为 target 的方法总数
return traverse(nums, 0, target)
}
方法二 -- 记忆化搜索优化
其实上面的过程中存在重复计算,这个时候我们就可以用记忆化搜索的方式,减少重复计算,也就是添加一个缓存表记录一下每次递归的计算结果,之后就可以查询这个缓存表去避免重复地计算
比如idx === 7, target === 8的结果为 66,而idx === 7, target === 9的结果为 77,那么我们的缓存表就应该设计成嵌套的map结构,也就是:
{
7: {
8: 66,
9: 77
}
}
代码如下:
/**
* @description 方法二 -- 记忆化搜索优化
*/
const solution2 = (nums: number[], target: number): number => {
// 定义递归函数:从 nums[idx...] 开始,求出 target 有多少种方式
const traverse = (
nums: number[],
idx: number,
target: number,
memo: Map<number, Map<number, number>>,
): number => {
// base case
if (idx === nums.length) {
return target === 0 ? 1 : 0
}
// 查询缓存表
if (!memo.has(idx)) {
memo.set(idx, new Map())
}
const targetMap = memo.get(idx)!
if (targetMap.has(target)) {
// 缓存命中
return targetMap.get(target)!
}
// 计算结果
const res: number =
traverse(nums, idx + 1, target - nums[idx], memo) +
traverse(nums, idx + 1, target + nums[idx], memo)
// 将结果存入缓存
targetMap.set(target, res)
return res
}
return traverse(nums, 0, target, new Map())
}