数据结构(五):图

一、图的概念

        图由结点的有穷集合V和边的集合E组成。其中,结点也称为顶点。一对结点(x, y)称为边(edge),表示顶点x连接到顶点y。边可以包含权重/成本,显示从顶点x到y所需的成本。若两个顶点之前存在一条边,就表示这两个顶点具有相邻关系。

二、图的遍历

        在图中,需要注意一般都需要通过一个哈希表visited来记录所有节点是否已经遍历过,防止进入死循环。顺便记录下类的学习,在定义类时,需要给类定义初始化函数,再定义类的属性和方法;在应用类时,需要先初始化类,再引用类的属性和方法去解决实际问题。

深度优先遍历(DFS)

  • 从图中的一个起始节点开始,沿着一条路径尽可能深地访问,直到到达最深的节点,然后再回溯到之前未访问的节点。
  • 通常使用递归或栈来实现,适用于查找连通分量、拓扑排序和解决迷宫等问题。
  • 在遍历过程中,深度优先搜索会优先探索最深的分支,直到达到叶子节点,然后再回溯到其他分支。

广度优先遍历(BFS)

  • 从图中的一个起始节点开始,先访问起始节点的所有邻居节点,然后依次访问邻居节点的邻居节点,依次类推。
  • 通常使用队列来实现,适用于查找最短路径、最小生成树和解决网络分析等问题。
  • 在遍历过程中,广度优先搜索会先探索当前节点的所有相邻节点,然后再向外扩展到下一层相邻节点。
#include<iostream>
#include<queue>
#include<stack>
#include<vector>
#include<unordered_set>
using namespace std;

# 定义图节点结构
struct GraphNode
{
    int val;
    vector<GraphNode*> neighbor;
    GraphNode(int value):val(value){}
};

# 定义图
class Graph
{
private:
public:
    # 定义图的节点集合
    vector<GraphNode*> nodes;
    # 添加节点(输入为数组)
    void add_nodes(vector<int> values)
    {
        for(auto val:values)
        {   GraphNode* node=new GraphNode(val);
            nodes.push_back(node);
        }
    }
    # 添加边(输入为两个顶点)
    void add_edges(GraphNode* node1,GraphNode* node2)
    {
        node1->neighbor.push_back(node2);
        node2->neighbor.push_back(node1);
    }
    # 深度优先遍历的递归方法
    void DFS_recursion(GraphNode* cur,unordered_set<GraphNode*>& visited)
    {
        if(cur==NULL||visited.find(cur)!=visited.end())return;
        visited.insert(cur);
        cout<<cur->val<<endl;
        for(auto neighbor_node:cur->neighbor)
        {
            DFS_recursion(neighbor_node,visited);
        }
    }
    # 深度优先遍历的非递归方法
    void DFS_not_recursion(GraphNode* root)
    {
        stack<GraphNode*> st;
        unordered_set<GraphNode*> visited;
        GraphNode* cur;
        st.push(root);
        while(!st.empty())
        {
            cur=st.top();
            cout<<cur->val<<endl;
            st.pop();
            visited.insert(cur);
            for(auto neighbor_node:cur->neighbor)
            {
                if(visited.find(neighbor_node)==visited.end())st.push(neighbor_node);
            }
        }
    }
    # 广度优先遍历的非递归方法
    void BFS(GraphNode* root)
    {
        GraphNode* cur;
        unordered_set<GraphNode*> visited;
        queue<GraphNode*> que;
        que.push(root);
        while(!que.empty())
        {
            cur=que.front();
            cout<<cur->val<<endl;
            que.pop();
            visited.insert(cur);
            for(auto neighbor_node:cur->neighbor)
            {
                if(visited.find(neighbor_node)==visited.end())que.push(neighbor_node);
            }
        }
    }
};

int main()
{
    Graph graph;
    vector<int> values={0,1,2,3,4,5};
    graph.add_nodes(values);
    
    GraphNode* node_0=graph.nodes[0];
    GraphNode* node_1=graph.nodes[1];
    GraphNode* node_2=graph.nodes[2];
    GraphNode* node_3=graph.nodes[3];
    GraphNode* node_4=graph.nodes[4];
    GraphNode* node_5=graph.nodes[5];

    graph.add_edges(node_0,node_1);
    graph.add_edges(node_0,node_2);
    graph.add_edges(node_1,node_3);
    graph.add_edges(node_1,node_4);
    graph.add_edges(node_4,node_5);
    

    unordered_set<GraphNode*> visited;
    graph.DFS_recursion(node_0,visited);
    graph.DFS_not_recursion(node_0);
    graph.BFS(node_0);
}

三、图的应用

3.1 判断图是否为树

        判断一个图是否为树的条件是:1)该图是否存在环;2)该图是否连通。

#include<iostream>
#include<unordered_set>
#include<vector>
using namespace std;

struct GraphNode{
    int val;
    vector<GraphNode*> neighbor;
    GraphNode(int value):val(value){}
};

class Graph{
public:
    vector<GraphNode*> nodes;
    
    void add_nodes(int value)
    {
        GraphNode* graphnode=new GraphNode(value);
        nodes.push_back(graphnode);
    }

    void add_edges(GraphNode* node1,GraphNode* node2)
    {
        node1->neighbor.push_back(node2);
        node2->neighbor.push_back(node1);
    }

};

// 通过深度优先遍历判断图是否存在环 图是树的条件为:1.没有环 2.连通图
bool isTree(GraphNode* curNode,GraphNode* parentNode,unordered_set<GraphNode*>& visited)
{
    // 若访问到已访问的节点,则该图存在环,则该图不是树,返回false
    if(visited.find(curNode)!=visited.end())return false;
    // visited记录已访问的节点
    visited.insert(curNode);
    for(auto next:curNode->neighbor)
    {
        // 继续遍历邻居节点
        if(!isTree(next,curNode,visited)&&next!=parentNode)return false;
    }
    return true;
}


int main()
{
    Graph graph;
    graph.add_nodes(0);
    graph.add_nodes(1);
    graph.add_nodes(2);
    graph.add_nodes(3);
    graph.add_nodes(4);
    graph.add_nodes(5);
    graph.add_nodes(6);
    graph.add_nodes(7);
    
    

    GraphNode* node0=graph.nodes[0];
    GraphNode* node1=graph.nodes[1];
    GraphNode* node2=graph.nodes[2];
    GraphNode* node3=graph.nodes[3];
    GraphNode* node4=graph.nodes[4];
    GraphNode* node5=graph.nodes[5];
    GraphNode* node6=graph.nodes[6];
    GraphNode* node7=graph.nodes[7];
    
    graph.add_edges(node0,node1);
    graph.add_edges(node0,node2);
    graph.add_edges(node1,node3);
    graph.add_edges(node1,node4);
    graph.add_edges(node1,node5);
    graph.add_edges(node1,node6);
    graph.add_edges(node1,node7);

    unordered_set<GraphNode*> visited;
    bool flag=true;
    if(isTree(node0,nullptr,visited))
    {
        // 判断所有节点是否都已遍历 判断该图是否连通
        for(auto node:graph.nodes)
        {
            if(visited.find(node)==visited.end())
            {
                flag=false;
                break;
            }
        }
        if(flag==true)cout<<"This is a tree.";
        else cout<<"This is not a tree.";
    }
    else cout<<"This is not a tree.";
}
3.2 计算图的边数

        通过广度优先遍历来计算图的边数。

#include<iostream>
#include<unordered_set>
#include<vector>
using namespace std;

struct GraphNode{
    int val;
    vector<GraphNode*> neighbor;
    GraphNode(int value):val(value){}
};

class Graph{
public:
    vector<GraphNode*> nodes;
    
    void add_nodes(int value)
    {
        GraphNode* graphnode=new GraphNode(value);
        nodes.push_back(graphnode);
    }

    void add_edges(GraphNode* node1,GraphNode* node2)
    {
        node1->neighbor.push_back(node2);
        node2->neighbor.push_back(node1);
    }

    int count_edges()
    {
        // count记录当前边数
        int count=0;
        // visited记录已访问节点
        unordered_set<GraphNode*> visited;
        for(auto node:nodes)
        {
            for(auto neigh:node->neighbor)
            {
                // 若邻居节点未访问过 则边+1
                if(visited.find(neigh)==visited.end())count++;
            }
            // 将当前节点标记为已访问
            visited.insert(node);
        }
        return count;
    }

};


int main()
{
    Graph graph;
    graph.add_nodes(0);
    graph.add_nodes(1);
    graph.add_nodes(2);
    graph.add_nodes(3);
    graph.add_nodes(4);
    graph.add_nodes(5);
    graph.add_nodes(6);
    graph.add_nodes(7);
    graph.add_nodes(8);
    graph.add_nodes(9);
    
    GraphNode* node0=graph.nodes[0];
    GraphNode* node1=graph.nodes[1];
    GraphNode* node2=graph.nodes[2];
    GraphNode* node3=graph.nodes[3];
    GraphNode* node4=graph.nodes[4];
    GraphNode* node5=graph.nodes[5];
    GraphNode* node6=graph.nodes[6];
    GraphNode* node7=graph.nodes[7];
    GraphNode* node8=graph.nodes[8];
    GraphNode* node9=graph.nodes[9];
    
    graph.add_edges(node0,node1);
    graph.add_edges(node0,node2);
    graph.add_edges(node0,node3);
    graph.add_edges(node0,node4);
    graph.add_edges(node1,node5);
    graph.add_edges(node1,node6);
    graph.add_edges(node1,node7);
    graph.add_edges(node2,node8);
    graph.add_edges(node2,node9);
    graph.add_edges(node5,node6);
    
    cout<<"The number of edges is:"<<graph.count_edges();
}
3.3 找到两个顶点的最短路径

        一般通过图的广度优先遍历来找到两个顶点的最短路径。

#include<iostream>
#include<unordered_set>
#include<unordered_map>
#include<vector>
#include<queue>
using namespace std;

struct GraphNode{
    int val;
    vector<GraphNode*> neighbors;
    GraphNode(int value):val(value){}
};

class Graph{
private:
    // 用于判断最短路径
    int MAX=1000000;
public:
    vector<GraphNode*> nodes;
    
    void add_nodes(int value)
    {
        GraphNode* graphnode=new GraphNode(value);
        nodes.push_back(graphnode);
    }

    void add_edges(GraphNode* node1,GraphNode* node2)
    {
        node1->neighbors.push_back(node2);
        node2->neighbors.push_back(node1);
    }

    // 广度优先遍历找出所有最短路径
    vector<vector<GraphNode*>> shortest_path(GraphNode* node1,GraphNode* node2)
    {
        // 用于判断最短路径的长度
        int min_length=MAX;
        // 记录所有路径
        queue<vector<GraphNode*>> path_queue;
        // 记录满足条件的结果
        vector<vector<GraphNode*>> result;
        // 记录已访问节点 
        unordered_set<GraphNode*> visited;
        path_queue.push({node1});
        while(!path_queue.empty())
        {
            // 取出当前路径
            vector<GraphNode*> curpath=path_queue.front();
            path_queue.pop();
            // 取出当前路径最后一个节点
            GraphNode* curnode=curpath[curpath.size()-1];
            // 判断当前路径最后一个节点是否为目标节点
            if(curnode==node2)
            {
                // 如果当前满足条件的路径长度小于最短路径 则清空存放结果的数组后再加入当前路径
                if(curpath.size()<min_length)
                {
                    // 清空存放结果的数组
                    result.clear();
                    result.push_back(curpath);
                    min_length=curpath.size();
                }
                // 如果当前满足条件的路径长度等于最短路径 则直接存放当前路径
                else if(curpath.size()==min_length)
                {
                    result.push_back(curpath);
                }
            }
            else
            {
                // 继续往队列中加入新的路径 即当前路径最后一个节点的邻居
                for(auto neighbor:curnode->neighbors)
                {
                    if(neighbor!=curnode&&!visited.count(neighbor))
                    {
                        curpath.push_back(neighbor);
                        path_queue.push(curpath);
                        curpath.pop_back();
                    }
                }
                visited.insert(curnode);
            }     
        }
        return result;
    }
};

int main()
{
    Graph graph;
    graph.add_nodes(0);
    graph.add_nodes(1);
    graph.add_nodes(2);
    graph.add_nodes(3);
    graph.add_nodes(4);
    
    GraphNode* node0=graph.nodes[0];
    GraphNode* node1=graph.nodes[1];
    GraphNode* node2=graph.nodes[2];
    GraphNode* node3=graph.nodes[3];
    GraphNode* node4=graph.nodes[4];
    
    graph.add_edges(node0,node1);
    graph.add_edges(node0,node2);
    graph.add_edges(node0,node4);
    graph.add_edges(node2,node3);
    graph.add_edges(node1,node3);
    graph.add_edges(node3,node4);

    vector<vector<GraphNode*>> result=graph.shortest_path(node0,node3);
    cout<<"numbers of shortest path:"<<result.size()<<endl;
    int index=1;
    for(auto vec:result)
    {
        cout<<"path"<<index++<<":";
        for(auto node:vec)
        {
            cout<<node->val<<" ";
        }
        cout<<endl;
    }
}
3.4 拓扑排序

        拓扑排序不能应用于有环图,因为我们的前提条件是总能至少在图中找到一个入度为0的顶点开始拓扑,但是当图中存在环时,我们在这个环中找不到任意一个入度为0顶的点。

        拓扑排序是利用的广度优先的思想,也是借助队列这个辅助数据结构,具体的算法流程是:首先建立一个countArr数组并统计每一个顶点的入度填到对应数组中;通过遍历,首先将所有入度为0的节点入队,并将节点总数vexCount相应减少,然后利用广度优先搜索的思路进行循环,里面注意的操作是:每当出队一个顶点时,我们将以该顶点为弧尾的弧顶顶点对应的入度countArr[i]减一,并判断该countArr[i]是否为0,为零则将该元素入队,并将vexCount减一,循环直至队列为空。最后判断vexCount是否为0,如果不为零,则说明成环了。最后输出的顶点出队顺序就是该图对应的一个拓扑序列。

#include<iostream>
#include<vector>
#include<queue>
using namespace std;

void find_order(int NumVertex,vector<vector<int>> prerequires)
{
    // CountArr记录每个顶点的入度
    vector<int> CountArr(NumVertex,0);
    // 记录每个顶点的入度
    for(auto entry:prerequires)
    {
        CountArr[entry[0]]++;
    }
    queue<int> que;
    queue<int> resque;
    for(int i=0;i<NumVertex;i++)
    {
        if(CountArr[i]==0)que.push(i);
        NumVertex--;
    }
    while(!que.empty())
    {
        int cur=que.front();
        que.pop();
        resque.push(cur);
        for(auto entry:prerequires)
        {
            if(cur==entry[1])
            {
                if(--CountArr[entry[0]]==0)
                {
                    que.push(entry[0]);
                    NumVertex--;
                }
            }
        }
    }
    while(!resque.empty())
    {
        cout<<resque.front();
        resque.pop();
    }
}

int main()
{
    find_order(4,{{1,0},{2,0},{3,1},{3,2}});
    cout<<endl;
    find_order(3,{{1,0},{2,0},{1,2}});
}
3.5 最短距离
3.5.1 迪杰斯特拉算法

        迪杰斯特拉算法是用来求解单源最短路径的算法,采用了贪心的策略,不能处理负权值。

具体过程:

①已知邻接矩阵arr,目标节点source和存放source与其他节点的最短距离的数组shortest。

②每次选择当前集合内与source距离最短的节点index:得到的一定是节点index与source之间的最短距离,因为已经是当前集合内与source距离最短的节点了,经过其他节点时距离只会更远(因此不能处理负权值的情况)。

③将节点index标记为已访问,并将index与source之间的距离加入shortest。

④得到节点index与source之间的最短距离后,计算经过节点index时,其他节点和source之间的距离有没有更短,即arr[source][j]=min(arr[source][j]与arr[source][index]+arr[index][j])。

⑤在所有节点都标记为已访问后,shortest中存放的便是source与其他所有节点之间的最短距离。

3.5.2 佛洛依德算法

        佛洛依德算法是用来求解多源最短路径的算法,采用了动态规划的策略,可以处理负权值。

①已知邻接矩阵arr,顶点数V。

②每次设定一个顶点index,计算在经过顶点index时,arr中各顶点之间的距离有没有变短。即arr[i][j]=min(arr[i][j],arr[i][index]+arr[index][j])。

③在执行V次后,矩阵arr中存放的便是顶点之间的最短距离。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值