目录

宽度优先搜索的核心思想

算法实现步骤

BFS的特点和应用场景

BFS 在树结构的应用


宽度优先搜索的核心思想

想象一下你在玩一个迷宫游戏,你站在起点,想知道最快到达终点的路线。BFS的策略是:

首先探索所有起点直接相连的位置(第一层)。

然后探索所有与第一层位置直接相连的、且未被访问过的位置(第二层)。

接着探索所有与第二层位置直接相连的、且未被访问过的位置(第三层)。

... 以此类推,直到找到终点或者遍历完所有能到达的点。

这种“由近及远”的搜索方式,保证了一旦找到目标,所用的步数(或路径长度)一定是最短的。

算法实现步骤

BFS通常使用一个队列(Queue) 这种数据结构来辅助实现。队列的特点是“先进先出”(FIFO),这正好符合我们“先访问的节点,其邻居也先被访问”的需求。

步骤分解:

初始化:选择一个起始节点,将其标记为“已访问”。将该起始节点放入队列。

循环执行以下步骤,直到队列为空:
a. 出队:从队列的头部取出一个节点(我们称它为“当前节点”)。
b. 处理:访问该节点(例如,检查它是否是目标节点,或者进行其他操作)。
c. 探索邻居:检查当前节点的所有“未访问”的邻居节点。
* 将每一个未访问的邻居节点标记为“已访问”。
* 将这些邻居节点依次放入队列的尾部。

结束:当队列为空时,说明所有从起点可达的节点都已经被访问过了,算法结束。

一个生动的例子

    0
   / \
  1   2
 / \   \
3   4   5

步骤模拟:

步骤队列状态 (队首<- ... <-队尾)当前节点访问顺序动作说明
初始[0]--将起点0放入队列
1[1, 2]00取出0,访问它。将0的未访问邻居1, 2加入队列。
2[2, 3, 4]11取出1,访问它。将1的未访问邻居3, 4加入队列。
3[3, 4, 5]22取出2,访问它。将2的未访问邻居5加入队列。
4[4, 5]33取出3,访问它。3没有未访问的邻居。
5[5]44取出4,访问它。4没有未访问的邻居。
6[]55取出5,访问它。5没有未访问的

最终的访问顺序(BFS遍历序列)是:0, 1, 2, 3, 4, 5。你可以看到,这正是一层一层输出的结果。

BFS的特点和应用场景

特点:

完备性:如果解存在,BFS一定能找到。

最优性:在无权图中,BFS找到的路径一定是最短路径。

空间复杂度高:在最坏情况下,需要存储一整层的节点,对于分支因子为b的树,第d层有b^d个节点,空间复杂度为O(b^d)。

应用场景:

图的连通性:判断两个节点是否连通。

最短路径:在无权图中求两点间的最短路径。

迷宫求解:找到从起点到终点的最短路径。

社交网络:查找“度”关系(例如,寻找三度好友)。

序列变换(如单词接龙问题)。

树的层序遍历。

BFS 在树结构的应用

层序遍历

题目链接

解析:采用深度优先搜索,但难点是不知道每层具体有多少结点,也就不知道要把多少结果放在数组的同一行,解决方法是在遍历队列之前用一个变量先记录一下队列的长度,然后执行长度次访问队头的操作,执行完后,再次记录队列的长度,下一次遍历队列时就访问长度次......以此类推。

class Solution {
public:
    vector> levelOrder(Node* root) {
        queue q;
        vector> 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> zigzagLevelOrder(TreeNode* root) {
        vector> ret;
        queue 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> q;
        if(root) q.push(make_pair(root,1));
        unsigned maxlen = 0;
        while(!q.empty())
        {
            pair front = q.front();
            pair back = q.back();
            if(back.second - front.second + 1 > maxlen)
                maxlen = back.second - front.second + 1;
            int sz = q.size();
            while(sz--)
            {
                pair 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;
    }
};