宽度优先搜索
BFS 是最常考的,而BFS 中,最常考的是拓扑排序
BFS的代码实现:
BFS代码模板:
不需要分层:
Queue<T> queue = new LinkedList<T>; // queue 的作用是存储之后需要访问的点
Set<T> set = new HashSet<T>(); // set 的作用是判断这个点有没有访问过
queue.offer(head);
set.add(head);
while(!queue.isEmpyt()) {
T this_node = queue.pop();
for (T its_neighbors : this_node.neighbors) {
if (set.contains(its_neighbors)) { // 如果这个点访问过,那么就直接continue
continue;
}
queue.offer(its_neighbors);
set.add(its_neighbors);
}
// 此代码中没有分层操作
需要分层的版本:
Queue<T> queue = new LinkedList<T>(); //存储需要访问的节点
Set<T> set = new HashSet<T>(); // 存储访问过的节点
queue.offer(head);
set.add(head);
while (!queue.isEmpty()) {
int size = queue.size(); // 限定只遍历这一层的节点
for (int i = 0; i < size; i++) { // 使用 for 循环遍历这一层的节点
T this_node = queue.poll();
for (T its_neighbors: this_node.neighbors) {
if (set.contains(its_neighbors)) {
continue;
}
queue.offer(its_neighbros);
set.contains(its_neighbors);
}
// 这个是需要分层的版本
实现queue的另外的方法:
1. 使用两个队列实现BFS:
// T 表示任意你想存储的类型
Queue<T> queue1 = new LinkedList<>();
Queue<T> queue2 = new LinkedList<>();
Set<T> set = new HashSet<T>(); // 这里需要一个set来避免重复访问
queue.offer(startNode);
set.add(startNode);
int currentLevel = 0;
while (!queue1.isEmpty()) {
int size = queue1.size();
for (int i = 0; i < size; i++) {
T head = queue1.poll();
for (T its_neighbors: head.neighbors) {
if (set.contains(its_neighbors) {
continue;
}
queue2.offer(neighbor);
}
}
Queue<T> temp = queue1;
queue1 = queue2;
queue2 = temp;
queue2.clear();
currentLevel++;
}
2. 采用dummy node
dummy node 通常不保存任何值。其作用是指向链表的第一个节点(head 节点)。这样就可以方便对head进行操作或者删除。BFS中的dummy node 的作用是放在每层遍历的结尾,表示这层的节点已经遍历完全了。代码如下:
Queue<T> queue = new LinkedList<>();
queue.offer(startNode);
queue.offer(null);
currentLevel = 0;
// 因为有 dummy node 的存在,不能再用 isEmpty 了来判断是否还有没有拓展的节点了
while (queue.size() > 1) {
T head = queue.poll();
if (head == null) {
currentLevel++;
queue.offer(null);
continue;
}
for (all neighbors of head) {
queue.offer(neighbor);
}
}
什么时候使用BFS:
有的时候就是考你能否看出使用BFS
1. 图的遍历:层级遍历(BFS所做的事情就是层级遍历);由点及面(把与给定点连同的点都找到);拓扑排序(找到点与点之间的依赖关系)
层级遍历:Binary tree Level order Traversal; 二叉树的序列化及反序列化:Serialize and Deserialize Binary Tree; Binary Tree Level Order Traversal(不知道ArrayList能否这么运行) ;
由点及面:Clone Graph
拓扑排序:Topological sorting; Course Schedule I II III; Alien Dictionary。拓扑排序的思路很简单,但是解决问题用到的数据结构有点意思,用到了HashMap。拓扑排序的作用是检验一些事件的依赖关系,不能有环状依赖。
2. 最短路径:本质上和层级遍历是一样的。但是仅用于边长相等的情况(即简单图的情况)。因为最短路径就是最少的层数。DP也可以解决最短路径问题。如果是最长路径,可以用DP 或者 DFS
最短路径:Word Ladder(可以看出这个是隐式图,因为各个单词之间是有联系的)。一看是简单图,百分之百是BFS。
3. 非递归的所有方案
二叉树上的BFS:
二叉树上的BFS主要是 层级遍历 和 二叉树的序列化
二叉树上的BFS:二叉树的层级遍历。BFS使用一种特殊数据结构 队列。 图的每一层都存到队列中。扩展的时候,遍历每一层的每一个节点,找到这个节点的所有儿子。二叉树的层级遍历属于BFS的模板。注意模板里要有两重循环,但是如果是图而不是树的话,需要三重循环。
层级遍历的DFS 做法:记录每个点的level,然后把这个点append到相应level的list中;进行多次DFS,每次只记录某一层的点。虽然这种方法效率很低(比BFS低),但是没有额外空间的耗费,不需要存储某一层的所有节点。
DFS方法没有额外的空间耗费,而BFS由于需要记录每一层的节点,所以有额外的空间耗费。
BFS DFS 的时间复杂度都是多少呢????
二叉树的序列化问题:
什么是序列化:将内存中的object变成string之后存储在硬盘中。
序列化的目的:1. 由于内存的特性,一但断电,里面存储的数据就会消失。所以要及时将数据转移到不会消失的硬盘里去。2. 当想要在机器之间传递数据的时候,需要将数据序列化成字符流数据,通过网络传输和接受。
常见的序列化格式:XML; JSON; Thift; ProtoBuff
矩阵中的BFS:
Number of Islands; Knight Shortest Path;Knight Shortest Path II;
矩阵可以理解为隐式图。(因为矩阵可以看作是相互连接的)
矩阵与图是不同的。假设矩阵中有R行C列,那么会有R ×C 个点, R×C×2条边。所以在矩阵中进行BFS的时候,时间复杂度为O(R*C) 因为需要把所有点和所有边都走一遍。
Numbers of islands:这个题思路很清晰,但是还有两个需要注意的点。矩阵BFS问题的特色——需要用坐标变换; 需要验证经过坐标变换得到的点是否在矩阵内部。
Knight Shortest Path: 和上面那个题是差不多的。但是这里有一个优化的follow up。可以用双向BFS进行优化。
Number of Islands:由点及面,和 clone graph 是一致的。用坐标变换数组表示四个方向。要判断这个点是否在界内。如果用 DFS, 由于递归深度是N^2,所以可能会 stack overflow
Knight Shortest Path: 八个方向。给出起点和终点,每次跳跃长度又是一样的,典型简单图的BFS。用BFS 做出的结果就是最短的。加速方法:双向BFS。因为给了起点和终点。这样节省的时间复杂度不是1/2,而是根号关系。会节省很多时间。
Knight Shortest Path II:可以用动态规划来做。
Topological sorting:DFS 也可以做拓扑排序,但是不推荐。什么是脚本代码???
拓扑排序不需要分层遍历,也不需要hashmap(BFS中使用hashmap的原因是防止一个点被反复访问)。如果最后得到的排序数组中元素的个数和总共元素的个数不一致,说明这里面有环状依赖,说明这个拓扑排序是错误的。有且仅有一个拓扑排序,就是要保证每次从queue中取出的量是1.
Alien Dictionary: 需要你自己去建立一个图,怎么存储图,如何变成priority queue。
Zombie in matrix
build post office
图的BFS:
图和树的本质区别:图有环,树没环。树是由n- 1个点将n个点连接起来的。
图中BFS的时间复杂度:假设图中有 N 个点, M条边,则M最大是 O(N^2) 级别的。图上的BFS时间复杂度 = O(N + M) 。因为每个点要访问一次,每条边也要访问一次。需要注意,并不是两个循环叠加起来,时间复杂度就是两个循环的乘积,有可能是和!最坏的情况是O(N^2),但是最坏情况一般不会出现。
图是一种去中心化的结构,每个点之间是邻居,是平等关系。图中某点的邻居的邻居可能是这个点本身。
树是一种中心化的结构,每个点之间是父子关系。
所以图中需要hashmap 记录哪些点访问过了,哪些点没有访问过。
Clone graph: 需要先由一个点,找到其他所有点(由点及面的过程)并不需要分层遍历,但是仍然可以用BFS。
word ladder:隐式图最短路径。一看这个图是简单图,百分之百是用BFS。但是你要能够知道这是个图。在有关问题中,hashset 和 queue是一起出现的。hashmap 的时间复杂度是 O(size of key)
宽度优先搜索的优化——双向宽度有限搜索
双向宽度有限搜索的目的是求出从起点到重点的最短路径。其定义是,从起点和重点同时开始向中间搜索,当这两个搜索交汇时,搜索到的路径之和就是最短路径。
可将计算量降低到原来搜索的根号量级。原理如下:
假设每个节点相连的节点数都为N。假设单一方向的BFS需要走X层,则一共需要访问的节点是X^N。
如果是从两端向中间走,则每个BFS需要走的层数是 N/2。则每个BFS需要访问的节点数是 X^N/2.一共只需访问 2 * X^N/2.
代码如下:
// 实际上就是两个BFS写在一起,没有什么别的难度
public int doubleBFS(UndirectedGraphNode start, UndirectedGraphNode end) {
if (start.equals(end)) {
return 1;
}
// 起点开始的BFS队列
Queue<UndirectedGraphNode> startQueue = new LinkedList<>();
// 终点开始的BFS队列
Queue<UndirectedGraphNode> endQueue = new LinkedList<>();
startQueue.add(start);
endQueue.add(end);
int step = 0;
// 记录从起点开始访问到的节点
Set<UndirectedGraphNode> startVisited = new HashSet<>();
// 记录从终点开始访问到的节点
Set<UndirectedGraphNode> endVisited = new HashSet<>();
startVisited.add(start);
endVisited.add(end);
while (!startQueue.isEmpty() || !endQueue.isEmpty()) {
int startSize = startQueue.size();
int endSize = endQueue.size();
// 按层遍历
step ++;
for (int i = 0; i < startSize; i ++) {
UndirectedGraphNode cur = startQueue.poll();
for (UndirectedGraphNode neighbor : cur.neighbors) {
if (startVisited.contains(neighbor)) {//重复节点
continue;
} else if (endVisited.contains(neighbor)) {//相交
return step;
} else {
startVisited.add(neighbor);
startQueue.add(neighbor);
}
}
}
step ++;
for (int i = 0; i < endSize; i ++) {
UndirectedGraphNode cur = endQueue.poll();
for (UndirectedGraphNode neighbor : cur.neighbors) {
if (endVisited.contains(neighbor)) {
continue;
} else if (startVisited.contains(neighbor)) {
return step;
} else {
endVisited.add(neighbor);
endQueue.add(neighbor);
}
}
}
}
return -1; // 不连通
}
相关问题:
Shortest path in undirected graph
Knight shortest path
Knight shortest path II