首页 > 其他分享 >数据结构——图

数据结构——图

时间:2023-10-18 20:01:34浏览次数:28  
标签:dist int 路径 edge 顶点 srcsub 数据结构


图的基本概念

图是由顶点集合及顶点间的关系组成的一种数据结构:G=(V,E)

其中:顶点集合V,边集合E

V={x|x属于某个数据对象集}

E={(x,y)|x,y属于V}

(x,y)表示点x到点y的一条双向通路,是无方向的

数据结构——图_邻接矩阵


顶点:图中的节点,第几个顶点记作vi

两个顶点vi和vj相关联称为顶点vi到顶点vj之间的一条边

图分为有向图和无向图

在有向图中,顶点对<x,y>是有序的,顶点对<x,y>称为顶点x到y的一条边<x,y><y,x>是两条不同的边。

在无向图中,顶点对(x,y)称为顶点x和顶点y相关联的一条边,边是没有方向的,(x,y)(y,x)是同一条边。

完全图:在无向图的基础上,任意两个顶点之间有且仅有一条边——有n*(n-1)/2条边,则称此图为无向图。 在有向图中,任意两个顶点之间有且仅有方向相反的边——n*(n-1),此图称为有向完全图

邻接顶点:在无向图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的度是指与它相关联的边的条数,记作dev(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都存在一条路径,同时vj到vi也存储一条路径,则称此图是强连通图。

生成树:一个连通图的最小连通子图称为该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。


图的存储结构

图要怎么进行存储呢?
对于节点只需要连续的空间即可,对于边的存储,有下面几种方法。

  1. 用矩阵存储——邻接矩阵
  2. 用链表存储——邻接表

邻接矩阵

邻接矩阵就是用一个二维数组存储节点和节点之间的关系在无向图中,邻接矩阵是对称的 如果边带有权值,并且两个节点之间是连通的,边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替 用邻接矩阵存储可以快速只读两个顶点之间的关系,但是当顶点比较多的时候、边比较少的时候,不仅浪费空间,而且无法快速知道一个顶点和多少个边相连


实现:

用数组存储点
用二维数组存储边
用map进行转换——把其他类型的数据与下标进行对应

成员函数及其构造函数
//V点,W边的权值
template<class V, class W,W MAX_W=INT_MAX,bool Direction=false>
class Graph
{
    vector<V> _vertex;
    map<V, int> _vertextosub;
    vector<vector<W>> _edge;
public:
    Graph(const V* str,int size)
    {
        //把顶点和坐标进行对应
        _vertex.resize(size);
        for (int i = 0; i < size; i++)
        {
            _vertex[i] = str[i];
            _vertextosub[str[i]] = i;
        }
        //边都初始为最大,表示不相连
        _edge.resize(size);
        for (auto& x : _edge)
            x.resize(size, MAX_W);
    }
    int Getvertexsub(const V& x)
    {
        //找到返回,找不到返回-1
        auto iterator = _vertextosub.find(x);

        if (iterator == _vertextosub.end())
            return -1;
        return iterator->second;
    }
    void AddEdge(const V& src,const V& dst,const W& w)
    {
        //找到顶点的下标
        int srcsub = Getvertexsub(src);
        int detsub = Getvertexsub(dst);
        if (srcsub == -1 && detsub == -1)
        {
            perror("顶点获取下标失败");
            exit(-1);
        }
        //成功进行添加权值
        _edge[srcsub][detsub] = w;
        if (Direction == false)
            _edge[detsub][srcsub] = w;
    }
};


邻接表

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


实现

template<class W>
struct Edge//边的结构
{
    int _scr;//起点
    int _dst;//终点
    W _w;//权值
    Edge* _next;
    Edge(int scr, int dst, const W& w)
        :_scr(scr),
        _dst(dst),
        _w(w),
        _next(nullptr)
    {}

};
template<class V, class W, bool Direction =false >
class Graph
{
    typedef Edge<W> Edge;
    vector<V> _vertex;
    map<V, int> _vertextosub;
    vector<Edge*> _edge;//边
public:
    Graph(const V* str,int size )
    {
        _vertex.resize(size);
        for (int i = 0; i < size; i++)
        {
            _vertex[i] = str[i];
            _vertextosub[str[i]] = i;
        }
        _edge.resize(size, nullptr);
    }
    int Getvertexsub(const V& x)
    {
        //找到返回,找不到返回-1
        auto iterator = _vertextosub.find(x);

        if (iterator == _vertextosub.end())
            return -1;
        return iterator->second;
    }
    void AddEdge(const V& src, const V& dst, const W& w)
    {
        //找到顶点的下标
        int srcsub = Getvertexsub(src);
        int detsub = Getvertexsub(dst);
        if (srcsub == -1 && detsub == -1)
        {
            perror("顶点获取下标失败");
            exit(-1);
        }
        //成功进行添加权值
        Edge* e = new Edge(srcsub, detsub, w);
        //头插
        e->_next = _edge[srcsub];
        _edge[srcsub] = e;
        if (Direction == false)
        {
            e = new Edge(detsub, srcsub, w);
            e->_next = _edge[detsub];
            _edge[detsub] = e;
        }
    }
}


两者的优缺点

邻接矩阵:

邻接矩阵的方式非常适合稠密图邻接矩阵可以在O(1)的基础上判断两个顶点的连接关系,并取得权值 相对而言不适合查找一个顶点连接的所有边——O(N)

邻接表:

适合存储稀疏图适合查找一个顶点连接出去的所有边 不适合确定两个顶点是否相连及权值


图的遍历

这里我们用邻接矩阵来进行深度和广度搜索

BFS(广度)
广度遍历相当于一层一层的进行遍历(根据节点遍历它所在该行的二维数组),那么我们就要用到队列,但是我们在遍历的时候要注意不要形成环,所以遍历的时候要注意标记,标记已经遍历的节点

void BFS(const V& src)
{
    int srcsub = Getvertexsub(src);
    if (srcsub < 0)
    {
        perror("不存在该节点");
        exit(-1);
    }
    queue<int> q;//存每层的节点下标
    vector<bool> v(_vertex.size(),false);//存节点是不是被访问过了
    //遍历二维数组
    q.push(srcsub);
    v[srcsub] = true;
    int level = 1;
    while (!q.empty())
    {
        for (int x = 1; x <= level; x++)
        {
            int i = q.front();
            cout << _vertex[i] << "->" << i << ';';
            q.pop();

            for (int j = 0; j < _edge.size(); j++)
            {
                if (_edge[i][j] != MAX_W && v[j] != true)
                {
                    q.push(j);
                    v[j] = true;
                }
            }
            
        }
        level = q.size();
        cout << endl;
    }
    cout << endl;
    for (int i = 0; i < v.size(); i++)
    {
        if (v[i] == false)
            cout << _vertex[i] << "->" << i << endl;
    }

}

DFS(深度)
深度遍历也是有肯存在环路的问题,所以也要进行节点的标记

void _DFS(const V& src,vector<bool>& v)
{
    int srcsub = Getvertexsub(src);
    if (srcsub < 0)
    {
        perror("不存在该节点");
        exit(-1);
    }
    cout << _vertex[srcsub] << "->" << srcsub << endl;
    v[srcsub] = true;
    for (int i = 0; i < _edge.size(); i++)
    {
        if (_edge[srcsub][i] != MAX_W && v[i] != true)
            _DFS(_vertex[i],v);
    }
}
void DFS(const V& src)
{
    vector<bool> v(_vertex.size(), false);

    _DFS(src, v);

}


最小生成树

连通图中的每一棵生成树,都是原图的一个极大无环子图 。即:从其中删除任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。
若连通图由n个顶点组成,则其生成树必含有n个顶点和n-1条边。
构成最小生成树的准则有三条:

  1. 只能使用图中的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接图中的n个顶点
  3. 选用的n-1条边不能构成回路


Kruskal算法

从所有边中,优先选取权值最小的那个边,注意选取的时候要注意不要形成环

W Kruskal(Self& mintree)
{
    //首先完成最小生成树的构造
    int n = _vertex.size();
    mintree._vertex = _vertex;
    mintree._vertextosub = _vertextosub;
    mintree._edge.resize(n);
    for (int i = 0; i < n; i++)
        mintree._edge[i].resize(n, MAX_W);
    
    //用优先级队列获得权值的大小排列——小到大
    priority_queue<edge,vector<edge>,greater<edge>> pq;
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            //i<j对于无向图来说,不需要保存重复的数据
            //对于有向图来说,不需要遍历下面一半数据
            //因为i表示的是节点的起点,j为节点的终点
            if (i < j && _edge[i][j] != MAX_W)
            {
                pq.push(edge(i, j, _edge[i][j]));
            }
        }
    }
    //构造最小生成树
    W minw = W();
    int Size = 0;
    //并查集
    UnionSet Uniset(n);
    Edge<W> t;
    while (!pq.empty())
    {
        t = pq.top();
        int scr = t._scr;
        int dst = t._dst;
        W w = t._w;
        if (Uniset.Find(scr)!=Uniset.Find(dst))
        {
            cout << _vertex[scr] << "->" << _vertex[dst] << ":" << w << endl;
            Uniset.Union(scr, dst);
            mintree._edge[scr][dst] = w;
            Size++;
            minw += w;
        }
        pq.pop();
        
    }
    //最小生成树的边数一定是n-1条
    if (Size != n - 1)
        return W();


    return minw;
}


Prim算法

该算法的实现是:
把这些节点分成两部分,
一部分是生成树的节点——记作X集合
另一部分是出去除去生成树的节点的部分——记作Y集合
从X中找与Y连接中最短的边,然后把Y中的该点加入到X集合中,从Y集合中去除该节点

在下面我们实现的时候,没有用Y集合,因为完全没有必要

W Prim(Self& mintree,const V& src)
{
    int n = _vertex.size();//有多少节点
    mintree._vertex = _vertex;
    mintree._vertextosub = _vertextosub;
    mintree._edge.clear();
    mintree._edge.resize(n);
    for (int i = 0; i < n; i++)
        mintree._edge[i].resize(n, MAX_W);

    vector<bool> X(n, false);

    int srcsub = Getvertexsub(src);//起点下标
    int dstsub;//终点下标
    W w = W();//权值
    W minw = W();
    X[srcsub] = true;
    int size = 0;//边的数
    priority_queue<edge, vector<edge>, greater<edge>> pq;
    for (int i = 0; i < n; i++)
    {
        if (_edge[srcsub][i] != MAX_W)
            pq.push(edge(srcsub, i, _edge[srcsub][i]));
    }
    
    while (!pq.empty())
    {
        edge e = pq.top();
        srcsub = e._scr;
        dstsub = e._dst;
        w = e._w;
        pq.pop();
        //这个最小的边是终点是没有被选取的
        if (X[dstsub] == true)
            continue;
        //连接该边
        mintree._edge[srcsub][dstsub] = w;
        X[dstsub] = true;
        minw += w;
        size++;
        for (int i = 0; i < n; i++)
        {
            if (_edge[dstsub][i] != MAX_W&&X[i]==false)
                pq.push(edge(dstsub, i, _edge[dstsub][i]));
        }
    }
    if (size == n - 1)
        return minw;
    else
        return W();
}


最短路径问题

最短路径问题:从在带权有向图G中的某一顶点出发,找出一条通往另一个顶点的最短路径,最短也就是沿路径各边的权值总和达到最小

单源最短路径算法

确定起点的最短路径问题——单源最短路径

Dijkstra算法

该算法只能解决非负权值的问题——权值不是负数

刚开始的时候,到所有点的路径和都是无穷大,表示无法到达。对于起始点,到该点的最短路径为0。然后去更新与之相连的其他节点的路径。 下一次选取到其他节点最短的路径为下一个路过的节点,同时更新它所连接的节点的路径,如果比之前的小,就进行更新,否则不进行更新。反复此过程,直到所有的节点的访问完,最短路径也就出来了。

void Dijkstra(const V& src,vector<int>& dist,vector<int>& path)
{
    int n = _vertex.size();
    //初始化
    dist.resize(n, MAX_W);
    path.resize(n, -1);
    //节点是否访问过
    vector<bool> v(n, false);
    //起点初始化
    int srcsub = Getvertexsub(src);
    dist[srcsub] = 0;//最小路径的权值
    path[srcsub] = srcsub;
    for (int j = 0; j < n; j++)
    {
        //对起点的相连的边进行松弛操作
        for (int i = 0; i < n; i++)
        {
            if (_edge[srcsub][i] != MAX_W && v[i] == false
                && _edge[srcsub][i] + dist[srcsub] < dist[i])
            {
                dist[i] = _edge[srcsub][i] + dist[srcsub];
                path[i] = srcsub;
            }
        }
        //获取下一个最小的路径起点
        W minsub = MAX_W;
        for (int i = 0; i < n; i++)
        {
            if (v[i] == false && dist[i] < minsub)
            {
                srcsub = i;
                minsub = dist[i];
            }
        }
        v[srcsub] = true;
    }
}


Bellman-Ford算法

该算法就是解决Dijkstra不能解决负权值的问题——暴力求解的方法
该算法是怎么工作的呢?

s->i是我们求的是s经过多个点到i的路径权值i->j表示的所有路径 当我们更新出s->i的路径的时候,同时更新用i->j对所有路径进行更新操作(松弛操作) 这样就能得出s->j的路径权值——这种求出的方法是s->i->j的最短,但是如果出现k(k) s->k的路径权值是由s->i求出的。那么我们还需要再次从新开始遍历——更新路径,每次更新会更新一条,最坏的情况是更新n条,就是n次。 要注意负权回路的问题,该算法也能检查负权回路

bool BellmanFord(const V& src,vector<int>& dist,vector<int>& path)
{
    int n = _vertex.size();
    //初始化
    dist.resize(n, MAX_W);
    path.resize(n, -1);
    //起点初始化
    int srcsub = Getvertexsub(src);
    dist[srcsub] = 0;//最小路径的权值
    path[srcsub] = srcsub;
    bool flag = true;
    for (int k = 0; k < n; k++)
    {
        flag = true;//判断是否全部更新完成
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                //i->j的路径
                //dist[i]表示s->i的路径权值,松弛操作进行更新其他边
                if (_edge[i][j] != MAX_W&&dist[i]+ _edge[i][j]<dist[j])
                {
                    dist[j] = dist[i] + _edge[i][j];
                    path[j] = i;
                    flag = false;
                }
            }
        }
        if (flag)
            break;
    }
    //判断是否有负权回路
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            if (_edge[i][j] != MAX_W && dist[i] + _edge[i][j] < dist[j])
            {
                return false;
            }
        }
    }
    return true;
}


多元最短路径算法——Floyd-Warshall算法

全局最短路径问题——多源最短路径,求图中所有的最短路径
该算法用的是动态规划的思想,求的是任意两点之间的权值
具体求法:

假设我们求的是i->j那么可能存在节点k,使得i->k->j 所有i->j之间的路径可以用i->k加上k->j来求 Dij=min(Dik+Dkj,Dij) 那么路径和权值都应该用二维进行存储(压缩空间之后)

void FloydWarshall(vector<vector<int>>& dist,vector<vector<int>>& path)
{
    //初始化
    int n = _vertex.size();
    dist.resize(n);
    path.resize(n);
    for (int i = 0; i < n; i++)
    {
        dist[i].resize(n, MAX_W);
        path[i].resize(n, -1);
        for (int j = 0; j < n; j++)
        {
            if (_edge[i][j] != MAX_W)
            {
                dist[i][j] = _edge[i][j];
                path[i][j] = i;
            }
            if (i == j)
            {
                dist[i][j] = 0;
            }
        }
    }
    //依次用k作为中间转换
    for (int k = 0; k < n; k++)
    {
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {

                if (dist[i][k] != MAX_W && dist[k][j] != MAX_W
                    && dist[i][k] + dist[k][j] < dist[i][j])
                {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    //path[i][j]存的是j对应的父节点,而此时父节点应该存储的“k”
                    //该“k”存储在path中
                    path[i][j] = path[k][j];
                }
            }
        }
    }

}

标签:dist,int,路径,edge,顶点,srcsub,数据结构
From: https://blog.51cto.com/u_15869810/7922510

相关文章

  • 小白学算法-什么是矩阵数据结构以及有哪些应用?
    什么是矩阵数据结构以及有哪些应用矩阵表示按行和列的顺序排列的数字的集合。必须将矩阵的元素括在圆括号或方括号中。例如:具有9个元素的矩阵如下所示。该矩阵M有3行和3列。矩阵M的每个元素都可以通过其行号和列号来引用。例如,M[2][3]=6。矩阵是由行和列组成的二维数组。......
  • 3.1-Pandas数据结构
    3.1-Pandas数据结构  3.1.1认识Pandas库¶基于Numpy的一种工具,为解决数据分析任务而创建的,纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具基本上你能用Excel或者Bi工具进行的数据处理,Pandas也都能实现,而且更快 In [ ]:......
  • 【数据结构】7.平衡搜索树(AVL树和红黑树)
    0.概述对于普通的搜索树,如果一直插入比第一个元素小的元素,它会退化成一个无限向左下角眼神的单链表,使得时间复杂度退化为O(n)。如果我们在插入时保持树的结构是平衡的,则可以保证查找、插入和删除的时间复杂度有对数级的时间性能,下面讲到的AVL树和红黑树都是平衡搜索树,通过旋......
  • 1数据结构
    数据结构数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。基本概念数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据元素:是组成数据的、有一定意义的基本单位,在计算......
  • C#学习笔记--数据结构、泛型、委托事件等进阶知识点
    C#进阶简单数据结构类ArrayList元素类型以Object类型存储,支持增删查改的数组容器。因而存在装箱拆箱操作,谨慎使用。//ArrayListArrayListarray=newArrayList();//增=================array.Add("Hello");array.Add(true);array.Add("Tony");//添加单个元素array.Add(......
  • 十天学完基础数据结构-第四天(链表(Linked List))
    链表的基本概念链表是一种线性数据结构,与数组不同,链表的元素(节点)之间通过指针相互连接。链表有以下基本概念:节点:链表中的每个数据项称为节点,每个节点包含数据和一个指向下一个节点的指针。头节点:链表的第一个节点称为头节点,它通常用来表示整个链表的起始位置。尾节点:链表的最后一个......
  • 数据结构
    数据结构数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。基本概念数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据元素:是组成数据的、有一定意义的基本单位,在计算......
  • 2-快速上手——从0到1掌握算法面试需要的数据结构(一)
    数据结构层面,大家需要掌握以下几种:数组栈队列链表树(这里我们着重讲二叉树)对于这些数据结构,各位如果没有大量的可支配时间可以投入,那么其实不建议找厚厚的大学教材来刷。此时此刻,时间为王,我们追求的是效率的最大化。不同的数据结构教材,对数据结构有着不同的划分、不同的解......
  • 数据结构与算法 | 数组(Array)
    数组(Array)数组(Array)应该是最基础的数据结构之一,它由相同类型的元素组成的集合,并按照一定的顺序存储在内存中。每个元素都有一个唯一的索引,可以用于访问该元素。 //java数组示例 int[]numbers1={2,0,2,3,9,23}; //或者 int[]numbers2=newint[6];基本概念数组基......
  • 数据结构之拓扑序列
    例题展示例题解决拓扑排序指的是从一个入度为0的点开始,将这个点记录下来,同时将这个点以及这个点的出度的线去除,再找入度为0的点,直到将所有的顶点遍历完成。故而,上述例题中的拓扑排序序列为01243567012436570214356702143657四种。......