携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情
一、前言
BFS
(Breath First Search
,广度优先搜索)和DFS
(Depth First Search
,深度优先搜索)是特别常用的两种算法。
BFS
相对 DFS
的最主要区别是: BFS
找到的路径一定是最短的,但代价是空间复杂度比 DFS
高很多。
因为
BFS
的逻辑,depth
每增加一次,队列中的所有节点都向前迈一步,这个逻辑保证了一旦找到一个终点,走的步数是最少的。
BFS
算法基本上会使用队列,典型的是树的层序遍历:
void traverse(TreeNode root) {
if (null == root) return;
// 初始化队列,将 root 加入队列
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
// 层级遍历代码
System.out.println(root.val);
if (cur.left != null) q.offer(cur.left);
if (cur.right != null) q.offer(cur.right);
}
}
队列的一些操作,必知:
// 创建队列
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(e); // 入队
queue.add(e); // 入队
queue.poll(); // 出队
// 优先队列:最小堆(默认)
Queue<Integer> q = new PriorityQueue<>((a, b) -> a - b);
// 优先队列:最大堆
Queue<Integer> q = new PriorityQueue<>(Collections.reverseOrder());
// 自定义排序:最小堆
Queue<ListNode> q = new PriorityQueue<>((a, b) -> a.val - b.val);
双向 BFS
双向 BFS
与 传统 BFS
区别:
- 传统的
BFS
: 是从起点开始向四周扩散,遇到终点时停止。 - 双向
BFS
: 是从起点和终点同时开始扩散,当两边有交集的时候停止。
双向 BFS
局限性在于: 必须知道终点在哪。
Tips
: 无论传统 BFS
还是双向 BFS
,空间复杂度都是一样的。双向 BFS
只是一种技巧。
题目:127. 单词接龙
二、题目
(1)二叉树的右视图(中)
题干分析
这个题目说的是,给你一棵二叉树,并且你站在这棵树的右边,你要返回从上到下看到的节点值。
# 比如说,给你的二叉树是:
1
/ \
2 4
/ \
6 8
# 站在这棵二叉树的右边看过来,从上到下看到的数字依次是:
[1, 4, 8]
思路解法
思路有二: BFS
和 DFS
方法一:BFS
- 层序遍历顺序:从左到右,一个个入队。
- 输出每一层中队列的最后一个元素即可。
// 方法一: BFS
// Time: O(n), Space: O(n), Faster: 82.03%
public List<Integer> rightSideViewBFS(TreeNode root) {
if (null == root) return Collections.emptyList();
Queue<TreeNode> queue = new LinkedList<>();
List<Integer> result = new ArrayList<>();
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
TreeNode node = null;
while (size-- > 0) {
node = queue.poll();
if (null != node.left) queue.add(node.left);
if (null != node.right) queue.add(node.right);
}
result.add(node.val);
}
return result;
}
方法二:DFS
- 注意递归方向: 先右子树,再左子树。
// 方法二:DFS
// Time: O(n), Space: O(n), Faster: 100.00%
public List<Integer> rightSideViewDFS(TreeNode root) {
List<Integer> result = new ArrayList<>();
dfs(root, result, 0);
return result;
}
private void dfs(TreeNode root, List<Integer> result, int level) {
if (root == null) return;
if (level == result.size()) result.add(root.val);
dfs(root.right, result, level + 1);
dfs(root.left, result, level + 1);
}
(2)打开转盘锁(中)
题干分析
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。
列表 deadends
包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。
示例 1:
输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。
示例 2:
输入: deadends = ["8888"], target = "0009"
输出:1
解释:把最后一位反向旋转一次即可 "0000" -> "0009"。
示例 3:
输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:无法旋转到目标数字且不被锁定。
思路解法
思路有二: BFS
和 双向 BFS
方法一:BFS
deads
哈希set
: 记录死亡数字visited
哈希set
: 记录访问过的数字- 队列:
BFS
遍历使用
// Time: O(b^d * d^2 + md), Space: O(b^d * d^2 + md), Faster: 15.30%
public int openLock(String[] deadends, String target) {
// 记录需要跳过的死亡密码
Set<String> deads = new HashSet<>();
for (String s : deadends) deads.add(s);
// 记录已经穷举过的密码,防止走回头路
Set<String> visited = new HashSet<>();
Queue<String> q = new LinkedList<>();
// 从起点开始启动 BFS
int step = 0;
q.offer("0000");
visited.add("0000");
while (!q.isEmpty()) {
int size = q.size();
for (int i = 0; i < size; ++i) {
String cur = q.poll();
// 判断密码是否合法
if (deads.contains(cur)) {
continue;
}
if (target.equals(cur)) {
return step;
}
for (int j = 0; j < 4; ++j) {
String up = plusOne(cur, j); // 向下拨, 0 -> 1
if (!visited.contains(up)) {
q.offer(up);
visited.add(up);
}
String down = minusOne(cur, j); // 向上拨, 0 -> 9
if (!visited.contains(down)) {
q.offer(down);
visited.add(down);
}
}
}
++step; // 增加步数
}
// 穷举完了,没有找到目标
return -1;
}
// 向下拨, 0 -> 1
private String plusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '9') {
ch[j] = '0';
} else {
ch[j] += 1;
}
return new String(ch);
}
// 向上拨, 0 -> 9
private String minusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '0') {
ch[j] = '9';
} else {
ch[j] -= 1;
}
return new String(ch);
}
方法二:双向BFS
// Time: O(b^d * d^2 + md), Space: O(b^d * d^2 + md), Faster: 84.26%
public int openLockBFS2(String[] deadends, String target) {
// 记录需要跳过的死亡密码
Set<String> deads = new HashSet<>();
for (String s : deadends) deads.add(s);
// 记录已经穷举过的密码,防止走回头路
Set<String> visited = new HashSet<>();
Set<String> q1 = new HashSet<>();
Set<String> q2 = new HashSet<>();
// 从起点开始启动 BFS
int step = 0;
// 初始化起点和终点
q1.add("0000");
q2.add(target);
while (!q1.isEmpty() && !q2.isEmpty()) {
// 在遍历的过程不能修改哈希集合
// 用 temp 存储 q1的扩散结果
Set<String> temp = new HashSet<>();
// 将 q1 中的所有节点向周围扩散
for (String cur : q1) {
// 判断密码是否合法
if (deads.contains(cur)) {
continue;
}
if (q2.contains(cur)) {
return step;
}
visited.add(cur);
for (int j = 0; j < 4; ++j) {
String up = plusOne(cur, j); // 向下拨, 0 -> 1
if (!visited.contains(up)) {
temp.add(up);
}
String down = minusOne(cur, j); // 向上拨, 0 -> 9
if (!visited.contains(down)) {
temp.add(down);
}
}
}
++step; // 增加步数
// temp 相当于 q1
// 这里交换 q1 q2,下一轮 while 就是扩散 q2
q1 = q2;
q2 = temp;
}
// 穷举完了,没有找到目标
return -1;
}