【算法】队列 + 宽度优先搜索
目录
宽度优先搜索的核心思想
算法实现步骤
BFS的特点和应用场景
BFS 在树结构的应用
宽度优先搜索的核心思想
想象一下你在玩一个迷宫游戏,你站在起点,想知道最快到达终点的路线。BFS的策略是:
首先探索所有起点直接相连的位置(第一层)。
然后探索所有与第一层位置直接相连的、且未被访问过的位置(第二层)。
接着探索所有与第二层位置直接相连的、且未被访问过的位置(第三层)。
... 以此类推,直到找到终点或者遍历完所有能到达的点。
这种“由近及远”的搜索方式,保证了一旦找到目标,所用的步数(或路径长度)一定是最短的。
算法实现步骤
BFS通常使用一个队列(Queue) 这种数据结构来辅助实现。队列的特点是“先进先出”(FIFO),这正好符合我们“先访问的节点,其邻居也先被访问”的需求。
步骤分解:
初始化:选择一个起始节点,将其标记为“已访问”。将该起始节点放入队列。
循环执行以下步骤,直到队列为空:
a. 出队:从队列的头部取出一个节点(我们称它为“当前节点”)。
b. 处理:访问该节点(例如,检查它是否是目标节点,或者进行其他操作)。
c. 探索邻居:检查当前节点的所有“未访问”的邻居节点。
* 将每一个未访问的邻居节点标记为“已访问”。
* 将这些邻居节点依次放入队列的尾部。结束:当队列为空时,说明所有从起点可达的节点都已经被访问过了,算法结束。
一个生动的例子
0/ \1 2/ \ \
3 4 5
步骤模拟:
步骤 | 队列状态 (队首<- ... <-队尾) | 当前节点 | 访问顺序 | 动作说明 |
---|---|---|---|---|
初始 | [0] | - | - | 将起点0放入队列 |
1 | [1, 2] | 0 | 0 | 取出0,访问它。将0的未访问邻居1, 2加入队列。 |
2 | [2, 3, 4] | 1 | 1 | 取出1,访问它。将1的未访问邻居3, 4加入队列。 |
3 | [3, 4, 5] | 2 | 2 | 取出2,访问它。将2的未访问邻居5加入队列。 |
4 | [4, 5] | 3 | 3 | 取出3,访问它。3没有未访问的邻居。 |
5 | [5] | 4 | 4 | 取出4,访问它。4没有未访问的邻居。 |
6 | [] | 5 | 5 | 取出5,访问它。5没有未访问的 |
最终的访问顺序(BFS遍历序列)是:0, 1, 2, 3, 4, 5。你可以看到,这正是一层一层输出的结果。
BFS的特点和应用场景
特点:
完备性:如果解存在,BFS一定能找到。
最优性:在无权图中,BFS找到的路径一定是最短路径。
空间复杂度高:在最坏情况下,需要存储一整层的节点,对于分支因子为b的树,第d层有b^d个节点,空间复杂度为O(b^d)。
应用场景:
图的连通性:判断两个节点是否连通。
最短路径:在无权图中求两点间的最短路径。
迷宫求解:找到从起点到终点的最短路径。
社交网络:查找“度”关系(例如,寻找三度好友)。
序列变换(如单词接龙问题)。
树的层序遍历。
BFS 在树结构的应用
层序遍历
题目链接
解析:采用深度优先搜索,但难点是不知道每层具体有多少结点,也就不知道要把多少结果放在数组的同一行,解决方法是在遍历队列之前用一个变量先记录一下队列的长度,然后执行长度次访问队头的操作,执行完后,再次记录队列的长度,下一次遍历队列时就访问长度次......以此类推。
class Solution {
public:vector<vector<int>> levelOrder(Node* root) {queue<Node*> q;vector<vector<int>> ret;if(root) q.push(root);int i = 0;while(!q.empty()){ret.resize(i + 1); // 为下一次记录结果开辟空间int sz = q.size(); // 记录这一层的结点个数while(sz--) {// 访问对头ret[i].push_back((q.front())->val); // 把对头的子结点加入队列for(auto j : (q.front())->children){q.push(j);}// 对头已经访问完了,出队列q.pop();}i++;}return ret;}
};
题目链接
解析:先进行二叉树的层序遍历,最后将结果数组的对应部分进行逆序
class Solution {
public:vector<vector<int>> zigzagLevelOrder(TreeNode* root) {vector<vector<int>> ret;queue<TreeNode*> q;int sz = 0;int i = 0;if(root) q.push(root);while(!q.empty()){sz = q.size();ret.resize(i + 1);while(sz--){TreeNode* top = q.front();ret[i].push_back(top->val);q.pop();if(top->left) q.push(top->left);if(top->right) q.push(top->right);}i++;}for(int j = 1; j < ret.size(); j += 2){reverse(ret[j].begin(),ret[j].end());}return ret;}
};
题目链接
解析:以从左到右,从上到下,从根结点开始为 1,依次对非空结点进行编号,可以发现如果根结点的编号为 n ,那么它的左孩子和右孩子的编号分别为 2*n 和 2*n + 1,树某行的宽度 = 最右边的结点编号 - 最左边结点编号 + 1。对树进行层序遍历,队列中每个元素存储结点和结点的编号。
lass Solution {
public:int widthOfBinaryTree(TreeNode* root) {queue<pair<TreeNode*,unsigned int>> q;if(root) q.push(make_pair(root,1));unsigned maxlen = 0;while(!q.empty()){pair<TreeNode*,unsigned int> front = q.front();pair<TreeNode*,unsigned int> back = q.back();if(back.second - front.second + 1 > maxlen) maxlen = back.second - front.second + 1;int sz = q.size();while(sz--){pair<TreeNode*,unsigned int> front = q.front();if((front.first)->left) q.push(make_pair((front.first)->left,front.second * 2));if((front.first)->right) q.push(make_pair((front.first)->right,front.second * 2 + 1));q.pop();}}return maxlen;}
};