学习笔记——图

目录

 

用邻接矩阵实现图

用邻接表实现图

广度优先搜索

深度优先搜索

最短路径

Dijkstra算法(单源最短路径)

Floyd算法(任意两点最短路径)

最小生成树

Prim算法       时间复杂度0(n^2)

Kruskal算法       时间复杂度O(eloge)


用邻接矩阵实现图

邻接矩阵实现的图结构定义如下

typedef struct graph *Graph;    //Graph是一个类型,为graph型指针
struct graph
{
    WItem NoEdge;//无边标记
    int n;        //顶点数
    int e;        //边数
    WItem **a;    //邻接矩阵,a是一个二级指针
}AWDgraph;

函数GraphInit(n,noEdge)创建一个有n个孤立顶点的图

Graph GraphInit(int n,WItem noEdge) //返回指向一个图的指针
{
    Graph G=malloc(sizeof*G);    //G是变量名,sizeof要加*
    G->n=n;
    G->e=0;
    G->a=Make2DArray(G->n+1,G->n+1,noEdge);    //创建一个权值全为noEdge的矩阵
    //不赋权图将上一句的NoEdge改为0
    return G;
}

函数GraphVerticles(G)和GraphEdges(G)分别返回图G的顶点数和边数

int GraphVerticles(graph G)
{
    return G->n;
}

int GraphEdges(graph G)
{
    return G->e;
}

函数GraphExits(i,j,G)判断当前图G中的边(i,j)是否存在

int GraphExits(int i,int j,graph G)
{
    if(i<1||j<1||i>G->n||j>G->n||G->a[i][j]==G->NoEdge) return 0;
    //输入不在合法范围内或该边值为NoEdge
    //不赋权图把if条件中的NoEdge改为0
    return 1;
}

函数GraphAdd(i,j,w,G)在图G中加入边(i,j)

void GraphAdd(int i,int j,WItem w,Graph G)
{
    if(i<1||j<1||i>G->n||j>G->n||i==j||G->a[i][j]!=G->NoEdge)
    //若输入不在合法范围或在对角线上或该边已经存在,则报错
    //不赋权图把if条件中的NoEdge改为0
        Error("Bad input");
    G->a[i][j]=w;
  //G->a[j][i]=w;    无向图加一句这个
  //不赋权图只需要将上述语句的w改成1
    G->e++;    //更新边数
}

函数GraphDelete(i,j,G)在图G中删除边(i,j)

void GraphDelete(int i,int j,graph G)
{
    if(i<1||j<1||i>G->n||j>G->n||G->a[i][j]==G->NoEdge)
    //输入不在合法范围内或待删边本不存在
    //不赋权图把if条件中的NoEdge改为0
        Error("Bad input");
    G->a[i][j]=NoEdge;
  //G->a[j][i]=NoEdge;    无向图加一句这个
  //不赋权图只需要将上述语句的w改成0
    G->e--;    //更新边数
}

函数InDegree(i,G)返回图G中顶点度数

int InDegree(int i,graph G)
{
    int sum=0;
    /*可单独作为求入度的方法*/
    for(int j=0;j<G->n;j++)
    {
        if(G->a[i][j]!=NoEdge)   //无向图改NoEdge为0
            sum++;
    }
    /*如果是无向图加上以下代码*/
    /*也可单独作为求出度的方法*/
    for(int j=0;j<G->n;j++)
    {
        if(G->a[j][i]!=NoEdge)    //无向图改NoEdge为0
            sum++;
    }
    /**/
    return sum;
}

用邻接表实现图

邻接表结点结构定义

typedef struct lnode *glink;    //glink是一个类型,为lnode型指针
struct lnode
{
    int v;    //边的另一个顶点
    glink next;    //邻接表指针
  //若是赋权图,加上w属性
  //WItem w;
};

创建一个新的邻接表结点

glink NewLNode(int v,glink next)    //返回指向lnode的指针
{
    glink x=malloc(sizeof*x);    //x是变量名,sizeof取变量名长度要加*
    x->v=v;
    x->next=next;
  //若是赋权图
  //x->w=w;
    return x;
}

用邻接表实现图的结构定义

typedef struct graph *Graph;
struct graph
{
    int n;    //顶点数
    int e;    //边数
    glink *adj;    //glink型的一维数组,数组中的成员都是指针
}Lgraph;

函数GraphInit(n)创建一个用邻接表表示的有n个孤立顶点的图

Graph GraphInit(int n)
{
    Graph G=malloc(sizeof*G)     //G是变量名,sizeof前加*
    G->n=n;
    G->e=0;
    G->adj=malloc(n+1)*sizeof(glink))    //adj是一个指针数组,成员都是glink指针,v此时是0
    for(int i=0;i<=n)
        G->adj[i]=0;
    return G;
}

函数GraphVerticles(G)和函数GraphEdge(G)分别返回图的顶点数和边数同邻接矩阵的写法,略。

函数GraphExist(i,j,G)判断当前有向图G中的边(i,j)是否存在

int GraphExist(int i,int j,graph G)

{
    if(i<1||j<1||i>G->n||j>G->n) return 0;
    glink p=G->adj[i];
    while(p&&p->v!=j) p=p->next;    //不走到最后且不为目标,则继续向前
    if(p) return 1;
    else return 0;
}

函数GraphAdd(i,j,G)通过在顶点i的邻接表表首插入顶点j来实现向图中加入一条边(i,j)的操作

void GraphAdd(int i,int j,Graph G)    //赋权图加上WItem w
{
    if(i<1||j<1||i>G->n||j>G->n||i==j||GraphExits(i,j,G))
        Error("Bad input");
    G->adj[i]=NewLNode(j,G->adj[i]);    //直接取代G->a[i]
  //无向图加上下一句
  //G->adj[j]=NewLNode(i,G->adj[j]);
  //赋权图加上w
  //G->adj[i]=NewLNode(j,w,G->adj[i]);
    G->e++;
}

函数GraphDelete(i,j,G)删除有向图G中的边(i,j)

void GraphDelete(int i,int j,Graph G)
{
    glink p,q;
    if(i<1||j<1||i>G->n||j>G->n||!GraphExists(i,j,G))
    {
        Error("Bad input");
    }
    p=G->adj[i];
    if(p->v==j)    //如果第一个节点就是待删除节点,直接令其成为邻接表数组中的成员
    {
        G->adj[i]=p->next;
        free(p);
    }
    else
    {
        while(p&&p->v!=j) p=p->next;    //若未走到最后且未找到目标则继续向前
        if(p)    //若找到目标
        {
            q=p->next;
            p->next=q->next;
            free(q);
        }
    }
  //有向图加上下面的代码
  /*
    p=G->adj[j];
    if(p->v==i)    //如果第一个节点就是待删除节点,直接令其成为邻接表数组中的成员
    {
        G->adj[j]=p->next;
        free(p);
    }
    else
    {
        while(p&&p->v!=i) p=p->next;    //若未走到最后且未找到目标则继续向前
        if(p)    //若找到目标
        {
            q=p->next;
            p->next=q->next;
            free(q);
        }
    }
    */

    G->e--;
}

函数OutDegree(i,G)通过计算顶点i的邻接表长,返回有向图G中顶点的出度

int OutDegree(int i,Graph G)
{
    glink p;
    int sum=0;
    if(i<1||i>G->n) Error("Bad input");
    p=G->adj[i];
    while(p)
    {
        sum++;
        p=p->next;
    }
    return sum;
}

函数InDegree(i,G)返回有向图G中顶点i的入度

int InDegree(int i,Graph G)
{
    int sum=0;
    for(int j=0;j<G->n;j++)
    {
        if(GraphExists(i,j,G))
            sum++;
    }
    return sum;
}

广度优先搜索

//用邻接矩阵实现的无向图G中的广度优先搜索算法bfs描述
bfs(G,i)
{
    /*从顶点v开始,广度优先搜索图G的算法*/
    标记顶点i;
    用顶点i初始化顶点队列Q;
    while(!QueueEmpty(Q))
    {
        i=DeleteQueue(Q);
        设j是i的邻接顶点;
        while(j)
        {
            if(j未标记)
            {
                标记顶点j;
                EnterQueue(j,Q);
            }
            j=i的下一个邻接顶点
        }
    }
}

===================================================================
//具体实现时,用一个数组pre来记录搜索到的顶点的状态。初始时对所有顶点v有pre[v]=0。
//用一全局变量cnt记录算法对图中顶点的访问次序。算法结束后,数组pre[i]中的值是算法访问顶点i的序号。


void bfs(Graph G,int i)
{
    Queue Q=QueueInit();
    EnterQueue(i,Q);    //顶点先入队
    while(!QueueEmpty(Q))
    {
        if(pre[i=DeleteQueue(Q)]==0)    //未访问过
        {
            pre[i]=cnt++;    //访问该结点并标记
            for(int j=1;j<=G->n;j++)
            {
                if(G->adj[i][j]&&pre[j]==0)    //若边存在且未访问过,入队
                    EnterQueue(j,Q);
            }
        }
    }
}

//图中可能不止一个连通分量,遍历全图算法如下

void GraphSearch(Graph G)
{
    cnt=1;
    for(int i=1;i<=G->n;i++) pre[i]=0;
    for(int i=1;i<=G->n;i++)
    {
        if(pre[i]==0)bfs(G,i);    //对每个连通分量调用一次bfs
    }
}
//天勤上面用邻接表表示图的bfs写法
int visit[MAXSIZE]=0;    //访问状态数组
void bfs(Graph *G,int v)
{
    ArcNode *p;
    visit[v]=1;
    //Visit(v);
    Queue<int> q;
    q.push(v);
    while(!IsEmpty(q))
    {
        int t=q.top();
        q.pop();
        p=G->adj[t].firstarc;    //p指向当前结点第一条边
        while(p)
        {
            if(visit[p->adjvex]==0)
            {
                visit[p->adjvex]=1;
                //Visit(p->adjvex);
                q.push(p->adjvex);    //若当前邻接顶点未被访问过,则置为已访问,并压入队列
            }
            p=p->nextarc;    //否则,p指向当前结点的下一条邻边
        }
    }
}
void BFS(Graph *G)
{
    for(int i=0;i<G->n;i++)
        if(visit[i]==0)
            bfs(G,i);
}

深度优先搜索

//用邻接矩阵实现的无向图G中的深度优先搜索算法dfs如下
void dfs(Graph G,int i)
{
    pre[i]=cnt++;    //初始化时所有结点pre[i]=0,当pre[i]不为0使表示该结点已访问过,pre[i]的值为算法访问结点i的序号
    for(int j=1;j<=G->n;j++)    //顺序遍历与结点i相邻的结点,
    {
        if(G->[i][j])    //若边<i,j>存在
        {
            if(pre[j]==0)dfs(G,j);    //若结点j还未被访问过,对j递归调用dfs,若访问过,j++探测下一个相邻结点
        }
    }
}

//用邻接表实现的有向图G中的深度优先搜索算法dfs如下
void dfs(Graph G,int i)
{
    glink p;
    pre[i]=cnt++;
    for(p=G->adj[i];p;p=p->next)
    {
        if(pre[i]==0)dfs(G,p->v);    //若p指向的结点已访问过,结束当前层次的递归,返回上一层递归,执行p=p->next探测下一个相邻结点
    }   
}

void GraphSearch(Graph G)
{
    cnt=1;
    for(int i=1;i<=G->n;i++) pre[i]=0;
    for(int i=1;i<=G->n;i++) if(pre[i]==0) dfs(G,i);
}
//天勤上面用邻接表表示图的dfs写法
int visit[MAXSIZE]=0;    //全局变量用来保存访问状态
int dfs(Graph *G,int v)
{
    ArcNode *p;
    visit[v]=1;    //置当前结点为已访问
    Visit(v);
    p=G->adj[v].firstarc;    //p指向顶点v的第一条边
    while(p)
    {
        if(visit[p->adjvex])==0    //若当前结点未被访问过,则往深处继续访问
        {
            dfs(G,p->adjvex)
        }
        p=p->nextarc;    //否则检测下一个与v邻接的结点
    }    //执行到这一步时,所有与v邻接的结点已经检查完毕,该层dfs也执行到了最后一句,自动返回上一层递归
}
void DFS(Graph *G)
{
    for(int i=0;i<G->n;i++)
        if(visit[i]==0)
            dfs(G,i);
}
//天勤上判断图G是否是树的算法,基于dfs
int vc=0,ec=0;    //定义全局变量vc和ec,就不用在函数里面传了
int visit[MAXSIZE]=0;
void dfs(Graph *G,int v)
{
    ArcNode *p;
    visit[v]=1;    //置当前结点为已访问
    vc++;    //结点数自增1
    p=G->adj[v].firstarc;
    while(p)
    {
        if(visit[p->adjvex]==0)    //若当前顶点的邻接点未被访问过,则访问它,并对ec自增1
        {
            ec++;
            dfs(G,p->adjvex);
        }    
        p=p->next;
    }
}

int IsTree(Graph *G)
{
    dfs(G,1);
    if(vc==G->n&&(ec/2)==G->e)    //若vc等于图中顶点数,ec等于图中边数的2倍
        return 1;
    return 0;
}
//由于每次访问顶点ec都会累加上之前所有与访问过的结点关联的边数,因此遍历完之后,相当于ec中的数值是图的度数,(一条边2个度),因此ec/2就是访问过的边数。

最短路径

Dijkstra算法(单源最短路径)

算法思想:

设置一个顶点集合S,并不断作贪心扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已经。初始时,S中仅含源。设u是G的某个顶点,把从源到u且中间只经过S中顶点的路径成为源到u的特殊路径,并用数组dist来记录当前每个顶点所对应的最短特殊路径长度。Dijkstra算法每次从V-S中取出具有最短特殊路径长度的顶点u,将u添加到集合S中,同时对数组dist作必要的修改。一旦S包含了所有V中顶点,dist就记录了从源到其他所有顶点之间的最短路径长度。

算法描述:

步骤1:初始化dist[v]=a[s][v]

             对于所有与s邻接的顶点v置pre[v]=s;

             对于其他顶点u置pre[u]=0;

             建立表L包含所有pre[v]≠0的顶点v      //即L中初始时都是与源s直接相邻的顶点

步骤2:若表L空,则算法结束,否则转步骤3

步骤3:从表L中取出dist值最小的顶点v      //即与源s最近的顶点

步骤4:对于顶点v的所有邻接顶点u置dist[u]=min{dist[u],dist[v]+a[v][u]}

             若dist[u]改变,即加入顶点v后特殊路径更短,则置pre[u]=v;且若u不在表L中,将u加入L;转至步骤2

//在用邻接矩阵实现的赋权有向图中,单源最短路径问题的Dijkstra算法实现如下
void Dijkstra(int s,WItem dist[],int prev[],Graph G)
{
    int i,j;
    List L=ListInit();    //初始化L表
    if(s<1||s>G->n) Error("Out of bounds")    //处理非法输入
    /*初始化dist,prev和L*/
    for(int i=1;i<=G->n;i++)
    {
        dist[i]=a[s][i];    //顶点i到源点s的初始距离为a[s][i]的值
        if(dist[i]==G->NoEdge) prev[i]=0;    //若顶点i与源点s之间不存在边,则置i的前驱结点为0
        else{prev[i]=s;ListInsert(0,i,L);}    //否则,置i的前驱结点为s,并将顶点i加入L表,参数啥意思?
    }
    dist[s]=0;    //源点s的前驱为0
    /*修改dist和prev*/
    while(!ListEmpty(L))
    {
        /*找L中具有最小dist值得顶点v*/
        /*将顶点v从表L中删除,并修改dist的值*/
        i=ListDelMin(L,dist);    //顶点i是L表中所有已知特殊路径长度的结点中路径最短的
        for(int j=1;j<=G->n;j++)    //遍历所有与顶点i相邻的顶点
        {
            if(G->a[i][j]!=G->NoEdge&&(!prev[j]||dist[j]>dist[i]+G->a[i][j]))    //若边(i,j)存在,且加入结点i之后,结点j到源点s的距离缩短
            {
                /*dist减少*/
                dist[j]=dist[i]+G->a[i][j];    //更新结点j到源点s的特殊路径距离
                /*顶点j插入表L*/
                if(!prev[j]ListInsert(0,j,L))    //结点j的特殊路径长度已知,加入表L
                prev[j]=i;    //更新结点j的前驱
            }
        }
    }
}
//天勤上的写法,我觉得更好理解一些
void Dijkstra(Graph G,int v,int dist[],int path[])    //dist是v到某点vu的距离,path是vu到v最短路径上前驱结点的下标,dist是一维数组,因为将它看作二维数组时,有一维是不变的,下标都是v
{
    int sex[MAXSIZE];    //set记录顶点是否被加入路径,取值为1则为加入,0为未加入

    /*以下是对各数组进行初始化*/
    for(int i=0;i<G.n;i++)
    {
        dist[i]=G.edges[v][i];
        path[i]=v;
        set[i]=0;
    }
    set[v]=1;
    /*初始化完毕*/

    /*以下是找到剩余结点中与已生成的路径距离最短的结点*/
    int min=INF;
    for(int i=0;i<G.n;i++)
    {
        if(set[i]==0&&dist[i]<min)
        {
            u=i;
            min=G.dist[i];
        }
    }
    set[u]=1;
    /*找到该结点后,加入路径中*/

    /*加入顶点u到路径中之后,更新dist和path*/
    for(int i=0;i<G.n;i++)
    {
        if(set[i]==0&&dist[u]+G.edges[u][i]<dist[i])
        {
            dist[i]=dist[u]+G.edges[u][i];
            path[i]=u;    //u为i在路径上的前驱
        }
    }
}

Floyd算法(任意两点最短路径)

算法思想:

设置一个矩阵c,初始时c[i][j]=a[i][j]。然后再矩阵c上做n次迭代,经第k次迭代后,c[i][j]的值是从顶点i到顶点j,且中间不经过编号大于k的顶点的最短路径长度。在c上做第k次迭代时,c[i][j]=min{c[i][k]+c[k][j],c[i][j]},要计算c[i][j]只要比较当前c[i][j]与c[i][k]+c[k][j]的大小。当前c[i][j]的值表示从顶点i到j,中间顶点编号不大于k-1的最短路径长度;而c[i][k]+c[k][j]表示从顶点i到k,再从k到j,且中间不经过顶点编号大于k的顶点的最短路径长度。

二维数组path用来记录最短路径。当k是算法中使c[i][j]取得最小值的整数时,就置p[i][j]=k。当path[i][j]=0时,表示从顶点i到j的最短路径就是从i到j的边。在计算出c[i][j]的值后,容易由path记录的信息,找出相应的最短路径。

void Floyd(WItem **c,int **path,Graph G)
{
    /*初始化c[i][j]*/
    for(int i=1;i<=G->n;i++)
        for(int j=1;j<=G->n;j++)
        {
            if(i==j)
                c[i][j]=0;
            else
            { 
                c[i][j]=G->a[i][j];
                path[i][j]=0;    //path=0表示当前顶点i到顶点j的最短路径就是它们之前的边
            }
        }
    for(int k=1;k<=G->n;k++)
        for(int i=1;i<=G->n;i++)
            for(int j=1;j<G->n;j++)
            {
                if(c[i][k]!=NoEdge&&c[k][j]!=NoEdge&&(c[i][j]==NoEdge||c[i][j]>c[i][k]+c[k][j]))    
                //确保加入的顶点k与顶点i,顶点k与顶点j之前是可达的,当顶点i到顶点j无边或小于顶点i到k加上顶点k到j的距离时,更新顶点i到j的路径长度
                {
                    c[i][j]=c[i][k]+c[k][j];
                    p[i][j]=k;    //加入的点k是使c[i][j]取最小值的数
                }
            }
}

最小生成树

Prim算法       时间复杂度0(n^2)

算法思想:

首先设S={1},然后,只要S是V的真子集(就是还没有包含所有结点),就作如下的贪心选择:选取集合S中的一点i,V-S中的一点j,且a[i][j]最小的边,并将顶点j添加到集合S中。这个过程一直进型到S=V时为止。(不需要判断是否有回路,因为边的两个顶点来自不同集合)

如何找出满足条件的i和j,需要设置两个数组closet和lowcost。对于一个V-S中的顶点j,closest[j]是j在S中的一个邻接顶点,它与j在S中的其他邻接顶点k相比较都有a[j][closest[j]]<=a[j][k]。lowcost[j]的值就是a[j][closest[j]]

在Prim算法执行过程中,先找出V-S中使lowcost最小的顶点j,然后根据数组closest选取边(j,closest[j]),最后将j添加到S中,并对closest和lowcost作必要的修改。

void Prim(WItem *lowcost,int *closest,Graph G)
{
    /*初始化*/
    int *s;    //集合S,值为1表示在集合中,为0表示不在集合中
    s=malloc((G->n+1)*sizeof(int));
    for(int i=1;i<=G->n;i++)
    {
        lowcost[i]=a[1][i];    //先设置顶点1在S中
        closest[i]=1;
        s[i]=0;    //除1以外所有点都不在集合S中
    }
    s[1]=1;    //把顶点1加入集合S
    
    for(int i=1;i<=G->n;i++)    //执行n次
    {
        min=G->NoEdge;    //要找出最小值,先把min设为最大
        j=1;    //j用来记录到集合S距离最短的顶点序号,一开始先设为1作为初始值
        for(int k=2;k<=G->n;k++)    //k=2是因为一开始只有1在S中,V-S要从2开始
        {
            if(lowcost[k]<min)&&(!s[k])    //k到集合S最短距离小于min且k不在集合S中,即k在V-S中
            
            {
                min=lowcost[k];    //更新最小值,j为当前V-S中里S最近的顶点,现在令j为k
                j=k;
            }
        }
        //此时j是到集合S距离最短的顶点序号
        s[j]=1;    //把j加入集合S
        for(int k=2;k<=G->n;k++)    //j加入集合S后,要修改原来的closest和lowcost
        {
            if((G->a[k][j]<lowcost[k])&&(!s[k]))    //若K不在集合S且顶点j和k之间距离小于j加入集合S前顶点k到集合S的最短距离
            {
                //更新closest,lowcost
                closest[k]=j;
                lowcost[k]=G->a[k][j]
;            }
        }
    }
}

//V-S中的顶点为焦点向S中的顶点逐个连线找最短,j是通过k来更新的
//利用closest还能找到最短路径
//while(k>0)
//p=closest[k];
//k=p;

Kruskal算法       时间复杂度O(eloge)

首先将G的n个顶点看成n个孤立的连通分支。将所有的边按权从小到大排序,从第一条边开始,依边权递增的顺序查看每一条边,当查看到第k条边(v,w)时,若端点v和w分别是当前两个不同的连通分支T1和T2的顶点时,就用边(v,w)将T1和T2连接成一个连通分支,然后继续查看第k+1条边,否则直接查看第k+1条边,这个过程一直进行到只剩下一个连通分支时为止。

上述构造最小生成树的Kruskal算法需要按权的递增顺序查看图G的所有边。为此需要将G的所有边按边权值排序。存储每条边的结构定义为:

typedef struct edge
{
    int u;
    int v;
    WItem w;
}Edge;

/*函数EDGE(u,v,w)创造一条权威w的边(u,v)*/
Edge EDGE(int u,int v,WItem w)
{
    Edge e;
    e->u=u;
    e->v=v;
    e->w=w;
    return e;
}
/*函数Edges(a,G)抽取图G的所有边到赋权边数组a中,并返回图G的边数*/
int Edges(Edges a[],Graph G)
{
    int k=0;
    for(int i=1;i<=G->n;i++)
        for(int j=1;j<=G->n;j++)
            if(G->a[i][j]!=G->NoEdge)
                a[k++]=EDGE(i,j,G->a[i][j]);
    return k;
}

void Kruskal(Edge mst[],Graph G)
{
    Edge a[maxE];
    UFset U;    //并查集u
    int e=Edges(a,G);    //抽取G的所有边,e为边数
    quicksort(a,0,e-1);    //对边数组a排序,参数意义为对数组a从元素0到元素e-1进行排序
    U=UFinit(G->n);    //初始化并查集U
    for(int i=0,int k=0;i<e&&k<G->n-1;i++)    //k记录循环次数,执行n-1次结束
    {
        int s=UFind(a[i].u,U);    //a[i].u是当前最短边的一个端点,找出它所属的连通分支
        int t=UFind(a[i].v,U);    //a[i].v是当前最短边的另一个端点,找出它所属的连通分支
        if(s!=t)    //若s和t不是同一个连通分支
        {
            mst[k++]=a[i];    //更新最小生成树
            UFunion(s,t,U);    //合并连通分支s和t
        }
    }
    
}
发布了35 篇原创文章 · 获赞 2 · 访问量 1390

猜你喜欢

转载自blog.csdn.net/weixin_41001497/article/details/101385955