刷完这 15 道回溯,就可以无脑暴力手撕前端面试了(1)

  1. candidates 是有无重复,正整数数组

  2. 数组中的每一个值只能取一次;不可以重复取值,但是对于重复的值是可以取的,即 [1,1,2,3] -> 可以取 [1,1,2],[1,3] -> 4

  3. 为了不取到重复的值,就得跳过相同值,这个时候需要对数组排序

  4. 在每一层进行枚举的时候,循环中出现重复值的时候,剪掉这部分的枚举,因为肯定有相同的一部分

  5. 由于不可以重复取,所以 dfs 第一个入参的下标是要 +1 的,表示不可以重复取上一次哪一个值

var combinationSum2 = function (candidates, target) {

candidates.sort((a,b)=>a-b)

const ret= []

const dfs = (start,arr,sum) => {

if(sum === target) {

ret.push(arr)

return

}

if(sum>target || start>= candidates.length) return

for(let i = start;i<candidates.length;i++){

// 将重复的剪掉

if(i > start && candidates[i] === candidates[i-1]) continue

// 这里的 start 是启动枚举的下标,但是插入到临时数组的值是当前下标的值

dfs(i+1,[…arr,candidates[i]],sum+candidates[i])

}

}

dfs(0,[],0)

return ret

}

复制代码

216. 组合总和 III

分析

  1. 给定的不是具体的数组,而是长度限制 k, 和目标值 target – 等同于 candidates 是无重复,1-9 的正整数数组

  2. 所以可以看做是 39. 组合总和 的特殊情况,只是判定条件有出入

var combinationSum3 = function (k, n) {

const ret = [];

const dfs = (start, arr, sum) => {

if (arr.length === k && sum === n) {

ret.push(arr);

return;

}

if (arr.length > k || sum > n) {

return;

}

for (let i = start + 1; i < 10; i++) {

dfs(i, […arr, i], sum + i);

}

};

dfs(0, [], 0);

return ret

};

复制代码

377. 组合总和 Ⅳ

分析 – 回溯

  1. candidates 是无重复,正整数数组,可以重复取值且要取排列不同的组合

  2. 这道题和组合总和很像,区别在于本题求的是排列的数量,而题1 求的是不重复的组合

  3. 所以这里不需要限制组合起始枚举的下标了,每一次都从 0 开始即可

  4. 然后超时了

*/

var combinationSum4 = function (nums, target) {

let ret = 0;

const dfs = (sum) => {

if (sum === target) {

ret++;

return;

}

if (sum > target) return;

for (let i = 0; i < nums.length; i++) {

dfs(sum + nums[i]);

}

};

dfs(0);

return ret;

};

复制代码

分析 – dp

  1. dp[i] 表示值为 i 的时候存在的组合数量

  2. 状态转移方程 dp[i] = sum(dp[i-nums[k]])

  3. base case dp[0] = 1

var combinationSum4 = function (nums, target) {

const dp = new Array(target+1)

dp[0]= 1 // 如果刚好得到的值是0,那么就有 1,因为不取也是一种取法

for(let i = 1;i<target+1;i++){

dp[i] = 0

for(let j =0;j<nums.length;j++){

if(i>=nums[j]){

dp[i]+=dp[i-nums[j]]

}

}

}

return dp[target]

}

复制代码

78. 子集

分析 – 找规律

  1. 数组元素不相同,返回值不包含重复的子集,也就是不考虑位置排列情况

  2. 由于跟排列无关,所以只需要遍历一遍 nums 即可,没遍历一次获取到的值,都可以和现有的 ret 组合成新的一批数组,然后和旧的item组合成新的枚举数组

  3. 时间复杂度 {O(n^2)}O(n2)

var subsets = function (nums) {

let ret = [[]]

for(let num of nums ){

ret = […ret,…ret.map(item => item.concat(num))]

}

return ret

}

复制代码

分析 – 迭代回溯

  1. 使用迭代的方法枚举所有的情况出来, 和多叉树遍历没啥区别

  2. 时间复杂度 {O(N^2)}O(N2)

var subsets = function (nums) {

const ret = []

const dfs = (start,arr) => {

ret.push(arr)

if(arr.length === nums.length || start=== arr.length) return

for(let i = start;i<nums.length;i++){

dfs(i+1,[…arr,nums[i]])

}

}

dfs(0,[])

return ret

}

复制代码

90. 子集 II

分析 – 有重复值

  1. 78. 子集相比,就是多了重复值,且不允许重复值出现在返回数组中,所以明显要先排序了

  2. 然后在回溯过程中,如果下一次迭代的值和当前值一样,则跳过,达到去重的效果

var subsetsWithDup = function (nums) {

nums.sort((a,b)=> a-b)

const ret = []

const dfs = (start,arr) => {

ret.push(arr)

if(start === nums.length ) return // start 超出下标,就是取到了最大下标值的时候了

for(let i = start;i<nums.length;i++){

dfs(i+1,[…arr,nums[i]])

while(nums[i] === nums[i+1]){

i++ // 去重

}

}

}

dfs(0,[])

return ret

}

复制代码

131. 分割回文串

分析

  1. 这是一个变种的组合问题,因为排列顺序已经确定好了只要切割就好

  2. 所以在遍历过程中,只有当符合回文要求的子串,才能切割,然后往下走,否则剪掉较好

  3. 回文子串的判定可以简单的用左右双指针来实现

var partition = function(s) {

const ret = []

// 判断是否是回文子串

function isValid(s) {

if(s.length === 1) return true // 只有一个字符

let l = 0,r = s.length-1

while(l<r){

if(s[l] !== s[r]) return false

l++

r–

}

return true

}

const dfs = (start,arr) => {

if(start === s.length){

ret.push(arr)

return

}

let temp =‘’

for(let i =start;i<s.length;i++){

temp+=s[i]

if(isValid(temp)){

dfs(i+1,[…arr,temp])

}

}

}

dfs(0,[])

return ret

};

复制代码

93. 复原 IP 地址

分析

  1. 这道题和 131. 分割回文串 类似

  2. 这里也是切分字符串,只是判定条件变成了每一分段都要符合有效的 IP 地址,但是架子是一样的

  3. 这里的判定条件也多,只需要将合乎要求的条件算上,就能砍掉不少的分支

var restoreIpAddresses = function (s) {

const ret = [];

function isValid(s) {

if (s.length > 1 && s[0] == 0) return false; // 不能以 0 起头

if (s >= 1 << 8) return false; // 要在 [0,255] 之间

return true;

}

const dfs = (start, arr) => {

if (arr.length === 4 && start !== s.length) return; // 已经分成4分,但是还没分完

if (start === s.length) {

if (arr.length === 4) {

ret.push(arr.join(“.”));

}

// 无论是否分成四份,都离开了

return;

}

let str = “”;

for (let i = start; i < s.length && i < start + 3; i++) {

str += s[i];

if (isValid(str)) {

dfs(i + 1, […arr, str]);

}

}

};

dfs(0, []);

return ret;

};

复制代码

112. 路径总和

分析

  1. 路径是 root-leaf 完整路线上的和为 target

  2. dfs 中序遍历走下去即可

  3. 时间复杂度 {O(n)}O(n)

var hasPathSum = function(root, targetSum) {

let ret = false

const dfs = (root,sum) => {

if(ret || !root) return // 只要一条路走通了,其他都不用走了

sum += root.val

if(!root.left && !root.right && sum === targetSum) {

ret = true

return

}

if(root.left) dfs(root.left,sum)

if(root.right) dfs(root.right,sum)

}

dfs(root,0)

return ret

};

复制代码

113. 路径总和 II

分析

  1. 找的还是 root - leaf 的路径,但是这一次要把找的所有符合要求的路径都保存起来

  2. 时间复杂度 {O(n)}O(n)

var pathSum = function(root, targetSum) {

const ret = []

const dfs = (root,arr,sum) => {

if(!root) return

sum+=root.val

arr = […arr,root.val]

if(!root.left && !root.right && sum == targetSum){

ret.push(arr)

}

if(root.left) dfs(root.left,[…arr],sum)

if(root.right) dfs(root.right,[…arr],sum)

}

dfs(root,[],0)

return ret

};

复制代码

437. 路径总和 III

分析

  1. 这次找的路径可以是树中任意 起始-结束 节点,;

  2. 但是路径必须是向下的,也就是不能是 a.left - a - a.right 的样子,这其实是减轻难度的限制条件

  3. 所以还是一样的自顶向下遍历就好,但是遇到满足需求的路径,还是要继续遍历到叶子节点位置

  4. 和 112. 路径总和 与 113. 路径总和 II 最大不同是,这一次的路径是不限制起始点和终点的;

  5. 不限制终点,那么我可以在遍历过程中,只要满足 targetSum, 就记录一次,一直到叶子节点位置,不需要到了叶子节点再判断

  6. 而不限制起始点是根节点,那么就是可以以任意节点为起始点,也就是需要遍历整一棵树作为起始点时候,往下去找路径了;

  7. 时间复杂度{O(nlogn)}O(nlogn)

var pathSum = function (root, targetSum) {

let ret = 0;

// 这是以任意 root 节点找路径和的 dfs

const dfs = (root, sum) => {

if (!root) return;

sum += root.val;

if (sum === targetSum) ret++;

if (!root.left && !root.right) return; // 叶子节点了,结束

if (root.left) dfs(root.left, sum);

if (root.right) dfs(root.right, sum);

};

// 这是遍历整棵树,然后继续往下走

const outer = (root) => {

if (!root) return;

dfs(root, 0);

outer(root.left);

outer(root.right);

};

outer(root);

return ret;

};

复制代码

51. N 皇后

参考: leetcode-cn.com/problems/n-…

分析 – 直接求符合要求的 chessboard

  1. 行就是树递归的深度,列就是每一层的宽度,使用回溯的办法进行树的 dfs 遍历

  2. 整个过程需要 3 大部分,回溯的方式遍历树,找出符合要求的节点 chessboard[row][col], 将符合要求的二维数组转换成符合要求的字符串数组

  3. 时间复杂度 {O(n*logn)}O(n∗logn)

var solveNQueens = function (n) {

const ret = [];

// 1. N 皇后实际走的过程 – 回溯树

const dfs = (row, chessboard) => {

if (row === n) {

// 已经到了叶子结点下 null 了 –

// 但是 chessboard 是一个二维数组,不能随便就push 进去的,需要深拷贝一下

ret.push(getStrChessboad(chessboard));

return;

}

// 每一行都是从 0 - n-1 , 然后不符合要求的就回溯回去

for (let col = 0; col < n; col++) {

if (isValid(row, col, chessboard)) {

// 如果 chessboard[row][col] 符合要求,则算一条路

chessboard[row][col] = “Q”;

dfs(row + 1, chessboard);

chessboard[row][col] = “.”; // 回溯回来

}

}

};

// 判断当前节点是否符合 N 皇后的要求 – 需要注意,这里 [0,n-1] 是从左往右算

function isValid(row, col, chessboard) {

// 同一列

for (let i = 0; i < row; i++) {

if (chessboard[i][col] === “Q”) {

return false;

}

}

// 从左往右 45` 倾斜

for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i–, j–) {

if (chessboard[i][j] === “Q”) {

return false;

}

}

// 从右往左 135` 倾斜

for (let i = row - 1, j = col + 1; i >= 0 && j < n; i–, j++) {

if (chessboard[i][j] === “Q”) {

return false;

}

}

// 如果不是同一列或者左右斜线,则满足要求

return true;

}

// 将二维数组的 N 皇后转成一维数组字符串形式

function getStrChessboad(chessboard) {

const ret = [];

chessboard.forEach((row) => {

let str = “”;

row.forEach((item) => {

str += item;

});

ret.push(str);

});

return ret;

}

const chessboard = new Array(n).fill([]).map(() => new Array(n).fill(“.”));

dfs(0, chessboard);

return ret;

};

复制代码

52. N皇后 II

分析

  1. 问题和 51. N 皇后 基本一样,只是求的值从完整的 N 皇后方案,变成了只要知道有几个就可以了

  2. 所以第三部分转换可以直接删除,然后直接拷贝过来即可

var totalNQueens = function (n) {

let ret = 0;

const dfs = (row, chessboard) => {

if (row === n) {

ret++;

return;

}

for (let col = 0; col < n; col++) {

if (isValid(row, col, chessboard)) {

chessboard[row][col] = “Q”;

dfs(row + 1, chessboard);

chessboard[row][col] = “.”;

}

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
= function (n) {

let ret = 0;

const dfs = (row, chessboard) => {

if (row === n) {

ret++;

return;

}

for (let col = 0; col < n; col++) {

if (isValid(row, col, chessboard)) {

chessboard[row][col] = “Q”;

dfs(row + 1, chessboard);

chessboard[row][col] = “.”;

}

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-RmASW010-1715092505181)]

[外链图片转存中…(img-xj5AsZ0f-1715092505181)]

[外链图片转存中…(img-HdoNPbXB-1715092505181)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值