图的搜索/遍历(BFS、DFS)


从图的某一顶点出发,按照某种搜索方法沿着图中的边对图中所有顶点访问一次,且只访问一次。


一、广度优先搜索(Breadth-First-Search,BFS)

类似于二叉树的广度优先遍历(层序遍历),先访问起始顶点v,依次访问v的各个未被访问的邻接顶点w1、w2、……、wi,再依次访问w1、w2、……、wi的所有未被访问过的邻接顶点,直到该连通分量的所有顶点都被访问过为止。若此时图中尚有顶点未被访问过,则另选一个未曾被访问过的顶点作为起始点,重复上述过程,直到图中所有顶点都被访问到为止。

在算法中,需要使用的一个基本操作是:通过某一顶点找到与之相邻的其他顶点。可以先实现图的两个基本操作:

  • FirstNeighbor(G, x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。
  • NextNeighbor(G, x, y):若图G中顶点y是顶点x的一个邻接顶点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。
    利用这两个基本操作编写一个for循环,就可以实现通过某一顶点找到于之相邻的其他顶点:

利用这两个基本操作编写一个for循环,就可以实现通过某一顶点找到于之相邻的其他顶点:

// 通过顶点x找到与之相邻的其他顶点
for(w = FirstNeighbor(G, x);w >= 0;w = NextNeighbor(G, x, w)) {

}

在二叉树的广度优先遍历(层序遍历)中,由于二叉树不存在**“回路”,搜索相邻结点时,不可能搜到已经访问过的结点。但在图中,搜索相邻的顶点时,有可能搜到已经访问过的顶点。因此需要使用一个数组visited标记各个顶点是否已经被访问过**,只需在搜索过程中检查visited数组,就可以知道当前顶点是否已经被访问过:

// 通过顶点x找到与之相邻的其他顶点
for(w = FirstNeighbor(G, x);w >= 0;w = NextNeighbor(G, x, w)) {
    // 检查x的所有邻接顶点是否被访问过
    if(!visited[w]) { // 若当前顶点未被访问过
        
    }
}

算法过程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

伪代码

bool visited[MAX_VERTEX_NUM]; // 访问标记数组

// 图的广度优先遍历
void BFS(Graph G, int v) { // 从顶点v出发,广度优先遍历图G
    queue<int> que; // 辅助队列
    visit(v); // 访问起始顶点v
    visited[v] = true; // 标记v已被访问
    que.push(v); // 顶点v入队列
    // 队列非空时,进入循环
    while(!que.empty()) {
        int x = que.front(); // 获取队头顶点x
        que.pop(); // 队头顶点出队列
        // 通过顶点x找到与之相邻的其他顶点
        for(w = FirstNeighbor(G, x);w >= 0;w = NextNeighbor(G, x, w)) {
            // 检查x的所有邻接顶点是否被访问过
            if(!visited[w]) { // 若当前顶点未被访问过
                visit(w); // 访问当前顶点
                visited[w] = true; // 标记当前顶点已被访问
                que.push(w); // 当前顶点入队列
            }
        }
    }
}

存在的问题:
若图是非连通图,BFS函数只能遍历完起始顶点所在的连通分量,无法遍历完所有顶点。
在这里插入图片描述
需要在调用完BFS函数后,再次检查visited数组,看是否还有顶点未被访问,若尚有顶点未被访问过,则另选一个未曾被访问过的顶点作为起始点,再次调用BFS函数,直到图中所有顶点都被访问到为止。
最终伪代码:

bool visited[MAX_VERTEX_NUM]; // 访问标记数组

// 图的广度优先遍历
void BFS_traverse(Graph G) { // 对图G进行广度优先遍历
    // 初始化visited数组
    for(int i = 0;i < G.vexnum;i ++) {
        visited[i] = false;
    }
    // 遍历顶点
    for(int i = 0;i < G.vexnum; i ++) {
        if(!visited[i]) { // 若当前顶点未被访问过
            BFS(G, i); // 从当前顶点开始广度优先遍历图G
        }
    }
}

void BFS(Graph G, int v) { // 从顶点v出发,广度优先遍历图G
    queue<int> que; // 辅助队列
    visit(v); // 访问起始顶点v
    visited[v] = true; // 标记v已被访问
    que.push(v); // 顶点v入队列
    // 队列非空时,进入循环
    while(!que.empty()) {
        int x = que.front(); // 获取队头顶点x
        que.pop(); // 队头顶点出队列
        // 通过顶点x找到与之相邻的其他顶点
        for(w = FirstNeighbor(G, x);w >= 0;w = NextNeighbor(G, x, w)) {
            // 检查x的所有邻接顶点是否被访问过
            if(!visited[w]) { // 若当前顶点未被访问过
                visit(w); // 访问当前顶点
                visited[w] = true; // 标记当前顶点已被访问
                que.push(w); // 当前顶点入队列
            }
        }
    }
}

对于无向图,调用BFS函数的次数=图的连通分量数

复杂度

空间复杂度:O(|V|)。

时间复杂度:

  • 邻接矩阵:O(|V|2)。
  • 邻接表:O(|V|+|E|)。

二、深度优先搜索(Depth-First-Search,DFS)

类似于树的先序遍历(深度优先遍历),首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点w1,再访问与w1邻接且未被访问的任一顶点w2……重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问国,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。

在算法中,依然要使用FirstNeighbor(G, x)NextNeighbor(G, x, y)来实现通过某一顶点找到与之相邻的其他顶点的基本操作。也需要使用数组visited标记各个顶点是否已经被访问过

// 通过顶点x找到与之相邻的其他顶点
for(w = FirstNeighbor(G, x);w >= 0;w = NextNeighbor(G, x, w)) {
    // 检查x的所有邻接顶点是否被访问过
    if(!visited[w]) { // 若当前顶点未被访问过
        
    }
}

算法过程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

伪代码

bool visited[MAX_VERTEX_NUM]; // 访问标记数组

void DFS(Graph G, int v) { // 从顶点v出发,深度优先遍历图G
    visit(v); // 访问顶点v
    visited[v] = true; // 标记v已被访问
    // 通过顶点v找到与之相邻的其他顶点
    for(w = FirstNeighbor(G, v);w >= 0;w = NextNeighbor(G, v, w)) {
        // 检查v的所有邻接顶点是否被访问过
        if(!visited[w]) { // 若当前顶点未被访问过
            DFS(G, w);
        }
    }
}

存在的问题:
若图是非连通图,DFS函数只能遍历完起始顶点所在的连通分量,无法遍历完所有顶点。
在这里插入图片描述
需要在调用完DFS函数后,再次检查visited数组,看是否还有顶点未被访问,若尚有顶点未被访问过,则另选一个未曾被访问过的顶点作为起始点,再次调用DFS函数,直到图中所有顶点都被访问到为止。
最终伪代码:

bool visited[MAX_VERTEX_NUM]; // 访问标记数组

// 图的深度优先遍历
void DFS_traverse(Graph G) { // 对图G进行深度优先遍历
     // 初始化visited数组
    for(int i = 0;i < G.vexnum;i ++) {
        visited[i] = false;
    }
    // 遍历顶点
    for(int i = 0;i < G.vexnum; i ++) {
        if(!visited[i]) { // 若当前顶点未被访问过
            DFS(G, i); // 从当前顶点开始深度优先遍历图G
        }
    }
}

void DFS(Graph G, int v) { // 从顶点v出发,深度优先遍历图G
    visit(v); // 访问顶点v
    visited[v] = true; // 标记v已被访问
    // 通过顶点v找到与之相邻的其他顶点
    for(w = FirstNeighbor(G, v);w >= 0;w = NextNeighbor(G, v, w)) {
        // 检查v的所有邻接顶点是否被访问过
        if(!visited[w]) { // 若当前顶点未被访问过
            DFS(G, w);
        }
    }
}

对于无向图,调用DFS函数的次数=图的连通分量数

复杂度

空间复杂度:O(|V|)。

时间复杂度:

  • 邻接矩阵:O(|V|2)。
  • 邻接表:O(|V|+|E|)。

例题

在这里插入图片描述
分析:在二维数组grid中,将值为’1’的元素看作是顶点,将竖向或横向的连续两个’1’看作是一条边,问题实际上就是求图grid中的连通分量的个数。遍历整个二维数组,当遇到’0’时,表示此处无顶点,或该顶点已被访问过;当遇到’1’时,表示该顶点还未被访问,从该顶点开始进行广/深度优先搜索,被访问过的顶点就将其值改为’0’表示已被访问过,直到遍历完整个二维数组,岛屿的数量就是调用广/深度优先搜索函数的次数。

广度优先搜索解法

class Solution {
public:
    // 从顶点[i][j]开始广度优先搜索
    void bfs(vector<vector<char>>& grid, int a ,int b) {
        int m = grid.size(); // 行数
        int n = grid[0].size(); // 列数
        queue<pair<int, int>> que; // 定义一个辅助队列
        // 访问初始顶点
        grid[a][b] = '0'; // 将顶点值设为'0'表示该顶点已被访问过
        que.push({a, b}); // 将初始顶点入队列
        while(!que.empty()) { // 当队列不为空时进入循环
            // 取出队头顶点
            int i = que.front().first;
            int j = que.front().second;
            que.pop();
            // 依次将其邻接的未被访问的顶点访问并入队列
            if(i-1 >= 0 && grid[i-1][j] == '1') {
                grid[i-1][j] = '0';
                que.push({i-1, j});
            }
            if(i+1 < m && grid[i+1][j] == '1') {
                grid[i+1][j] = '0';
                que.push({i+1, j});
            }
            if(j-1 >= 0 && grid[i][j-1] == '1') {
                grid[i][j-1] = '0';
                que.push({i, j-1});
            }
            if(j+1 < n && grid[i][j+1] == '1') {
                grid[i][j+1] = '0';
                que.push({i, j+1});
            } 
        }
    }

    int numIslands(vector<vector<char>>& grid) {
        int m = grid.size(); // 行数
        if(m == 0) return 0;
        int n = grid[0].size(); // 列数
        int count = 0; // 记录调用dfs函数的次数

        // 遍历整个二维数组
        for(int i = 0;i < m;i ++) {
            for(int j = 0;j < n;j ++) {
                // 当顶点值为'1'时表示该顶点未被访问过
                if(grid[i][j] == '1') {
                    // 从该顶点开始广度优先地访问其所在的连通分量
                    bfs(grid, i, j);
                    count ++;
                }
            }
        }

        return count;
    }
};

深度优先搜索解法

class Solution {
public:
    // 从顶点[i][j]开始深度优先搜索
    void dfs(vector<vector<char>>& grid, int i ,int j) {
        int m = grid.size(); // 行数
        int n = grid[0].size(); // 列数
        // 访问初始顶点
        grid[i][j] = '0'; // 将顶点值设为'0'表示该顶点已被访问过
        // 按照上下左右的顺序递归的调用dfs函数访问其邻接顶点
        if(i-1 >= 0 && grid[i-1][j] == '1') dfs(grid, i-1, j);
        if(i+1 < m && grid[i+1][j] == '1') dfs(grid, i+1, j);
        if(j-1 >= 0 && grid[i][j-1] == '1') dfs(grid, i, j-1);
        if(j+1 < n && grid[i][j+1] == '1') dfs(grid, i, j+1);
    }

    int numIslands(vector<vector<char>>& grid) {
        int m = grid.size(); // 行数
        if(m == 0) return 0;
        int n = grid[0].size(); // 列数
        int count = 0; // 记录调用dfs函数的次数

        // 遍历整个二维数组
        for(int i = 0;i < m;i ++) {
            for(int j = 0;j < n;j ++) {
                // 当顶点值为'1'时表示该顶点未被访问过
                if(grid[i][j] == '1') {
                    // 从该顶点开始深度优先地访问其所在的连通分量
                    dfs(grid, i, j);
                    count ++;
                }
            }
        }

        return count;
    }
};

代码中访问顶点四个方向的邻接顶点可以优化为使用两个数组+for循环实现:

class Solution {
public:
    // 从顶点[i][j]开始深度优先搜索
    void dfs(vector<vector<char>>& grid, int i ,int j) {
        int m = grid.size(); // 行数
        int n = grid[0].size(); // 列数
        if(i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') return; // 出界或等于'0'
        // 访问初始顶点
        grid[i][j] = '0'; // 将顶点值设为'0'表示该顶点已被访问过
        // 递归的调用dfs函数访问其邻接顶点
        int di[4] = {0, 0, 1, -1};
        int dj[4] = {1, -1, 0, 0};
        for(int index = 0;index < 4;index ++) {
            dfs(grid, i+di[index], j+dj[index]);
        }
    }

    int numIslands(vector<vector<char>>& grid) {
        int m = grid.size(); // 行数
        if(m == 0) return 0;
        int n = grid[0].size(); // 列数
        int count = 0; // 记录调用dfs函数的次数
        // 遍历整个二维数组
        for(int i = 0;i < m;i ++) {
            for(int j = 0;j < n;j ++) {
                // 当顶点值为'1'时表示该顶点未被访问过
                if(grid[i][j] == '1') {
                    // 从该顶点开始深度优先地访问其所在的连通分量
                    dfs(grid, i, j);
                    count ++;
                }
            }
        }

        return count;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值