广度:从起点开始,先扫一层,再扫一层,层层递进,地毯式搜索 (一开始什么都学,越来越精)
深度:一直到底,再回来 (先把一个东西学的透彻)
模板
BFS – 非递归写法 符合人类的思维习惯,需要维护一个 queue
var BFS = function(root) {
const q = [root]
const visited = [root]
const res = []
while (q.length) {
const currentLevel = []
const currentLevelLength = q.length
for (let i = 0; i < currentLevelLength; i++) {
const node = q.shift()
visited.push(node)
// 处理当前节点
currentLevel.push(node)
// 给 Queue 添加节点
if (node.left) q.push(node.left)
if (node.right) q.push(node.right)
}
// processing work
res.push(currentLevel)
}
return res
};
DFS - 递归写法
let visited = new Set()
const res = []
var DFS = function(node, visited) {
visited.add(node)
// process current node
for (const next_node of node.children) {
if (!visited.has(next_node)) {
DFS(next_node, visited)
}
}
return res
};
102. 二叉树的层序遍历
BFS
var levelOrder = function(root) {
if (!root) return []
const res = []
const q = [root]
while (q.length) {
const currentLevel = []
const currentLevelLength = q.length
// 循环当前队列所有元素,shift 然后 push 进 currentLevel 最后添加 queue
for (let i = 0; i < currentLevelLength; i++) {
const node = q.shift()
currentLevel.push(node.val)
if (node.left) q.push(node.left)
if (node.right) q.push(node.right)
}
res.push(currentLevel)
}
return res
};
104. 二叉树的最大深度
DFS
var maxDepth = function(root) {
if (!root) return 0
return Math.max(maxDepth(root.left),maxDepth(root.right)) + 1
};
BFS
var maxDepth = function(root) {
if (!root) return 0
const q = [root]
let res = 0
while (q.length) { // 一直到最后的叶子节点,才退出循环,此时 res 为最大深度
const currentLevel = []
const currentLevelLength = q.length
for (let i = 0; i < currentLevelLength; i++) {
const node = q.shift()
currentLevel.push(node)
if (node.left) q.push(node.left)
if (node.right) q.push(node.right)
if(!node.left && !node.right){
// 没有左节点和右节点,说明其为叶子节点,可用来判断最小深度
}
}
res++
}
return res
};
111. 二叉树的最小深度
var minDepth = function(root) {
if (!root) return 0
if (!root.left) return 1 + minDepth(root.right)
if (!root.right) return 1 + minDepth(root.left)
return Math.min(minDepth(root.left), minDepth(root.right)) + 1
};
22. 括号生成
若输入为 n 维护一个 2n 的数组,数组中存在 ( 或 ) 两种可能性,总共生成 2^2n 可能性,判断是否合法
在上面的基础上进行剪枝
缩进使用两个空格 https://google.github.io/styleguide/jsguide.html
var generateParenthesis = function(n) {
const list = []
const _gen = (l, r, n, res) => {
if (l == n && r == n) { // n 全部花完,组装完毕
list.push(res)
}
if (l < n) { // 先组装 (
_gen(l + 1, r, n, res + "(")
}
// 再组装 ), 添加 ) 时,必须要有 ( ,所以左边的值要大于右边的值
if (r < l) {
_gen(l, r + 1, n, res + ")")
}
}
_gen(0, 0, n, "")
return list
};
Java List<List<Integer>>
二维数组
/* 初始化 类似二位数组 */
List<List<Integer>> list = new ArrayList<>();
/* 添加list的层和值 */
/* 方法一 添加层,再在层中添加数字*/
list.add(new LinkedList<>()); // 添加层数
list.get(0).add(1);
list.add(new LinkedList<>());
list.get(1).add(11);
list.get(1).add(12);
list.get(1).add(0, 13); // [13, 11, 12]
System.out.println(list); // [[1], [13, 11, 12]]
System.out.println(list.get(1)); // [13, 11, 12]
/* 方法二 一次添加一个一维数组 */
list.add(new LinkedList<>(Arrays.asList(1, 2, 3, 6)));
System.out.println(list); // [[1], [13, 11, 12], [1, 2, 3, 6]]
/* list元素的删除 */
list.remove(1);
list.get(1).remove(0);
System.out.println(list); // [[1], [2, 3, 6]]
Arrays.toString( list.get(i).toArray() )
如果想要把数组中的内容
打印出来,直接使用toString方法只会打印出数组的地址,因此需要使用Arrays的toString
方法
Arrays.asList(1, 2, 3, 6)
将一个数组转换为 List
46. 全排列
回溯算法与深度优先遍历
以下是维基百科中「回溯算法」和「深度优先遍历」的定义。
回溯法
采用试错
的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案;
- 在尝试了所有可能的分步方法后宣告该问题没有答案
深度优先搜索算法
(英语:Depth-First-Search,DFS)是一种用于遍历
或搜索树
或图
的算法。这个算法会 尽可能深 的搜索树的分支。当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。
我刚开始学习「回溯算法」的时候觉得很抽象,一直不能理解为什么递归之后需要做和递归之前相同的逆向操作,在做了很多相关的问题以后,我发现其实「回溯算法」与「 深度优先遍历 」有着千丝万缕的联系。
个人理解
「回溯算法」与「深度优先遍历」都有「不撞南墙不回头」的意思。我个人的理解是:
「回溯算法
」强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。强调了 ⭐️ 回退
操作对于搜索的合理性。(🌲 树形状态
上的深度优先遍历,范围更细)
而「深度优先遍历
」强调一种⭐️ 遍历
的思想,与之对应的遍历思想是「广度优先遍历」。至于广度优先遍历为什么没有成为强大的搜索算法,我们在题解后面会提。
java 实现
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
boolean[] used = new boolean[nums.length];
List<Integer> path = new ArrayList<>();
dfs(nums, 0, path, used, res);
return res;
}
private void dfs(int[] nums, int depth,
List<Integer> path, boolean[] used,
List<List<Integer>> res) {
if (depth == nums.length) {
// 添加 path 的拷贝
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (!used[i]) {
path.add(nums[i]);
used[i] = true;
dfs(nums, depth + 1, path, used, res);
used[i] = false;
// 撤销已选择的元素
path.remove(path.size() - 1);
}
}
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
Solution solution = new Solution();
List<List<Integer>> lists = solution.permute(nums);
System.out.println(lists);
}
}
js 实现
var permute = function (nums) {
const path = []
const used = new Array(nums.length).fill(false)
const res = []
const dfs = (nums, depth, path, used, res) => {
if (depth == nums.length) {
res.push([...path])
}
for (let i = 0; i < nums.length; i++) {
if (!used[i]) {
used[i] = true
path.push(nums[i])
dfs(nums, depth + 1, path, used, res)
path.pop()
used[i] = false
}
}
}
dfs(nums, 0, path, used, res)
return res
};
数组拷贝
ES6的Array.from()
ES6的扩展运算符(…)
52. N皇后 II
x 轴 在右边、 y 轴在下边
左边: y = x + c 即 x - y = -c
右边: y = -x + c 即 x + y = c
var totalNQueens = function(n) {
let res = 0
const cols = new Set()
const pie = new Set() // x + y
const na = new Set() // x - y
const dfs = (n, row) => {
if (row == n) {
res++
return
}
for (let col = 0; col < n; col++) {
if (cols.has(col) || pie.has(row + col) || na.has(row - col)) continue
cols.add(col)
pie.add(row + col)
na.add(row - col)
dfs(n, row + 1)
cols.delete(col)
pie.delete(row + col)
na.delete(row - col)
}
}
dfs(n, 0)
return res
};
基于位运算的回溯
var totalNQueens = function(n) {
let res = 0
const dfs = (row, col, pie, na) => {
if (row == n) {
res++
return
}
// 得到可以放皇后的位置(0 表示没有皇后放置),取反为 1 表示可以放置皇后 &去掉前面冗余的数字
let bits = (~(col | pie | na)) & ((1 << n) - 1)
while (bits) {
// 得到最后的 1 的位置
const p = bits & -bits
// col | p 更新列,表示这个列已经占据一个皇后。向下走一行后,撇捺要往左右移动一位
dfs(row + 1, col | p, (pie | p) << 1, (na | p) >> 1)
// 去掉最后的 1
bits &= bits - 1
}
}
dfs(0, 0, 0, 0)
return res
};
51. N 皇后
var solveNQueens = function (n) {
const res = []
const cols = new Set()
const pie = new Set() // x + y
const na = new Set() // x - y
const dfs = (n, row, cur_state) => {
if (row == n) {
res.push(cur_state)
return
}
for (let col = 0; col < n; col++) {
if (cols.has(col) || pie.has(row + col) || na.has(row - col)) continue
cols.add(col)
pie.add(row + col)
na.add(row - col)
dfs(n, row + 1, cur_state + col)
cols.delete(col)
pie.delete(row + col)
na.delete(row - col)
}
}
dfs(n, 0, [])
const _generate_result = (n) => {
const borads = []
for (const r of res) {
const borad = []
for (const i of r) {
borad.push(".".repeat(i) + "Q" + ".".repeat(n - i - 1))
}
borads.push(borad)
}
return borads
}
return _generate_result(n)
};
基于位运算的回溯
var solveNQueens = function (n) {
const res = new Set()
const dfs = (row, col, pie, na, cur_state) => {
if (row == n) {
res.add(cur_state)
return
}
// 得到可以放皇后的位置(0 表示没有皇后放置),取反为 1 表示可以放置皇后 &去掉前面冗余的数字
let bits = (~(col | pie | na)) & ((1 << n) - 1)
while (bits) {
// 得到最后的 1 的位置
const p = bits & -bits
// log2^n = Math.log(n) / Math.log(2)
const Qcol = Math.log((col | p) - col) / Math.log(2)
dfs(row + 1, col | p, (pie | p) << 1, (na | p) >> 1, cur_state + Qcol)
// 去掉最后的 1
bits &= bits - 1
}
}
dfs(0, 0, 0, 0, [])
const _generate_result = (n) => {
const borads = []
for (const r of res) {
const borad = []
for (const i of r) {
borad.push(".".repeat(n - i - 1) + "Q" + ".".repeat(i))
}
borads.push(borad)
}
return borads
}
return _generate_result(n)
};