当前位置: 代码网 > it编程>编程语言>Java > 【图形结构】图 {图的基本概念;图的存储结构:邻接矩阵,邻接表;图的遍历:BFS,DFS}

【图形结构】图 {图的基本概念;图的存储结构:邻接矩阵,邻接表;图的遍历:BFS,DFS}

2024年08月06日 Java 我要评论
【图形结构】图 {图的基本概念;图的存储结构:邻接矩阵,邻接表;图的遍历:BFS,DFS}

一、图的基本概念

图是由顶点集合及顶点间的关系组成的一种数据结构:g = (v, e),其中:

  • 顶点集合v = {x|x属于某个数据对象集}是有穷非空集合;
  • e = {(x,y)|x,y属于v}或者e = {<x, y>|x,y属于v && path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。
  • (x, y)表示x到y的一条双向通路,即(x, y)是无方向的;path(x, y)表示从x到y的一条单向通路,即path(x, y)是有方向的。

顶点和边:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>。

有向图和无向图:在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边,比如下图g3和g4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图g1和g2为无向图。注意:无向边(x, y)等于有向边<x, y>和<y, x>。

树和图:

  • 树是一种特殊的图(无向,无环,连通)
  • 图不一定是树
  • 树关注的是节点中存储的值
  • 图关注的是顶点及边的权值

在这里插入图片描述

完全图:就是任意两个顶点之间都直接相连,是最稠密的图。在有n个顶点的无向图中,若有n * (n-1)/2(等差数列)条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图,比如上图g1;在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图,比如上图g4。

邻接顶点:在无向图中g中,若(u, v)是e(g)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图g中,若<u, v>是e(g)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边<u, v>与顶点u和顶点v相关联。

顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。

路径:在图g = (v, e)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。

路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。

在这里插入图片描述

简单路径:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径。

回路:若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。

在这里插入图片描述

子图:设图g = {v, e}和图g1 = {v1,e1},若v1属于v且e1属于e,则称g1是g的子图。 顶点和边都是原图的一部分

连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。

强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图。

生成树:在无向图中,一个连通图的最小连通子图称作该图的生成树。这里的最小指的是用最少的边连通所有的顶点,有n个顶点的连通图的生成树有n个顶点和n-1条边。 如下图中,无向图g1的生成树就可以是子图1。

在这里插入图片描述


二、图的存储结构

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?

2.1 邻接矩阵

邻接矩阵:使用数组表示顶点的集合,使用矩阵(二维数组)表示边的关系。

因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一个数组将顶点保存,然后采用矩阵来表示节点与节点之间的关系。

在这里插入图片描述

注意:

  1. 无向图的邻接矩阵是对称的,第i行(列)元素个数之和,就是顶点i的度。有向图的邻接矩阵则不一定是对称的,第i行(列)元素个数之和就是顶点i 的出(入)度。
  2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替。

在这里插入图片描述


邻接矩阵的实现

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

namespace matrix
{
template <class v, class w, w w_max = int32_max, bool direction = false>
class graph{
    vector<v> _vertexs; //顶点集合
    map<v, size_t> _indexmap; //顶点映射下标
    vector<vector<w>> _matrix; //邻接矩阵

public:
    graph() = default;

    graph(const v* vertexs, size_t n)
    {
        _vertexs.reserve(n);
        for(size_t i = 0; i < n; ++i)
        {
            _vertexs.push_back(vertexs[i]);
            _indexmap.insert(make_pair(vertexs[i], i));
        }

        _matrix.resize(n);
        for(vector<w>& e : _matrix)
        {
            e.resize(n, w_max);
        }
    }

    size_t getindex(const v& vertex)
    {
        auto it = _indexmap.find(vertex);
        if(it != _indexmap.end())
        {
            return it->second;
        }
        else
        {
            throw invalid_argument("不存在的顶点");
            return uint32_max;
        }
    }

    bool addedge(const v& src, const v& des, const w& w)
    {
        size_t s = getindex(src);
        size_t d = getindex(des);

        _matrix[s][d] = w;
        if(!direction)
        _matrix[d][s] = w;
    }

    void print()
    {
        //打印顶点和下标的映射关系
        for(size_t i = 0; i < _vertexs.size(); ++i)
        {
            cout << "[" << i << "]";
            cout << " --> ";
            cout << _vertexs[i] << endl;
        }
        cout << endl;

        //打印矩阵
        cout << "  ";
        for(size_t k = 0; k < _matrix.size(); ++k)
            cout << k << " "; // 打印列号
        cout << endl;
        for(size_t i = 0; i < _matrix.size(); ++i)
        {
            cout << i << " "; // 打印行号
            for(size_t j = 0; j < _matrix[i].size(); ++j)
            {
                if(_matrix[i][j] == w_max)
                    cout << "∞ ";
                else
                    cout << _matrix[i][j] << " ";
            }
            cout << endl;
        }
    }
};
};

邻接矩阵的优缺点

邻接矩阵的优点:

  1. 适合存储稠密图。
  2. 能够快速的判断两个顶点间的连接关系,并取到权值。o(1)

邻接矩阵的缺点:

  1. 不适合存储稀疏图,因为矩阵中存储了大量的0,比较浪费空间。
  2. 不方便查找一个顶点连接的所有边,需要遍历其他所有顶点。o(n)

2.2 邻接表

邻接表:使用数组表示顶点的集合,使用链表表示边的关系。

1.无向图邻接表存储

在这里插入图片描述

注意:无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点vi对应的边链表集合中结点的数目即可。

2.有向图邻接表存储

在这里插入图片描述

注意:有向图的邻接表存储分入边表和出边表。有向图中的每条边在邻接表中只出现一次。在出边表中,与顶点vi对应的边链表所含结点的个数,就是该顶点的出度;要得到vi顶点的入度,必须检测其他所有顶点对应的边链表,看有多少顶点的dst是vi,很不方便。 可以再建立一个入边表,在入边表中与顶点vi对应的边链表所含结点的个数,就是该顶点的入度,同理入边表不方便统计出度;


邻接表的实现

namespace link_table
{
    template <class w>
    struct edgenode
    {
        size_t _dsti;    // 边的终点
        w _w;            // 边的权值
        edgenode *_next; // 节点后继指针

        edgenode(size_t dsti, w w)
            : _dsti(dsti),
              _w(w),
              _next(nullptr)
        {
        }
    };

    template <class v, class w, bool direction = false>
    class graph
    {
        typedef edgenode<w> edge;
        vector<v> _vertexs;       // 顶点集合
        map<v, size_t> _indexmap; // 顶点映射下标
        vector<edge *> _table;    // 邻接表

    public:
        graph() = default;

        graph(const v *vertexs, size_t n)
        {
            _vertexs.reserve(n);
            for (size_t i = 0; i < n; ++i)
            {
                _vertexs.push_back(vertexs[i]);
                _indexmap.insert(make_pair(vertexs[i], i));
            }

            _table.resize(n, nullptr);
        }

        size_t getindex(const v &vertex)
        {
            auto it = _indexmap.find(vertex);
            if (it != _indexmap.end())
            {
                return it->second;
            }
            else
            {
                throw invalid_argument("不存在的顶点");
                return uint32_max;
            }
        }

        bool addedge(const v &src, const v &des, const w &w)
        {
            size_t s = getindex(src);
            size_t d = getindex(des);

            edge *eg = new edge(d, w);
            eg->_next = _table[s];
            _table[s] = eg;

            if (!direction)
            {
                edge *eg = new edge(s, w);
                eg->_next = _table[d];
                _table[d] = eg;
            }
        }

        void print()
        {
            // 打印顶点和下标的映射关系
            for (size_t i = 0; i < _vertexs.size(); ++i)
            {
                cout << "[" << i << "]";
                cout << " --> ";
                cout << _vertexs[i] << endl;
            }
            cout << endl;

            // 打印邻接表
            for (size_t i = 0; i < _table.size(); ++i)
            {
                cout << "[" << i << "]" << _vertexs[i] << "->";
                edge *cur = _table[i];
                while (cur != nullptr)
                {
                    cout << "[" << cur->_dsti << "]" << _vertexs[cur->_dsti];
                    cout << ":" << cur->_w << "->";
                    cur = cur->_next;
                }
                cout << "nullptr" << endl;
            }
        }
    };
};

邻接表的优缺点

邻接表的优点:

  1. 适合存储稀疏图。
  2. 适合查找一个顶点连接的所有边。(对于有向图,指的就是所有出边)

邻接表的缺点:

  1. 不方便确定两个顶点是否相连及权值

三、图的遍历

给定一个图g和其中任意一个顶点v0,从v0出发,沿着图中各边访问图中的所有顶点,且每个顶点仅被遍历一次。"遍历"即对结点进行某种操作的意思。

3.1 广度优先遍历

图的广度优先遍历(breadth-first search,bfs)是一种图的搜索算法,用于遍历图中的节点,从起始节点开始逐层遍历,先访问离起始节点最近的节点,再访问稍远一层的节点,以此类推。bfs通常使用队列来辅助遍历。

bfs的基本思路如下:

  1. 将起始节点加入队列,并将其标记为已访问
  2. 从队列中取出一个节点,访问并将该节点的相邻节点加入队列,标记相邻节点为已访问。
  3. 重复步骤2,直到队列为空

bfs的复杂度

  • 时间复杂度:对于dfs,bfs遍历来说,时间复杂度和存储结构有关:
    1. 若采用邻接矩阵存储,时间复杂度为o(n^2); 注意不是n*e
    2. 若采用邻接链表存储,时间复杂度为o(n+e);
  • 空间复杂度为 o(v),因为需要使用队列来保存节点。(最坏情况:围成一个圈)

在这里插入图片描述

代码实现:

		void bfs(const v &start)
        {
            queue<int> que;                               // 队列
            vector<bool> visited(_vertexs.size(), false); // 标记数组

            // 将起点入队,标记
            int starti = getindex(start);
            que.push(starti);
            visited[starti] = true;
            int d = 1; // 当前层的节点数,用于控制逐层遍历

            while (!que.empty())
            {
                while (d--)
                {
                    int front = que.front();
                    que.pop();
                    cout << "[" << front << "]" << _vertexs[front] << " ";
                    for (int i = 0; i < _vertexs.size(); ++i)
                    {
                        if (_matrix[front][i] != w_max && !visited[i])
                        {
                            que.push(i);
                            visited[i] = true;
                        }
                    }
                }
                d = que.size();
                cout << endl;
            }
        }

3.2 深度优先遍历

深度优先遍历(depth first search, dfs)是一种用于遍历图或树的算法,它的主要特点是尽可能深地搜索树的分支,当节点的一个分支被完全搜索过后,它会回溯到发现该节点的上一个节点,继续搜索其他路径。这个过程一直持续到所有从源节点可达的节点都被访问为止。

在深度优先遍历中,通常使用递归或栈来实现。递归版本通常更简洁,因为它利用了函数调用的栈来模拟显式的栈操作。非递归版本则更适用于需要明确控制栈的情况,比如当递归可能导致栈溢出时。

深度优先遍历的具体步骤可以概括为:

  1. 访问初始节点,并标记为已访问。
  2. 查找初始节点的第一个未被访问的邻接点。
  3. 如果邻接点存在且未被访问,递归地进行深度优先遍历。
  4. 回溯到初始节点,并尝试访问其下一个未被访问的邻接点。
  5. 如果所有邻接点都被访问过,回溯到上一个访问过的节点,继续访问其未被访问的邻接点。
  6. 重复上述步骤,直到所有节点都被访问过。

dfs的复杂度

  • 时间复杂度:对于dfs,bfs遍历来说,时间复杂度和存储结构有关:
    1. 若采用邻接矩阵存储,时间复杂度为o(n^2); 注意不是n*e
    2. 若采用邻接链表存储,时间复杂度为o(n+e);
  • 空间复杂度为 o(v),因为递归调用会使用系统栈来保存调用信息。(最坏情况:连成一条线)

在这里插入图片描述

代码实现:

		void dfs(const v &start)
        {
            int starti = getindex(start);
            vector<bool> visited(_vertexs.size(), false);
            _dfs(starti, visited);
        }

        void _dfs(int index, vector<bool> &visited)
        {
            cout << "[" << index << "]" << _vertexs[index] << endl;
            visited[index] = true;
            for (int i = 0; i < _vertexs.size(); ++i)
            {
                if (_matrix[index][i] != w_max && !visited[i])
                {
                    _dfs(i, visited);
                }
            }
        }

下一篇:【高阶数据结构】图 {最小生成树:kruskal算法,prim算法;最短路径:dijkstra算法,bellman-ford算法,floyd算法}

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com