为什么要写这个
- 在做题时,发现一部分复杂的问题,即使看答案也非常难以理解,我认为有更容易理解的方式去做,虽然可能性能上不是最佳解法,但是总比死背答案的好。
- 相当一部分题目是没有JS解法的,这对只会JS的前端来说很不友好。
全排列题目
给定一个不含重复数字的数组
nums,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入: nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入: nums = [0,1] 输出: [[0,1],[1,0]]
示例 3:**
输入: nums = [1]
输出: [[1]]
解析
这个题目官方的解释是回溯算法,回溯算法名字起的很唬人,但实际上还是一个递归。 官方的实现,实在难以理解。我认为此题目完全没有那么复杂。 以[1,2,3]假如让我们手写一个全排列的结果,我们会怎么做?
- 固定[1],然后看[2,3]的排列,有两个结果 [2,3]和[3,2]。那么1的全排列就有[1,2,3]和[1,3,2]
- 固定[2],然后看[1,3]的排列,有两个结果 [1,3]和[3,1]。那么1的全排列就有[2,1,3]和[2,3,1]
- 固定[3],然后看[1,2]的排列,有两个结果 [1,2]和[2,1]。那么1的全排列就有[3,1,2]和[3,2,1]
- 数组更长的时候,以此类推
我们可以看到规律,如果我们按照数组顺序,依次进行固定,把剩下的数字再进行排列,不就可以实现全排列了么?
按照这个逻辑,我们实现一个递归函数:
const FullPermutation =(nums)=>{
// 如果只有两个数字,我们进行手动排列,这就是递归的中断情况1
if(nums.length === 2) {
return [[nums[1],nums[0]],[nums[0],nums[1]]]; // 处理边界情况
}
// 如果只有一个数字,我们直接返回原数组,这是递归中断情况2
if(nums.length ===1){
return [nums]
}
// 存放结果
let resList = []
// 遍历,依次固定数组中的元素,作为排列结果的第一个数字
for(let num of nums){
const tmpNum = num;
// 固定第一个数字之后,把剩下的数字拿来做排列
const res = allOrder(tmpArray.filter(s=>s!==num));
// 排列完之后,再把这个固定的数字,放到排列的第一个位置
for(let i=0;i<res.length;i++){
res[i] =[tmpNum,...res[i]]
}
// 返回结果
resList = resList.concat(res)
}
return resList
}
相比官方的答案,这个结果我认为更符合普通人的做题逻辑,并且复杂度和官方的结果也是一样的。且代码量还比官方的少。而且这个实际上也是回溯的思路,只是不像官方答案那么明显的回溯。
类似的一个问题,兑换零钱。
零钱兑换
给不同的硬币金额coins,请兑换出taget的零钱。可以认为硬币每一个面额都是无限个。
示例 1:
输入: coins = [1,2,3] ,targert = 5
输出: [[1,1,1,1,1],[1,1,3],[1,1,1,2],[2,3],[2,2,1]]
解析
这个题目官方也是用回溯算法,实际上的过程就相当于把每一个硬币都递归的去加,一直去加到==target为止。 这个也没问题,但是如果让我去想,我觉得很难想到这种解法。 相反的,我认为这样子去做更容易理解:
- 遍历硬币金额,先拿到1,那我再去数组中找5-1
- 这样就变成从中[1,2,3]取4
- 再遍历这个数组,发现第一个取到1,那就变成从数组找4-1
- 此时发现可以取到
这个过程,一看还是个递归。
const findTarget = (coins, target) => {
const res = [];
// 遍历硬币面额
for (let num of coins) {
if (num === target) {
// 如果相等,则直接加入到结果中,这也是递归的终止条件
res.push([num])
} else if (num > target) {
//如果比target大,那不用往下递归了,直接下一个面额
continue
} else {
// 如果比target小,那不用往下递归了,直接下一个面额
const sub = findTarget(coins, target - num);
for (let subItem of sub) {
res.push([num, ...subItem]);
}
}
}
return res;
}
我们只用十几行代码就实现了这个功能,但是运行之后发现一个问题。 比如我们cions =[3,4,5],target=9,这种算法就会把[4,5]和[5,4]当成两个结果,但是实际上这两个方案是一样的。 为什么会有这种情况?因为我们在遍历4的所有场景时,已经算了[4,5]。我们等于是把包含4的所有情况都已经算完了,因此当4遍历完的时候,我们应该从coins中把4这个面额去掉。
const findTarget = (nums, target) => {
const res = [];
// 注意增加了tmpArray
const tmpArray = [...nums]
for (let num of nums) {
if (num === target)
res.push([num]);
//当前面值已经满足了,可以把它从tmparray中去除,以防被重复计算
tmpArray.shift()
} else if (num > target) {
continue
} else {
// 这里使用了tmpArray作为参数,思考一下原因
const sub = findTarget(tmpArray, target - num);
for (let subItem of sub) {
res.push([num, ...subItem]);
}
//当我们把当前这个面值的所有情况已经递归完了,那么就从tmpArray中去掉这个面值
tmpArray.shift()
}
}
return res;
}
最终我们输出的结果也是符合用例的。