数据结构(C语言版 严蔚敏著)——图

图的定义

· 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示

  为:G(V,E)。其中G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

    -在图中数据元素称之为顶点(Vertex)。

    -顶点集合要有穷非空。

    -任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以空。

· 无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Egde),

  用无序偶(Vi,Vj)来表示。

· 上图G1是一个无向图,G1={V1,E1},其中

    -V1={A,B,C,D},

    -E1={(A,B),(B,C),(C,D),(D,A),(A,C)}

 

· 有向边:若从顶点Vi到Vj的边有方向,则称这条边为有向边,也成为弧(Arc),

  用有序偶<Vi,Vj>来表示,Vi称为弧尾,Vj称为弧头。

· 上图G2是一个有向图,G2={V2,E2},其中

    -V2={A,B,C,D}

    -E2={<B,A>,<B,C>,<C,A>,<A,D>}

 

· 简单图:在图结构中,若不存在顶点到其自身的边,且同一条边不重复

  出现 ,则称这样的图为简单图。

· 以下两个则不属于简单图:

 

· 无向完全图:在无向完全图中,如果任意两个顶点之间都存在边,则称

  该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边。

 

· 有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的

  两天弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)条边。

 

· 稀疏图和稠密图:这里的稀疏和稠密是模糊的概念,都是相对而言,通常认为边或

  弧数小于n*logn(n是顶点的个数)的图称为稀疏图,反之称为稠密图。

· 有些图的边或弧带有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight),

  带权的图通常称为网(Network)。

 

· 假设有两个图G1=(V1,E1)和G2=(V2,E2),如果V2属于V1,E2属于E1,则

  称G2为G1的子图(Subgraph)。

 

图的顶点与边之间的关系

· 对于无向图G=(V,E),如果边(V1,V2)属于E,则称顶点V1和V2互为邻接点(Adjacent),即V1和V2相

  邻接。边(V1,V2)依附(incident)于顶点V1和V2,或者说(V1,V2)与顶点V1和V2相关联。

· 顶点V的度(Degree)是和V相关联的边的数目,记为TD(V),如下图,顶点A与B互为邻接点,

  边(A,B)依附于顶点A与B上,顶点A的度为3。

 

· 对于有向图G=(V,E),如果有<V1,V2>属于E,则称顶点V1邻接到顶点V2,顶点V2邻接自顶点V1。

· 以顶点V为头的弧的数目称为V的入度(InDegree),记为ID(V),以V为尾的弧的数目称为

  V的出度(OutDegree),记为OD(V),因此顶点V的度为TD(V)=ID(V)+OD(V)

· 下图顶点A的入度是2,出度是1,所以顶点A的度是3。

 

路径

· 无向图G=(V,E)中从顶点V1到顶点V2的路径。

· 下图用红线列举了从顶点B到顶点D的四种不同路径:

· 如果G是有向图,则路径也是有向的。

· 下图用红线列举顶点B到顶点D的两种路径,而顶点A到顶点B就不存在路径:

· 路径的长度是路径上的边或弧的数目。

· 第一个顶点到最后一个顶点相同的路径称为回路或者环(Cycle)。

· 序列中顶点不重复出现的路径称为 简单路径,除了第一个顶点和最后一个顶点

  之外,其余顶点不重复出现的回路,称为简单回炉或简单环。

· 下图左侧是简单环,右侧 不是简单环:

连通图

· 在无向图G中,如果匆匆顶点V1到顶点V2有路径,则称V1和V2是联通的,如果

  对于图中任意两个顶点Vi和Vj都是连通的,则称G是连通图。

· 无向图中的极大连通子图称为连通分量

· 注意以下概念:

    -首先要是子图,并且子图是要连通的。

    -连通子图含有极大顶点数。

    -具有极大顶点数的连通子图包含依附于这些顶点的所有边。

· 在有向图G中,如果对于每一队Vi到Vj都存在路径,则称G是强连通图。

· 有向图中的极大强连通子图称为有向图的强连通分量

· 下图左侧并不是强连通图,右侧是。并且右侧是左侧的极大强连通子图,

  也是左侧的强连通分量。

· 最后我们再来看连通图的生成树定义

· 所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个

  顶点,但只有足以构成一棵树的n-1条边。

 

· 如果一个有向图恰有一个顶点入度为0,其余顶点入度均为1,则是一棵有向树。

左边的有向图,能拆分成右边两棵有向树。

 

 

图的存储结构

· 因为任意两个顶点之间都可能村长联系,因此无法以数据元素在内存中的物理

  位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的)。

· 如果用多重链表来描述倒是可以做到,但是纯粹用多重链表导致的浪费是无法

  想象的(如果各个顶点的度数相差太大,就会造成巨大的浪费)。

邻接矩阵(无向图)

· 考虑到图是由顶点和边或弧两部分组成,自然会用到两个结构体来存储。

· 顶点因为不区分大小、主次,所以用一个一维数组来存储。

· 边或弧由于是顶点与顶点之间的关系,可以考虑二维数组来存储。

 

· 图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点

  信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

· vertex[4]={v0,v1,v2,v3},边数组arc[4][4]为对称矩阵(由于无向图不分先后)

  0表示不存在顶点间的边,1表示顶点间存在边。

· 对称矩阵:所谓对称矩阵就是n阶矩阵的元满足a[i][j]=a[j][i](0<=i,j<=n)。

· 有了这个二维数组组成的对称矩阵,可以很容易地知道图中的信息:

    -要判定任意两顶点是否有边无边就非常容易。

    -某个顶点的度就为这个顶点Vi在邻接矩阵中第i行(列)的元素之和。

    -顶点Vi的所有邻接点就是矩阵中第i行元素扫描一遍,为1就是邻接点。

邻接矩阵(有向图)

· 可见顶点数组vertex[4]={V0,V1,V2,V3},弧树组arc[4][4]也是一个矩阵,但因为是有

  向图,所以这个矩阵并不对称,例如由V1到V0有弧,得到arc[1][0]=1,而V0到V1

  没有弧,因此arc[0][1]=0。

· 关于入度和出度,顶点V1的入度为1,整合是第V1列各数之和,

  顶点V1的出度为2,正好是第V1行的各数之和。

邻接矩阵(网)

· 当i=j时书上是用∞表示,有的地方用0表示

· 这里“∞”表示一个计算机允许的,大于所有边上权值的值(int最大值65535)。

图的数组表示法,结构体:

#define INFINITY 65535      //表示无穷大-->在带权的图中用到,即网
#define MAX_VERTEX_NUM 20   //图的最大顶点数
#define MAX_INFO 20         //最大信息数
typedef char InfoType;     //附加信息类型
typedef int VRType;        //顶点关系类型
typedef int VertexType;    //顶点数据类型
//图的种类:有向图,有向网,无向图,无向网
typedef enum {
    DG, DN, UDG, UDN
} GraphKind;
typedef struct ArcCell {
    VRType adj;     //顶点关系类型,对无权图用1或0表示是否相邻
    //对带权图,则为权值类型
    InfoType *info; //附加信息指针
} ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

typedef struct {
    VertexType vexs[MAX_VERTEX_NUM];//顶点向量
    AdjMatrix arcs;     //邻接矩阵
    int vexnum, arcnum;  //当前顶点数和弧数
    GraphKind kind;     //图的种类
} MGraph;

 

邻接表(无向图)

 

· 我们可以发现,对于边数相对较少的图,上面的邻接矩阵是存在巨大浪费的。如下图

· 因此可以采用数组和链表组合起来存储,这里称为邻接表。

· 邻接表的处理方法是这样的:

    -图中顶点用一个一维数组存储。

    -图中每个顶点Vi的所有邻接点构成一个线性表,由于个数不确定,所以用单链表存储。

邻接表(有向图)

· 类似地,下面是把顶点当弧尾建立的邻接表,这样容易得到每个顶点的出度。

· 有时为了确定顶点的入度或以顶点为弧头的弧,可以建立一个有向图的逆邻接表。

邻接表(网)

· 对于带权值的网图,可以在边表结点定义中再增加一个数据域来存储权值即可。

图的邻接表存储,结构体

#define MAX_VERTEX_NUM 20
typedef char InfoType;     //附加信息类型
typedef int VertexType;    //顶点数据类型
typedef struct ArcNode{
    int adjvex;             //该弧所指向的顶点位置
    struct ArcNode *nextarc;//下个结点
    InfoType *info;         //当前结点(弧)的信息
}ArcNode;
typedef struct VNode{
    VertexType data;//顶点信息
    ArcNode *firstarc;//指向第一个依附于该顶点弧的指针
}VNode,*AdjList[MAX_VERTEX_NUM];
typedef struct {
    AdjList vertices;
    int vexnum,arcnum;//图的当前顶点数和弧数
    int kind;        //图的种类标志
}ALGraph;

 

十字链表(是对于有向图来说的,关心出入度问题,所以把两个邻接表结合一起)

 

· 邻接表固然优秀,但也有不足,例如对有向图的处理上,有时需要再建立一个逆邻接表。

· 这时候就出现了十字链表。

· 为此重新定义顶点表结构结点结构:

· 接着重新定义边表结点结构:

· 可以发现,蓝线是邻接表的指向,红线是逆邻接表的指向,是把他们合起来了。

· 十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到Vi为

  尾的弧,也容易找到以Vi为头的弧,因而容易求得顶点的出度和入度。

· 十字链表除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的。

  因此,在有向图的应用中,十字链表也是非常好的数据结构模型。

//十字链表
#define MAX_VERTEX_NUM 20
typedef char InfoType;     //附加信息类型
typedef int VertexType;    //顶点数据类型
typedef struct ArcBox{
    int tailvex,headvex;    //该弧的尾和头顶点的位置
    struct ArcBox *hlink,*tlink;//分别为弧头相同回合弧尾相同的弧的链域
    InfoType *info;     //该弧相关信息的指针
}ArcBox;
typedef struct VexNode{
    VertexType data;
    ArcBox *firstin,*firstout;//分别指向该顶点第一条弧和出弧
}VexNode;
typedef struct {
    VexNode xlist[MAX_VERTEX_NUM];//表头向量
    int vexnum,arcnum;      //有向图的当前顶点数和弧数 
}OLGraph;

 

邻接多重表(对无向图而言)

 

· 如果在无向图的应用中,关注的重点是顶点的话,那么邻接表是不错的选择,但如果更挂关注的是边的操作,

  比如对已经访问过的边做标记,或者删除某一条边等操作,邻接表就显得不方便了(如下图)

· 因此,这里仿造十字链表的方式,对边表结构进行改装,重新定义的边表结构如下:

· 其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。

  iLink指向依附点iVex的下一条边,jLink指向依附顶点jVex的下一条边。

· 也就是说在邻接多重表里边,边表存放的是一条边,而不是一个顶点。

//无向图的邻接多重表存储结构
#define MAX_VERTEX_NUM 20
typedef char InfoType;     //附加信息类型
typedef int VertexType;    //顶点数据类型
typedef enum {unvisited,visited}VisitIf;
typedef struct EBox{
    VisitIf mark;   //访问标记
    int ivex,jvex;  //该边依附的两个顶点的位置
    struct EBox *ilink,*jlink;//分别指向依附这两个顶点的下一条边
    InfoType *info; //该边信息指针
}EBox;
typedef struct VexBox{
    VertexType data;
    EBox *firstedge;//指向第一条依附该顶点的边
}VexBox;
typedef struct {
    VexBox adjmulist[MAX_VERTEX_NUM];
    int vexnum,edgenum;//无向图的当前顶点数和边数
}AMLGraph;

 

图的遍历(这里以邻接矩阵表示)

 

· 对于图的遍历,因为它的任一顶点都可以和其余的所有顶点相邻接,

  因此极有可能村长重复走过某个顶点或漏了某个顶点的遍历过程。

· 如果要避免这种情况,就需要合理科学地遍历,常见的有两种遍历:

  深度优先遍历和广度优先遍历。

深度优先遍历

· 也称为深度优先搜索,简称为DFS。

· 从图的某一点按照某个原则进行深度遍历,把遍历过的点作一个标记,在一个分支上

  遍历到底后,逐个顶点退回,遍历其他分支,直到退回到原点,遍历完毕。

· 类似于树的先根遍历,是树的先根遍历的推广。

假设先从A遍历

代码实现:

int FirstAdjVex(MGraph G, int v) {
    //返回v(序号)的第一个相邻节点(序号)
    if (v > G.vexnum || v < 0)
        return -1;
    int i, j;
    j = 0;
    //如果是网
    if (G.kind == DN || G.kind == UDN)
        j = INFINITY;
    for (i = 0; i < G.vexnum; i++)
        if (G.arcs[v][i].adj != j)
            return i;
    return -1;
}

int NextAdjVex(MGraph G, int v, int w) {
    //w是v的相邻节点,返回v相对w的下一个节点的序号,否则返回-1
    int i, j;
    j = 0;
    //如果为网
    if (G.kind == DN || G.kind == UDN)
        j = INFINITY;
    //两顶点不相邻
    if (G.arcs[v][w].adj == j)
        return -1;
    //从w之后的结点开始
    for (i = w + 1; i < G.vexnum; i++)
        if (G.arcs[v][i].adj != j)
            return i;
    return -1;
}


int visited[MAX_VERTEX_NUM];//访问标记数组

void DFS(MGraph G, int v) {
    //从第v个顶点出发递归地深度优先遍历图G
    visited[v] = 1;
    printf("%d", G.vexs[v]);//访问第v个顶点
    for (int w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w))
        if (!visited[w])//对未访问的邻接顶点w递归调用DFS
            DFS(G, w);
}

void DFSTraverse(MGraph G) {

    for (int i = 0; i < G.vexnum; ++i) {
        visited[i] = 0;   //访问标志数组初始化
    }
    for (int j = 0; j < G.vexnum; ++j) {
        if (!visited[j])
            DFS(G, j);   //对未访问的顶点调用DFS
    }
}
int main() {
    MGraph G;
    CreateGraph(G);
    DFSTraverse(G);
    return 0;
}

 

广度优先遍历

 

· 又称为广度优先搜索,简称BFS。

· 要实现广度优先遍历,可以利用队列来实现。

· 访问A后,把与A相邻的结点入队列,A标记已入过列。访问B,把B相邻的C,I,G入列

  并标记已入列。访问F,把F相邻的G,E入列,因为G已被标记,所以只要E入列,依次类推。

代码实现:

void BFSTraverse(MGraph G) {
    //按广度优先非递归遍历图G,使用辅助队列Q和访问标志数组visited
    int v, w;
    VertexType u;
    LinkQueue Q;
    for (v = 0; v < G.vexnum; ++v)//标志数组初始化
        visited[v] = 0;
    IniteQueue(Q);  //生成队列
    for (v = 0; v < G.vexnum; ++v)
        if (!visited[v]) {
            visited[v] = 1;
            printf("%d", G.vexs[v]);
            EnQueue(Q, v);   //v入队列
            while (!QueueEmpty(Q)) {
                DeQueue(Q, u);   //队头元素出队并至为u
                for (w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w))
                    if (!visited[w]) {    //w为u的尚未访问的邻接点
                        visited[w] = 1;
                        printf("%d", G.vexs[w]);
                        EnQueue(Q, w);
                    }
            }
        }
}

 

最小生成树

· 普里姆算法:寻找起始顶点邻接的最短边,并入集合,再寻找邻接的最短边 ,不能有环。

 

· 克鲁斯卡尔算法,寻找最短边,不能有环。

 

 

直接上两种算法的代码:

//普里姆算法
struct {
    VertexType adjvex;
    VRType lowcost;
}closedge[MAX_VERTEX_NUM];
int minmum(MGraph G){

    int min=INFINITY;
    int index=-1;
    for (int i = 0; i < G.vexnum; ++i) {
        //最小值大于 该边的权值 且 该边的权值不为 0
        if(min>closedge[i].lowcost&&closedge[i].lowcost!=0){
            min=closedge[i].lowcost;
            index=i;
        }
    }
    return index;
}

void MiniSpanTree_PRIM(MGraph G,VertexType u){
    //书本P174表格
    int k=LocateVex(G,u);//该顶点位置      k=0
    for (int j = 0; j < G.vexnum; ++j)//辅助数组初始化
        if(j!=k)//相当于u顶点 边的信息
            closedge[j]={u,G.arcs[k][j].adj};//{adjvex,lowcost}
    closedge[k].lowcost=0;//初始,U={u},把u并入集合
    for (int i = 1; i < G.vexnum; ++i) {//选择其余G.vexnum-1个顶点
        k=minmum(G);    //求出T的下一个结点,第k顶点
        printf("%d-->%d\n",closedge[k].adjvex,G.vexs[k]);//输出 生成树边
        closedge[k].lowcost=0;//第k顶点并入U集
        for(int j=0;j<G.vexnum;++j)
            if(G.arcs[k][j].adj<closedge[j].lowcost)
                //新顶点并入U后重新选择最小边
                closedge[j]={G.vexs[k],G.arcs[k][j].adj};
    }
}


//克鲁斯卡尔算法,自己写的
typedef struct {
    int beginNum;   //存放顶点下标
    int endNum;
    VertexType begin;//顶点名称
    VertexType end;
    VRType weight;//权重
}Edge[MAX_EDGE_NUM];//边集数组

void MiniSpanTree_Kruskal(MGraph G){
    Edge edge[G.arcnum];
    int set[G.vexnum];

    //初始化辅助数组
    for (int i = 0; i < G.vexnum; ++i) {
        set[i]=i;//{0,1,2,3,4,5,....}
    }
    int k=0;
    //得到边集数组
    for (int i = 0; i < G.vexnum; ++i)
        for (int j = 1; j < G.vexnum; ++j)
            if (i<j&&G.arcs[i][j].adj!=INFINITY){
                edge[k]->beginNum=i;
                edge[k]->endNum=j;
                edge[k]->begin=G.vexs[i];
                edge[k]->end=G.vexs[j];
                edge[k]->weight=G.arcs[i][j].adj;
                k++;
            }
    VertexType temp;
    VRType adj;
    //边集数组,按权重从小到大
    for (int i = 0; i < G.arcnum; ++i)
        for (int j = i+1; j < G.arcnum; ++j)
            if(edge[i]->weight>edge[j]->weight){
                temp=edge[i]->begin;
                edge[i]->begin=edge[j]->begin;
                edge[j]->begin=temp;
                temp=edge[i]->end;
                edge[i]->end=edge[j]->end;
                edge[j]->end=temp;
                adj=edge[i]->weight;
                edge[i]->weight=edge[j]->weight;
                edge[j]->weight=adj;
                k=edge[i]->beginNum;
                edge[i]->beginNum=edge[j]->beginNum;
                edge[j]->beginNum=k;
                k=edge[i]->endNum;
                edge[i]->endNum=edge[j]->endNum;
                edge[j]->endNum=k;
            }
    int a,b;
    for (int i = 0; i < G.arcnum; ++i) {
        if(set[edge[i]->beginNum]!=set[edge[i]->endNum]){
            //如果辅助数组中,对应数字不同
            //输出该边,修改辅助数组中数字
            //如果set[edge[i]->beginNum]为a
            //set[edge[i]->endNum]为b
            //那么把辅助数组中所有为b的改为a
            printf("%d->%d  weight:%d\n",edge[i]->begin,edge[i]->end,edge[i]->weight);
            a=set[edge[i]->beginNum];
            b=set[edge[i]->endNum];
            for (int j = 0; j < G.vexnum; ++j)
                if(set[j]==b)
                    set[j]=a;
        }
    }
}

· 普里姆算法的时间复杂度为O(n^2),与网中的边数无关,适用于求边稠密的最小生成树。

· 库鲁斯卡尔的时间复杂度为O(eloge),e为网中边的数目,适合于求边稀疏的最小生成树。

 

拓扑排序

· 一个无环的有向图称为无环图(Directed Acyclic Graph),简称DAG图。

· 所有的工程或者某种流程都可以分为若干个小的工程或者阶段,

  称这些小的工程或阶段为"活动"。

· 在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,

  这样的有向图为顶点表示活动的网,称之为AOV网。

· AOV网中的弧表示活动之间存在某种制约关系,且不能存在回路。

· 拓扑序列:设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2,...,Vn

  满足若从顶点Vi到Vj有一条路径,则在顶点序列中顶点Vi必在顶点Vj之前。则称

  这样的顶点序列为拓扑序列。

· 拓扑排序:就是对一个有向图构造拓扑序列的过程。

· 拓扑序列(其中一种):

1,13,4,8,15,5,2,3,10,11,12,7,6,9

注:前面的必须指向后面的。

· 对AOV网进行拓扑排序的方法和步骤如下:

    -从AOV网中选择一个没有前驱的顶点(入度为0)并输出他。

    -从网中删去该顶点,并且删去从该顶点发出的全部有向边。

    -重复上述两步,直到剩余网中不再存在没有前驱的顶点为止。

· 由刚才我们那幅AOV网图,我们可以用邻接表(因为需要删除顶点,所以

  选择邻接表会更加方便)数据结构表示:

代码实现:

int TopologicalSort(ALGraph G){
    //有向图G采用邻接表存储结构
    //若G无回路,则输出G的顶点的一个拓扑序列并返回1,否则0
    SqStack S;
    ArcNode *p;
    int i,k;
    InitStack(S);
    for ( i = 0; i < G.vexnum; ++i) {
        if(G.vertices[i]->in==0)
            Push(S,i);  //入度为0的顶点下标入栈
    }
    int count=0;    //对输出顶点计数
    while (!StackEmpty(S)){
        Pop(S,i);   //输出i号顶点,并计数
        printf("%d,%d\n",i,G.vertices[i]->data);
        ++count;
        for(p=G.vertices[i]->firstarc;p;p=p->nextarc){
            k=p->adjvex;    //对i号顶点的每个邻接点的入度减1
            if(!(--G.vertices[k]->in))  //若入度为0,则入栈
                Push(S,k);
        }
    }
    if(count<G.vexnum)
        return 0;
    else
        return 1;
}

算法时间复杂度:

    -对一个具有n个 顶点,e条边的网来说,初始建立入度为零的顶点栈,

      要检查所有顶点一次,执行时间为O(n)。

    -排序中,若AOV网无回路,则每个顶点入出栈各一次次,每个表结点

      被检查一次,因而执行时间是O(n+e)。

    -所以整个算法时间复杂度是O(n+e)。

 

关键路径

· AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,

  用边上的权值表示活动的持续时间,这种有向图的边表示活动的网称为AOE网。

· 把AOE网中入度为零的顶点称为始点或源点,出度为零的顶点称为终点或汇点。

· 路径长度最长的路径叫做关键路径。

    -etv(Earliest Time Of Vertex):事件最早发生时间,就是顶点的最早发生时间。

    -ltv(Latest Time Of Vertex):事件最晚发生时间,就是每个顶点对应的事件最晚

      需要开始的时间,如果超出此时间将会延误整个工期。

      从后面往前看,减去这个工时就为最晚发生时间

      例如:C8的etv为12,C6的ltv为12-4=8

    -ete(Earliest Time Of Edge):活动的最早开工时间,就是弧的最早发生时间。

    -lte(Latest Time Of Edge):活动的最晚发生时间,就是不推迟工期的最晚开工时间。

      类似ltv算法,也要倒着过来。

代码实现:

//拓扑排序
int TopologicalSort(ALGraph G) {
    //有向图G采用邻接表存储结构
    //若G无回路,则输出G的顶点的一个拓扑序列并返回1,否则0
    SqStack S;
    ArcNode *p;
    int i, k;
    InitStack(S);
    for (i = 0; i < G.vexnum; ++i) {
        if (G.vertices[i]->in == 0)
            Push(S, i);  //入度为0的顶点下标入栈
    }
    int count = 0;    //对输出顶点计数
    while (!StackEmpty(S)) {
        Pop(S, i);   //输出i号顶点,并计数
        printf("%d,%d\n", i, G.vertices[i]->data);
        ++count;
        for (p = G.vertices[i]->firstarc; p; p = p->nextarc) {
            k = p->adjvex;    //对i号顶点的每个邻接点的入度减1
            if (!(--G.vertices[k]->in))  //若入度为0,则入栈
                Push(S, k);
        }
    }
    if (count < G.vexnum)
        return 0;
    else
        return 1;
}

//关键路径
int ve[];//顶点最早开始时间
int vl[];//顶点最晚开始时间

int TopologicalOrder(ALGraph G, SqStack &T) {
    //有向网G采用邻接表存储结构,求各顶点事件的最早发生时间ve(全局变量)
    //T为拓扑序列顶点栈,S为零入度顶点栈
    //若G无回路,则用栈T返回G的一个拓扑序列,返回1,否则0
    SqStack S;
    ArcNode *p;
    int i, k, j;
    InitStack(S);
    InitStack(T);
    for (i = 0; i < G.vexnum; ++i) {
        if (G.vertices[i]->in == 0)
            Push(S, i);  //入度为0的顶点下标入栈
        ve[i] = 0;    //初始化
    }
    int count = 0;
    while (!StackEmpty(S)) {
        Pop(S, j);      //j号顶点入T栈并计数
        Push(T, j);
        ++count;
        for (p = G.vertices[j]->firstarc; p; p = p->nextarc) {
            k = p->adjvex;  //对j号顶点的每个邻接点的入度减1
            if (!(--G.vertices[k]->in)) //若入度减为0,则入栈
                Push(S, k);
            //当前出栈j号顶点的 最早发生时间+ 当前循环连接k号顶点的权值 >k号顶点最早 发生时间
            if (ve[j] + *(p->info) > ve[k])
                //k号顶点的最早发生时间=出栈顶点号的最早发生时间+与之相连边的权值
                ve[k] = ve[j] + *(p->info);
        }
    }
    if (count < G.vexnum)   //该有向网有回路
        return 0;
    else
        return 1;
}

int CriticalPath(ALGraph G) {
    //G为有向网,输出G的各项关键活动
    int j, k, dut, ee, el;
    char tag;
    SqStack T;
    ArcNode *p;
    if (!TopologicalOrder(G, T))
        return 0;
    for (int i = 0; i < G.vexnum; ++i)  //初始化顶点事件的最迟发生时间
        vl[i] = ve[G.vexnum - 1];
    while (!StackEmpty(T))      //按拓扑逆序求各顶点的vl值
        //依附于j号顶点的第一条边(包含边权重和指向的顶点)
        for (Pop(T, j), p = G.vertices[j]->firstarc; p; p = p->nextarc) {
            //该弧指向顶点的位置k
            k = p->adjvex;
            //该弧权重
            dut = *(p->info);
            //k的最迟发生时间-该弧权重<初始顶点最迟发生时间
            if (vl[k] - dut < vl[j])
                vl[j] = vl[k] - dut;
        }
    for (j = 0; j < G.vexnum; ++j)
        for (p = G.vertices[j]->firstarc; p; p = p->nextarc) {
            k = p->adjvex;
            dut = *(p->info);
            //注意ve,vl是顶点的最早和最晚发生时间
            //ee,el才是弧的最早和最晚发生时间
            //关键路径是对弧而言
            ee = ve[j];//该弧最早发生时间=该弧起始顶点最早发生时间
            el = vl[k] - dut;//该弧最晚发生时间=该弧结束顶点最晚发生时间-权重
            //带* 为关键
            tag = (ee == el) ? '*' : '';
            printf("起始顶点:%d 结束顶点:%d 该弧权重:%d 最早开始时间:%d 最晚开始时间:%d 是否关键:%c", j, k, dut, ee, el, tag);
        }
}

 

· 这两种算法的时间复杂度均为O(n+e),前一种算法的常数因子要小些。由于计算弧的活动

 

  最早开始时间和最迟开始时间的复杂度为O(e),所以总的求关键路径的时间复杂度O(n+e)

 

最短路径

· 在网图和非网图中,最短路径的含义是不同的。

    -网图是两顶点经过的边上权值之和最少的路径。

    -非网图是两顶点之间经过的边数最少的路径。可以看成网图,权值都为1。

· 把路径起始的第一个顶点称为源点,最后一个顶点称为终点。

· 关于最短路径,分为两种算法:

迪杰斯特拉算法

git图演示

    -初始所有顶点路径长为∞,设源点为0。

    -搜索该源顶点的路径,依次标上花费路径长度,然后归入S集(表示已遍历)

    -逐个搜索除S集合外所有已经标有花费路径长度的顶点,无需遍历归入S集顶点

      遍历到其他顶点,若花费小于其标值,则修改之,遍历完成后,并入S集。

    -重复上步步骤。

//最短路径,迪杰斯特拉算法
typedef int Patharc[MAX_VERTEX_NUM];//用于存储最短路径下标的数组
typedef int ShortPathTable[MAX_VERTEX_NUM];//用于存储到各点最短路径的权值和

void ShortestPath_DIJ(MGraph G, int V0, Patharc &P, ShortPathTable &D) {
    int v, w, min, k = NULL;
    int final[MAX_VERTEX_NUM];  //final[w]=1 表示已经求得顶点V0到Vw的最短路径
    //初始化数据
    for (v = 0; v < G.vexnum; v++) {
        final[v] = 0;             //全部顶点初始化为未找到最短路径
        D[v] = G.arcs[V0][v].adj; //将与V0点有连线的顶点加上权值
        P[v] = 0;                 //初始化路径数组P为0
    }
    D[V0] = 0;    //V0至V0路径为0
    final[V0] = 1;//V0至V0不需要求路径
    //开始主循环,每次求得V0到某个v顶点的最短路径
    for (v = 1; v < G.vexnum; v++) {
        min = INFINITY;
        //循环后得到一个已知最短路径的顶点,作为发散修正的顶点
        for (w = 0; w < G.vexnum; w++)
            if (!final[w] && D[w] < min) {
                k = w;
                min = D[w];
            }
        final[k] = 1; //将目前找到的最近的顶点置1
        //修正当前最短路径 及距离
        //从该顶点发散出去的各个顶点距离修正
        for (w = 0; w < G.vexnum; w++)
            //如果经过v顶点的路径比现在这条路径的长度短的话,更新
            if (!final[w] && (min + G.arcs[k][w].adj < D[w])) {
                D[w] = min + G.arcs[k][w].adj;//修改当前路径长度
                P[w] = k;     //存放前驱顶点
            }

    }
}

· 第一个FOR循环的时间复杂度是O(n),第二个FOR循环共进行n-1次,

  每次执行时间是O(n),所以总的时间复杂度是O(n^2)。

· 如果用带权的邻接表作为有向图的存储结构,则虽然修改D的时间可以减少,

  但由于在D向量中选择最小分量的时间不变,所以总时间仍为O(n^2)。

· 如果只希望找到源点到某一个特定终点的最短路径,这个问题和求

 源点到其他所有顶点的最短路径一样复杂,其时间复杂度也是O(n^2)。

 

佛洛依德算法

· 从任意阶段i到任意节点j的最短路径只有2种可能,一种是直接从i到j另一种是

  从i经过若干个节点k到j。所以,假设Dis(i,j)为节点u到节点v的最短路径的距离,

  对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,

  证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j),

  这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。

· 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。

· 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。

十字交叉法

方法:两条线,从左上角开始计算一直到右下角如图所示

给出矩阵,其中矩阵A是邻接矩阵,而矩阵Path记录u,v两点之间最短路径所必须经过的点。

算法实现:

//最短路径,佛洛依德算法
typedef int PathMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef int DistancMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

void ShortestPath_FLOYD(MGraph G, PathMatrix &P, DistancMatrix &D) {
    int v, w, k;
    //初始化D和P
    for (v = 0; v < G.vexnum; v++)
        for (w = 0; w < G.vexnum; w++) {
            D[v][w] = G.arcs[v][w].adj;
            P[v][w] = -1;
        }
    //算法核心
    for (k = 0; k < G.vexnum; k++)
        for (v = 0; v < G.vexnum; v++)
            for (w = 0; w < G.vexnum; w++)
                if (D[v][w] > D[v][k] + D[k][w]) {
                    D[v][w] = D[v][k] + D[k][w];
                    P[v][w] = P[v][k];
                }
}

· 此算法时间复杂度为O(n^3)。

 

 

数据结构》(C语言)是为“数据结构”课程编写的教材,也可作为学习数据结构及其算法的C程序设计的参数教材。学了数据结构后,许多以前写起来很繁杂的代码现在写起来很清晰明了. 本书的前半部分从抽象数据类型的角度讨论各种基本类型的数据结构及其应用;后半部分主要讨论查找和排序的各种实现方法及其综合分析比较。 全书采用类C语言作为数据结构和算法的描述语言。 本书概念表述严谨,逻辑推理严密,语言精炼,用词达意,并有配套出的《数据结构题集》(C语言),便于教学,又便于自学。 本书后附有光盘。光盘内容可在DOS环境下运行的以类C语言描述的“数据结构算法动态模拟辅助教学软件,以及在Windows环境下运行的以类PASCAL或类C两种语言描述的“数据结构算法动态模拟辅助教学软件”。内附 数据结构算法实现(严蔚敏配套实现程序) 目录: 第1章 绪论 1.1 什么是数据结构 1.2 基本概念和术语 1.3 抽象数据类型的表现与实现 1.4 算法和算法分析 第2章 线性表 2.1 线性表的类型定义 2.2 线性表的顺序表示和实现 2.3 线性表的链式表示和实现 2.4 一元多项式的表示及相加 第3章 栈和队列 3.1 栈 3.2 栈的应有和举例 3.3 栈与递归的实现 3.4 队列 3.5 离散事件模拟 第4章 串 4.1 串类型的定义 4.2 串的表示和实现 4.3 串的模式匹配算法 4.4 串操作应用举例 第5章 数组和广义表 5.1 数组的定义 5.2 数组的顺序表现和实现 5.3 矩阵的压缩存储 5.4 广义表的定义 5.5 广义表的储存结构 5.6 m元多项式的表示 5.7 广义表的递归算法第6章 树和二叉树 6.1 树的定义和基本术语 6.2 二叉树 6.2.1 二叉树的定义 6.2.2 二叉树的性质 6.2.3 二叉树的存储结构 6.3 遍历二叉树和线索二叉树 6.3.1 遍历二叉树 6.3.2 线索二叉树 6.4 树和森林 6.4.1 树的存储结构 6.4.2 森林与二叉树的转换 6.4.3 树和森林的遍历 6.5 树与等价问题 6.6 赫夫曼树及其应用 6.6.1 最优二叉树(赫夫曼树) 6.6.2 赫夫曼编码 6.7 回溯法与树的遍历 6.8 树的计数 第7章 7.1 的定义和术语 7.2 存储结构 7.2.1 数组表示法 7.2.2 邻接表 7.2.3 十字链表 7.2.4 邻接多重表 7.3 的遍历 7.3.1 深度优先搜索 7.3.2 广度优先搜索 7.4 的连通性问题 7.4.1 无向的连通分量和生成树 7.4.2 有向的强连通分量 7.4.3 最小生成树 7.4.4 关节点和重连通分量 7.5 有向无环及其应用 7.5.1 拓扑排序 7.5.2 关键路径 7.6 最短路径 7.6.1 从某个源点到其余各顶点的最短路径 7.6.2 每一对顶点之间的最短路径 第8章 动态存储管理 8.1 概述 8.2 可利用空间表及分配方法 8.3 边界标识法 8.3.1 可利用空间表的结构 8.3.2 分配算法 8.3.3 回收算法 8.4 伙伴系统 8.4.1 可利用空间表的结构 8.4.2 分配算法 8.4.3 回收算法 8.5 无用单元收集 8.6 存储紧缩 第9章 查找 9.1 静态查找表 9.1.1 顺序表的查找 9.1.2 有序表的查找 9.1.3 静态树表的查找 9.1.4 索引顺序表的查找 9.2 动态查找表 9.2.1 二叉排序树和平衡二叉树 9.2.2 B树和B+树 9.2.3 键树 9.3 哈希表 9.3.1 什么是哈希表 9.3.2 哈希函数的构造方法 9.3.3 处理冲突的方法 9.3.4 哈希表的查找及其分析 第10章 内部排序 10.1 概述 10.2 插入排序 10.2.1 直接插入排序 10.2.2 其他插入排序 10.2.3 希尔排序 10.3 快速排序 10.4 选择排序 10.4.1 简单选择排序 10.4.2 树形选择排序 10.4.3 堆排序 10.5 归并排序 10.6 基数排序 10.6.1 多关键字的排序 10.6.2 链式基数排序 10.7 各种内部排序方法的比较讨论 第11章 外部排序 11.1 外存信息的存取 11.2 外部排序的方法 11.3 多路平衡归并的实现 11.4 置换一选择排序 11.5 最佳归并树 第12章 文件 12.1 有关文件的基本概念 12.2 顺序文件 12.3 索引文件 12.4 ISAM文件和VSAM文件 12.4.1 ISAM文件 12.4.2 VSAM文件 12.5 直接存取文件(散列文件) 12.6 多关键字文件 12.6.1 多重表文件 12.6.2 倒排文件 附录A 名词索引 附录B 函数索引 参考书目
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值