图常见算法大全( 三种遍历算法 + 三种最短路径算法 + 两种最小生成树)


图的经典算法

完整版万字原文见史上最全详解图数据结构

一、图的遍历算法


1.void DFS(int startVertex);
2.void BFS(int startVertex);
3.void TopologicalSort();(两种实现方式)

1. DFS(深度优先搜索)


算法原理
是一种用于遍历或搜索图(包括树)中节点的算法。

其基本思想是沿着一个分支尽可能深地搜索,直到该分支无法继续扩展,再回溯到上一个分支,继续探索其他可能的路径

1


算法步骤
1. 选择一个起始节点,并访问它。

2. 对每个未被访问的邻接节点,递归地应用DFS。

3. 如果一个节点没有未被访问的邻接节点,则回溯到其父节点,继续进行其他分支的探索。

4. 直到所有节点都被访问或图的遍历完成。

代码实现

提要:

    function<void(int)> dfsVisit = [&](int vertex)

    1. 定义一个名为 dfsVisit 的递归 Lambda 函数,接受一个顶点作为参数。 

        function 是 C++ 标准库中的一个类模板,允许你存储可调用对象(如函数指针、lambda 表达式等)

        void(int) 指定这个函数不返回任何值(void),并且接受一个 int 类型的参数

    2.  这里存储的可调用对象 dfsVisit 是函数指针对象

        使用了 lambda 表达式 [&](int vertex) 来构造一个 function<void(int)>类模板的函数指针对象。
        
        lambda 表达式通过 [&] 以引用的方式捕获外部变量(如 visited 和 traversal)的引用,便于在递归中使用
void MyGraph::DFS(int startVertex)
{
   
    vector<bool> visited(n, false); // 标记数组,记录顶点是否已被访问
    vector<int> traversal;       // 用于存储遍历过程中访问的顶点(路径)

    function<void(int)> dfsVisit = [&](int vertex)
    {
   
        // 1. 选择一个起始节点,并访问它。
        visited[vertex] = true;
        traversal.push_back(vertex);

        // 2. 对每个未被访问的邻接节点,递归地应用DFS。
        for (const auto &edge : adjList[vertex])
            if (!visited[edge.first])
                dfsVisit(edge.first);

        // 3. 如果一个节点没有未被访问的邻接节点,则回溯到其父节点,继续进行其他分支的探索。
    };
    dfsVisit(startVertex);

    for (int vertex : traversal)
        cout << vertex << " ";
    cout << endl;

}

思考

这段代码的实现中用了 lamba 函数而没有用辅助函数,为什么?

比较 :使用 lamba 表达式和使用辅助函数

1. lambda 表达式

    好处在于简化代码和提高可读性

    使得递归函数 dfsVisit 可以直接在 DFS 方法内部定义,这样就能方便地捕获外部的状态(如 visited 和 traversal)而不需要额外的参数

2. 辅助函数

    需要额外的参数 dfsVisit(startVertex, visited, traversal);
//使用辅助函数版的 dfs
void MyGraph::DFS(int startVertex) 
{
   
    vector<bool> visited(n, false); // 标记数组
    vector<int> traversal; // 存储访问的顶点
    // 需要调用独立的递归成员函数
    dfsVisit(startVertex, visited, traversal);
    // 输出遍历顺序
    for (int vertex : traversal) 
        cout << vertex << " ";
    cout << endl;
}

void MyGraph::dfsVisit(int vertex, vector<bool>& visited, vector<int>& traversal) 
{
   
    visited[vertex] = true;
    traversal.push_back(vertex);
    for (const auto& edge : adjList[vertex])
        if (!visited[edge.first])
            dfsVisit(edge.first, visited, traversal); // 递归调用
}

2. BFS(宽度优先搜索)

算法原理
是一种用于遍历或搜索图的算法

它以图的一个节点为起点,首先访问该节点的所有邻接节点,然后再访问这些邻接节点的邻接节点,以此类推。简单来说,BFS 是按层次逐层展开搜索的。

算法步骤
1. 初始化:
    创建一个队列 q 用来存放待访问的节点。
    创建一个布尔数组 visited 用来标记节点是否被访问过。

2. 访问节点:
    从队列中取出一个节点,标记它为已访问,并处理它(如输出)。

3. 将该节点的所有未访问过的邻接节点加入队列中。

4. 重复2 3 步,直到队列为空,表示所有可达节点都已经被访问。
代码实现
// 从给定的起始顶点进行广度优先搜索(BFS)。
void MyGraph::BFS(int startVertex)
{
   
    // 1. 初始化
    vector<bool> visited(n, false);
    queue<int> q;

    // 2. 访问节点
    visited[startVertex] = true;
    q.push(startVertex);

    while (!q.empty())
    {
   
        int currentVertex = q.front();
        q.pop();
        cout << currentVertex << " ";
        // 3. 将该节点的所有未访问过的邻接节点加入队列中。
        for (const auto &edge : adjList[currentVertex])
            if (!visited[edge.first])
                visited[edge.first] = true, q.push(edge.first);
    }
    cout << endl;
}


3. 拓扑排序(两种实现)

问题引入

算法原理
有向无环图(DAG)

(1)有向 ———> 按先后顺序排序( u -> v,u 在 v 之前出现)
  
(2)无环 ———> 需要包含所有节点

时间复杂度 O(E + V)

方法一:Kahn算法(基于入度)

核心思想

每次选择入度为 0 的点,然后

删除这个点(将这个点加入结果序列中),删除它的出边(将与这个点连接的所有点入度减 1)


外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对其排序的结果就是:2 -> 8 -> 0 -> 3 -> 7 -> 1 -> 5 -> 6 -> 9 -> 4 -> 11 -> 10 -> 12


算法步骤
1. 计算所有节点的入度。

2. 将所有入度为 0 的节点放入队列。

3. 从队列中取出一个节点,并将其加入拓扑排序结果中。

4. 对该节点的所有邻接节点,将其入度减 1,如果某个邻接节点的入度变为0,则将其加入队列。

5. 重复以上步骤,直到队列为空。

6. 如果最后拓扑排序中的节点数小于图中的节点数,则图中存在环。

代码实现

// 拓扑排序(Kahn 算法)

vector<int> topologicalSortKahn(int n, vector<vector<int>> &edges)
{
   
    vector<int> inDegree(n, 0); // 入度数组
    vector<vector<int>> adj(n); // 邻接表
    vector<int> result; //储存排序得到的数据

    // 1. 计算所有节点的入度。
    for (const auto &edge : edges)
    {
   
        int u = edge[0], v = edge[1];
        adj[u].push_back(v);
        inDegree[v]++;
    }

    // 2. 将所有入度为 0 的节点放入队列。
    queue<int> q;
    for (int i = 0; i < n; ++i)
        if (inDegree[i] == 0)
            q.push(i);

    while (!q.empty())
    {
   
        // 3. 从队列中取出一个节点,并将其加入拓扑排序结果中。
        int node = q.front();
        q.pop();
        result.push_back(node);

        // 4. 对该节点的所有邻接节点,将其入度减 1,如果某个邻接节点的入度变为 0,则将其加入队列。
        for (int neighbor : adj[node])
        {
   
            inDegree[neighbor]--;
            if (inDegree[neighbor] == 0)
                q.push(neighbor);
        }
    }

    // 6. 如果最后拓扑排序中的节点数小于图中的节点数,则图中存在环。
    if (result.size() != n)
    {
   
        return {
   }; 
    }

    return result;
}



思考

为什么如果最后拓扑排序中的节点数小于图中的节点数,则图中存在环?

拓扑排序按 先后顺序 排序 ——> 拓扑排序中的节点数小于图中的节点数 ———> 不是所有的节点都符合排序条件 ——> 有一些节点没有先后顺序 ————> 有一些节点形成了环

方法二:深度优先搜索(DFS)

核心思想 : 利用递归调用栈的特点,保证入栈按倒序,正确的拓扑排序即为出栈顺序


算法步骤
算法步骤:

1. 对每个未被访问的节点执行 DFS,标记该节点为访问中。

2. 如果访问到某个已经在 “ 访问中 ” 状态的节点,则说明图中存在环,无法进行拓扑排序。

3. DFS 完成后,标记节点为“已访问”,并将节点加入到结果栈(或者队列)中。

4. 最终结果是栈中节点的逆序。

代码实现

bool dfs(int node, vector<vector<int>> &adj, vector<int> &visited, stack<int> &result)
{
   
    // 当前节点正在访问,图中有环
    if (visited[node] == 1)
        return false; 
    // 当前节点已经访问过
    if (visited[node] == 2)
        return true; 

    // 1. 对每个未被访问的节点执行 DFS,标记该节点为访问中。
    visited[node] = 1; 

    for (int neighbor : adj[node])
        // 2. 如果访问到某个已经在 “ 访问中 ” 状态的节点,则说明图中存在环,无法进行拓扑排序
        if (!dfs(neighbor, adj, visited, result))
            return false;
    

    // 3. DFS 完成后,标记节点为“已访问”,并将节点加入到结果栈(或者队列)中。
    visited[node] = 2; // 标记为已访问
    result.push(node); // 将节点加入栈

    return true;
}

vector<int> topologicalSortDFS(int n, vector<vector<int>> &edges)
{
   
    vector<vector<int>> adj(n);
    for (const auto &edge : edges)
        adj[edge[0]].push_back(edge[1]);

    vector<int> visited(n, 0); // 0: 未访问, 1: 正在访问, 2: 已访问
    stack<int> result;

    // 1. 对每个未被访问的节点执行 DFS,标记该节点为访问中。
    for (int i = 0; i < n; ++i)
        if (visited[i] == 0)
            if (!dfs(i, adj, visited, result))
                return {
   }; // 如果存在环,返回空

    vector<int> order;
    while (!result.empty())
    {
   
        order.push_back(result.top());
        result.pop();
    }
    return order;
}


总结
(1)Kahn算法(基于入度):通常更直观易懂,适合大规模图,对于边的增删操作更加高效,且容易处理图中的环。

(2)DFS算法:适合递归或栈的场景,能够通过递归的后序遍历得到拓扑排序,也适合用来检测环。


最短路径算法


1. Dijkstra算法(单源最短路径)(无负权边图)


算法原理
1. Dijkstra 算法通过 贪心策略 计算从一个源顶点到其他所有 顶点的最短路径。

2. 时间复杂度为 O(V^2)(未优化时)或 O((V + E) log V)(使用优先队列时)

3. 应用:适用于无负权边的图。

4. 核心思想

    (1) 选定一个点,这个点满足两个条件:a.未被选过,b.距离最短

    (2) 对于这个点的所有邻近点去尝试松弛

实现代码(未优化版本)

1


// 求最短路径
// Dijkstra 不断选择当前距离源节点最近的未处理节点来构建最短路径。
// 贪心算法
// 时间复杂度 O(V^2)
void MyGraph::Dijkstra(int startVertex)
{
   
    //代表某个顶点是否被访问过,初始化所有的顶点为 false
    vector<bool> visited(n, false);
    //dis代表源点到其它点的最短距离,numeric_limits<int>::max()代表无穷
    vector<int> distances(n, numeric_limits<int>::max()); 
    //向量 prev 用来存储路径的前驱节点,用于之后路径重建,初始化为 -1
    vector<int> prev(n, -1);

    源点到源点的距离为 0
    distances[startVertex] = 0;

    //为什么只要寻找n-1个点呢?因为当剩下一个点的时候,这个点已经没有需要松弛的邻接点了
    for (int i = 0; i < n - 1; i++)
    {
   
        //进入循环之后,一开始不知道哪个是没有被访问过且距离源点最短的
        int now_minDistance = numeric_limits<int>::max();
        int now_minVertex = -1;
        
        //使用这个循环开始寻找没有被访问过且距离源点最短距离的点
        for (int j = 0; j < n; j++)
            if (!visited[j] && distances[j] < now_minDistance)
                now_minDistance = distances[j], now_minVertex = j;
            
        //标记当前节点已访问
        visited[now_minVertex] = true;

        //对这个距离源点最短距离的点的所有邻接点进行松弛
        for (const auto &pair : adjList[now_minVertex])
        {
   
            //  松弛操作(Relaxation)
            if (distances[now_minVertex] + pair.second < distances[pair.first])
            {
   
                distances[pair.first] = distances[now_minVertex] + pair.second, prev[pair.first] = now_minVertex;
            }
        }
    }
    for (int i = 0; i < n; i++)
        if (i != startVertex)
            cout << "vertex " << i << " distance from " << startVertex << " is " << distances[i] << endl;
    
}


实现代码(优化版本)

算法原理:
利用了优先队列(通常是最小堆)

将时间复杂度降低到 O((V + E) log V)。

void MyGraph::Dijkstra_withOptimize(int startVertex)
{
   
    vector<bool> visited(n, false);                       
    vector<int> distances(n, numeric_limits<int>::max()); 
    vector<int> prev(n, -1);                              


    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;

    pq.push({
   0, startVertex});
    distances[startVertex] = 0; 

    // for(int i = 0; i < n - 1; i++){
   
    while (!pq.empty())
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值