数据结构之图

图的存储

邻接矩阵存储

存储实现

#define MaxVertexNum 100 //结点数
typedef char VertexType;
typedef int ElemType;
typedef struct
{
    VertexType Vex[MaxVertexNum];//顶点表
    ElemType Edge[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表
    int vexnum,arcnum;//结点数  边数
}MGraph;

无向图的度

 有向图的的度

 邻接矩阵法,求结点的度/入度/出度的时间复杂度为O(|V|) 

 性能分析

空间复杂度为(n^2),只和结点个数有关,与边数无关,因此适合于稠密图

邻接矩阵法的性质

 邻接表

存储

无向图

 有向图

//边
typedef struct ArcNode
{
    int adjvex;         //边指向哪个结点
    struct ArcNode *next;//指向下一个弧的指针
    //InfoType info     //边的权值
}ArcNode;

//顶点表
typedef struct VNode
{
    VertexType data; //顶点信息
    ArcNode *first; //第一条边
}VNode,AdjList[MaxVertexNum];

//邻接表
typedef struct
{
    AdjList vertices;
    int vexnum,arcnum;
}ALGraph;

无向图的度

遍历结点的边链表,有几个边结点,度就是几

有向图的度 

出度,遍历结点的边链表。

入度,遍历结点数组的每个结点的边链表,找到指向某个结点的边。

图的邻接表表示方式不唯一

性能分析 

无向图:

每个边结点实际上被存了两份,所以空间复杂度为O(|V|+2|E|)

有向图:

每个边结点实际上存一份,所以空间复杂度为O(|V|+|E|)

十字链表存储有向图

一图看懂十字链表

十字链表法只用于存储有向图
注意:弧头和弧尾的对应,如A->B 弧头是B弧尾是A

性能分析

有向图:空间复杂度O(|V|+|E|)

解决了邻接矩阵空间复杂度高,邻接表不方便找入边不方便的问题

 邻接多重表存储无向图

一图看懂邻接多重表

 性能分析:

无向图:空间复杂度O(|V|+|E|)

删除边,删除节点都很方便

解决了邻接矩阵空间复杂度高,邻接表删除边与节点不方便的问题。

图的操作

判断图G中是否存在边<x,y>或(x,y)

邻接矩阵:O(1)

bool Adjacent(MGraph G,x,y)
{
    if(G.Edge[x][y] == 1)
        return true;
    return false;
}

邻接表:O(|E|)

bool Adjacent(AdjList G,int x,int y)
{
    ArcNode *p = G[x].first;
    while(p)
    {
        if(p->adjvex == y)
        {
            return true;
        }
        p=p->next;
    }
    return false;
}

列出图G中与结点x邻接的边

邻接矩阵:O(V)

void Neighbors(MGraph G,int x)
{
    for(int i = 0;i < G.vexnum; i++)
    {
        if(G.Edge[x][i] == 1)
        {
            cout<<i<<endl;
        }
    }
}

邻接表:

无向图O(V)(O(1)~O(V))

有向图:找入边O(E) 找出边O(V)

void Neighbors(AdjList G,int x)
{
    ArcNode *p = G[x].first;
    while(p)
    {
        cout<<p->adjvex<<endl;
        p=p->next;
    }
}

在图G中插入顶点x

邻接矩阵:直接在顶点集中插入新节点,O(1)。

邻接表:直接插入新节点,O(1)

从图中删除顶点x

邻接矩阵:直接将要删除的结点对应的行与列全部设为0,O(V)

邻接表:

无向图,删除一个结点,并释放其边链表的内存,O(E)(O(1)~O(E))

有向图,删除入边时间复杂度为O(E),删除出边为O(V)(O(1)~O(V))

若无向边(x,y)或有向边<x,y>不存在,则添加这条边

邻接矩阵:O(1)

邻接表:使用头插法可以做到O(1),尾插法是O(V)

若无向边(x,y)或有向边<x,y>存在,则删除这条边

邻接矩阵:O(1)

邻接表:使用头插法可以做到O(1),尾插法是O(V)

求图G中顶点x的第一个临界点,若有则返回顶点号,若没有或图中不存在x,则返回-1

邻接矩阵:O(V)(O(1)~O(V))

邻接表:无向图O(1)

有向图 出边O(1),入边O(E)(O(1)~O(E))

假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点顶点号,若y是x的最后一个邻接点,则返回-1

邻接矩阵:O(V)

邻接表:O(1)

获取图G中边(x,y)或<x,y>对应的权值

邻接矩阵O(1)

邻接表O(V) (O(1)~O(V))

设置图G中边(x,y)或<x,y>对应的权值。

图的遍历

广度优先搜索

辅助队列容量至少为V.

对于基于邻接矩阵实现的广度优先搜索:

时间复杂度为O(V^2),对于每个顶点,都需要遍历V个顶点

对于基于邻接表实现的广度优先搜索:

访问V个顶点需要O(V)的时间,查找各个顶点的邻接点共需要O(E)的时间,总的时间复杂度为O(V+E)

void BFS(Graph G,int v)//从顶底v开始除法,广度优先遍历G
{
    visit(v);
    vis[v]=true;
    Enqueue(Q,v);
    while(!isEmpty(Q))
    {
        DeQueue(Q,v);
        for(w = FirstNeighbor(G,v);w>=0;w = NextNeighbor(G,v,w))
        {
            if(!vis[w])
            {
                visit(w);
                visit[w] = true;
                EnQueue(Q,w);
            }
        }
    }
}

对于非连通图,上面的代码需要做一些改进:

bool vis[MAX_VERTEX_NUM];
void BFSTraverse(Graph G)
{
    memset(vis,false,sizeof(vis));
    InitQueue(Q);
    for(int i = 0;i<G.vexnum;i++)
    {
        if(!vis[i])
            BFS(G,i);
    }
}
void BFS(Graph G,int v)//从顶底v开始除法,广度优先遍历G
{
    visit(v);
    vis[v]=true;
    Enqueue(Q,v);
    while(!isEmpty(Q))
    {
        DeQueue(Q,v);
        for(w = FirstNeighbor(G,v);w>=0;w = NextNeighbor(G,v,w))
        {
            if(!vis[w])
            {
                visit(w);
                visit[w] = true;
                EnQueue(Q,w);
            }
        }
    }
}

深度优先搜索

bool vis[MAX_VERTEX_NUM];
void DFS(Graph G,int v)
{
    visit(v);
    vis[v]=true;
    for(w=FirstNeighbor(G,V);w>=0;w=NextNeighbor(G,v,w))
    {
        if(!vis[w])
        {
            DFS(G,w);
        }
    }
}

对于非连通图,上面的代码需要做一些改进:

bool vis[MAX_VERTEX_NUM];
void BFSTraverse(Graph G)
{
    memset(vis,false,sizeof(vis));
    InitQueue(Q);
    for(int i = 0;i<G.vexnum;i++)
    {
        if(!vis[i])
            BFS(G,i);
    }
}
void DFS(Graph G,int v)
{
    visit(v);
    vis[v]=true;
    for(w=FirstNeighbor(G,V);w>=0;w=NextNeighbor(G,v,w))
    {
        if(!vis[w])
        {
            DFS(G,w);
        }
    }
}

对于基于邻接矩阵实现的深度优先搜索:

时间复杂度为O(V^2),对于每个顶点,都需要遍历V-1个顶点

对于基于邻接表实现的深度优先搜索:

访问V个顶点需要O(V)的时间,查找各个顶点的邻接点共需要O(E)的时间,总的时间复杂度为O(V+E)

最短路问题

BFS处理单源无权图最短路

邻接矩阵:

时间复杂度为O(V^2)

邻接表

时间复杂度为O(V+E)

void BFS_MIN_Distance(Graph G,int u)
{
    //d[i]表示从u到i结点的最短路径
    for(int i = 1;i<G.vexnum;i++)
    {
        d[i]=INF;//初始化路径长度为无限大
        path[i]=-1;//当前结点在最短路中的前驱结点
    }
    d[u]=0;
    vis[u]=true;
    EnQueue(Q,u);
    while(!isEmpty(Q))
    {
        DeQueue(Q,u);
        for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,v))
        {
            if(!vis[w])
            {
                d[w]=d[u]+1;
                path[w]=u;
                vis[w]=true;
                EnQueue(Q,w);
            }
        }
    }
}

Dijkstra处理单源有权图最短路

时间复杂度为O(V^2)

Dijkstra也可以处理无权图,但不能处理带负权边的图

为什么无法处理带负权边的图呢?

Dijkstra的原理是每次选取一个到起始点最近的、还未被选过的点,把这个点到源点的距离定义为源点到该点的最短距离,但是如果存在负权边,这个最短距离就不一定是最短距离了。如下图所示

① 2是到1最近的点,标记2,更新1到4的距离为10
② 3是未标记点中到1最近的点,更新1到2的距离为4.
这里可以看到1到2的最短距离被更新了,因此1到4的最短距离也应该被更新,但是由于2被标记了,永远也无法再选到2去更新1到4的距离了,所以Dijkstra就失效了

int dis[MAX_VERTX];//存储每个结点到指定结点的最短路
int mp[MAX_VERTX][MAX_VERTX];//邻接矩阵存储图
int vis[MAX_VERTX];//标记数组,标记结点是否已经找到最短路
void Dijkstra(int s)
{
    int i,j,u,minn;
    memset(vis,0,sizeof(vis));
    //初始化s到其他点的距离
    for(i=1; i<=n; i++)
    {
        dis[i]=mp[s][i];
    }
    vis[s]=1;//第一个点已经找到最短路了(也就是它自己本身,长度为0)
    //找到s到其他n-1个点的最短路
    for(i=1; i<n; i++)
    {
        minn=INF;
        //找到一个离s最近的点,并且这个点之前没有被确定最短路
        for(j=1; j<=n; j++)
        {
            if(!vis[j]&&dis[j]<minn)
            {
                u=j;
                minn=dis[j];
            }
        }
        if(minn==INF)
            break;
        //标记这个点,即s已经找到到这个点的最短路
        vis[u]=1;
        //更新s到其他点的距离
        for(j=1; j<=n; j++)
        {
            if(!vis[j]&&dis[j]>dis[u]+mp[u][j])
            {
                dis[j]=dis[u]+mp[u][j];
            }
        }
    }
}

Floyd处理多源最短路

思路:对于两点之间的最短路,可以通过改变中转点或增加中转点的数量来找到更近的最短路。

Floyd实际上是枚举中转点,之后枚举图中每条路径之间的距离,不断地通过中转点来更新每条路径之间的最短路实现的。

实现:三层循环,第一层枚举中转点,第二层枚举路径起点,第三层枚举路径终点。设最外层循环执行的次数为i,则中转点可选取的数量为[1,i]。

时间复杂度为O(V^3)

可以处理带负权边的图,但不能处理带负权环的图。负权环的图没有最短路。

void Floyd(Graph G)
{
    //以k作为中转
    for(int k = 0;k<G.vexnum;k++)
    {
        //起始点为i
        for(int i = 0;i<G.vexnum;i++)
        {
            //终点为j
            for(int j = 0;j<G.vexnum;j++)
            {
                if(G.E[i][j]<G.E[i][k]+G.E[k][j])
                {
                    G.E[i][j]=G.E[i][k]+G.E[k][j];
                    path[i][j]=k;//从i到j的中转点为k
                }
            }
        }
    }
}

拓扑排序

可以用栈、队列甚至数组来实现

邻接矩阵:

O(V^2)

邻接表:

O(V+E)

基于邻接表的拓扑排序:

#define MaxVertexNum 100
typedef struct ArcNode
{
    int adjvex;
    struct ArcNode *next;
}ArcNode;
typedef struct VNode
{
    VertexType data;
    ArcNode *fisrt;
}
typedef struct
{
    AdjList vertices;
    int vexnum,int arcnum;
}Graph;
int indegree[MaxVertexNum];
bool TopologicalSort(Graph G)
{
    InitStack(S);
    for(int i = 0;i<G.vexnum;i++)
    {
        if(indegree[i]==0)
            Push(S,i);  //将所有入度为0的顶点入栈
    }
    int count = 0;//记录已经输出的顶点数
    while(!isEmpty(S))
    {
        Pop(S,i);
        cout<<i<<endl;//输出顶点i
        count++;
        for(ArcNode p=G.vertices[i].firstarc;p;p=p->next)
        {
            //将所有i指向的顶点的入度减一,并将度减为零的点压入栈中
            v=p->adjvex;
            if(!(--indegree[v]))
            {
                Push(S,v);
            }
        }
    }
    if(count<G.vexnum)
    {
        return false;
    }
    else
        return true;
    
}

其他代码

基于邻接表的DFS非递归遍历

void DFS(Graph G,int x)
{
    for(int i = 0; i<G.vernum; i++)
    {
        if(!visited[i])
        {
            InitStack(S);
            visited[x]=true;
            Push(S,x);
            while(!isEmpty(S))
            {
                x = Pop(S);//从栈中取出一个顶点
                visit(x);//访问x
                //把该顶点相连的、还未入过栈的顶点全部加入栈中。
                for(w=FirstNeighbor(G,x);w>=0;w=NextNeighbor(G,x,w))
                {
                    if(!visited[w])
                    {
                        visited[w]=true;
                        Push(S,w);
                    }
                }
            }
        }
    }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值