数据结构 图

知识图谱

1.相关词汇

欧拉通路:走所有边,整张图度为0(出度为负,入度为正)

哈密尔顿通路:走所有点

自相似结构:递归(去掉结点还是图)adjacent

弧头弧尾:有向弧箭头方向为弧头

在这里插入图片描述

最小生成树

在这里插入图片描述

2.图的两种表达方式

邻接矩阵

当边比较多的时候用矩阵表示

表示:二维数组,十字链表

有向:不一定对称,无向:一定对称

顶点向量:将所有顶点按顺序存到数组中

先看定义

typedef struct ArcNode {//定义边的结构体
    AdjType adjvex; //对于无权图,用1或0表示是否相邻;对带权图,则为权值类型
    OtherInfo info;
} ArcNode;

//这个类型是在教材基础上增加的,其目的是为了保持与其他图的类型一致性
typedef struct VertexNode {//定义顶点的结构体
    VertexData data;   //顶点数据,一般为字符标签
} VertexNode;

//定义邻接矩阵表示的图类型
typedef struct Graph {
    ArcNode arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //邻接矩阵的二维数组
    VertexNode vertex[MAX_VERTEX_NUM];            //创建顶点向量
    int vexnum, arcnum;                           //图的顶点数和弧数
    GraphKind kind;                               //图的种类标志
} AdjMatrix, Graph;

data存储如下

1,6,11
ABCDEF
A,B,50
A,E,45
A,C,10
B,E,10
B,C,15
C,A,20
C,D,15
D,B,20
D,E,35
E,D,30
F,D,3
v0 v1 v2 v3 v4 v5
A
void CreateGraph(Graph *G) {
    int i, j, k, weight; // 用于循环和临时存储数据的变量
    int kind; // 存储图的类型
    VertexData v1, v2; // 用于存储单条边的起始和结束顶点

    // 读取图的类型、顶点数和边数
    scanf("%d,%d,%d", &kind, &G->vexnum, &G->arcnum);
    getchar(); // 吸收换行符,准备读取下一个数据

    G->kind = kind; // 存储图的类型

    // 初始化所有边的权重为无穷大
    for (i = 0; i < G->vexnum; i++)
        for (j = 0; j < G->vexnum; j++)
            G->arcs[i][j].adjvex = INFINITY;

    // 读入每个顶点的标识符
    for (i = 0; i < G->vexnum; i++) {
        scanf("%c", &G->vertex[i].data);//存入顶点数组
    }
    getchar(); // 吸收换行符

    // 读入每条边的起始顶点、结束顶点及权重,并建立相应的弧
    for (k = 0; k < G->arcnum; k++) {
        scanf("%c,%c,%d", &v1, &v2, &weight); // 输入一条弧的两个顶点及权值
        getchar(); // 吸收换行符
        i = LocateVertex(G, v1); // 查找起始顶点的索引
        j = LocateVertex(G, v2); // 查找结束顶点的索引
        G->arcs[i][j].adjvex = weight; // 建立弧
        if (G->kind > DN) // 如果图是有向加权图,则同时建立反向弧
            G->arcs[j][i].adjvex = weight;
    }
}
通过邻接矩阵算入度和出度

入度:这一行的非零总和

出度:这一列的非零总和

通过入度和出度判断欧拉回路:图的出度总和和入度总和做差

邻接表

当定点比较多的时候使用邻接表

链表存储结点的邻接点——头插法(因为头插法是最快的)但是相比逆序

邻接表和邻接向量顶点多

void CreateGraph(Graph *G) {
    // 用于循环遍历和临时存储的变量
    int i, j, k;
    int kind, w; // kind表示图的类型,w表示边的权重
    ArcNode *arc; // 指向边结点的指针
    VertexData v1, v2; // 用于存储顶点信息

    // 读取图的类型、顶点个数和边数
    scanf("%d,%d,%d", &kind, &G->vexnum, &G->arcnum);
    getchar(); // 吸收换行符

    G->kind = kind; // 存储图的类型

    // 初始化顶点信息,将边表的第一邻接点置为空
    for (i = 0; i < G->vexnum; ++i) {
        scanf("%c", &G->vertex[i].data);
        G->vertex[i].firstarc = NULL;
    }
    getchar(); // 吸收换行符

    // 输入并创建边的信息
    for (k = 0; k < G->arcnum; ++k) {
        scanf("%c,%c,%d", &v1, &v2, &w);
        getchar(); // 吸收换行符

        // 根据顶点信息查找其在顶点数组中的位置
        i = LocateVertex(G, v1);
        j = LocateVertex(G, v2);

        // 创建新的边结点
        arc = (ArcNode *)malloc(sizeof(ArcNode));
        arc->adjvex = j; // 存储邻接顶点的索引
        arc->nextarc = G->vertex[i].firstarc; // 将新边插入到边表的头部
        arc->info.weight = w; // 存储边的权重信息
        G->vertex[i].firstarc = arc; // 更新顶点的边表头指针
    }
}

3.深度优先搜索:

找第一个邻接点:在一行中从左到右找第一个不为0的

相对于上一点的第一个邻接点:基于此位置向右搜索的下一个

void SearchRecur(Graph *G, int v0, CALLBACK visit) {//v0为搜索起始顶点的索引
    visit(G->vertex[v0].data);
    visited[v0] = true;//设为访问过

    Arc w = FirstAdjVertex(G, v0);//用弧的结构体获取邻接顶点
    while(w.adj != -1){
        if (!visited[w.adj]) {
            SearchRecur(G, w.adj, visit);
        }
        w = NextAdjVertex(G, v0, w);
    }

}

3.1.循环+自定义栈消除递归

理解

void SearchStack(Graph *G, int v0, CALLBACK visit) {
    Stack s, *S = &s;
    Arc w;
    int v;  //v表示正在处理的顶点的索引

    InitStack(&S);

    Push(S, v0); //开始的时候要先把v0入栈
    while (!IsEmpty(S)) //然后形成闭环
    {
        Pop(S, &v);
        if (!visited[v]) 
        {
            visit(G->vertex[v].data);
            visited[v] = true;
        }
        w = FirstAdjVertex(G, v);//用w接收v的第一个邻接点
        while (w.adj != -1) //循环遍历邻接点
        {
            if (!visited[w.adj]) 
            {
                Push(S, w.adj);//若没有访问过则入栈
                break;  //!!!这条语句对于adjmatrix和adjlist有不同的效果
            }
            w = NextAdjVertex(G, v, w);//更新w为下一个邻接点
        }
    }

    ClearStack(S);
}

在这里插入图片描述

递归:先找到第一个邻接点,往下走

如果没有break语句跳出while,则会先在while中遍历邻接点,沿着最后一个邻接点往下走,调用次数为1次

加上break后,在跳出while后会沿着第一个邻接点向下遍历,算法调用次数为3次,正好A有三个邻接点

4.广度优先

void SearchQueue(Graph *G, int v0, CALLBACK visit) {
    int v, j; // 用于遍历元素
    Arc w;
    Queue q, *Q = &q;

    visit(G->vertex[v0].data);
    visited[v0] = true; // 设置该顶点i已被访问

    InitQueue(Q); // 初始化队列

    EnterQueue(Q, v0);
    while(!IsEmpty(Q))
    {
        DeleteQueue(Q, &v);//&v获取队列第一个元素,用v接收
        
        
        w=FirstAdjVertex(G, v);//找v的第一个邻接点,以边的方式记录,即用w接收
        while (w.adj!=-1)//循环v通过w连的的邻接点
        {
            j=w.adj; //j接收w的邻接点预防错误
            if(!visited[j])// 如果没有访问过,则依次入队
            {
                visit(G->vertex[j].data);
                visited[j]=true;
                EnterQueue(Q, j);
            }
            w=NextAdjVertex(G, v, w);
        }
        
    }

    ClearQueue(Q);
}

邻接矩阵和邻接表在寻找first和next顶点时逻辑不同

4.1.在邻接表中寻找第一个邻接点或者下一个邻接点
Arc FirstAdjVertex(struct Graph *G, int v) {
    Arc w = {-1, G->vertex[v].firstarc};
    
    if (w.arc != NULL) w.adj = w.arc->adjvex;//若w有邻接边,w的邻接点就是结构体中的邻接点

    return w;
}

Arc NextAdjVertex(struct Graph *G, int v, Arc w) {
    Arc w2 = {-1, w.arc->nextarc};//w2是下一个邻接边
    
    if (w2.arc != NULL) w2.adj = w2.arc->adjvex;

    return w2;
}

其中Arc定义

typedef struct {
    int adj; //顶点位序,邻接点在顶点数组中的位序
    struct ArcNode *arc; //顶点为弧尾的弧,若为矩阵此成员无用
} Arc;
//存储邻接边的方法:必须使用结构体来记录边和所连顶点

5.简单路径算法

void path(Graph *G, int u, int v) {
   
    int pre[G->vexnum];//前驱结点数组

    for (int i = 0; i < G->vexnum; i++)
        pre[i] = -1;
    pre[u] = -2; //将pre[u]置为非-1,表示第u个顶点已被访问

    if (DFS(G, u, v, pre)) //用深度优先搜索找一条从u到v的简单路径。
        print_path(G, pre, v); //输出路径
    else
        printf("There's no path leads from %c to %c\n", G->vertex[u].data, G->vertex[v].data);
}

其中pre数组记录了一路的过程的向量,可以通过回溯找到路径

基于深度优先搜索的寻找简单路径

姑且将j成为当前结点,pre[j.adj]为当前节点对应张量位置的值

bool DFS(Graph *G, int u, int v, int *pre) {//从u到v简单路径
    for (Arc j = FirstAdjVertex(G, u); j.adj >= 0; j = NextAdjVertex(G, u, j)) {
    //  j是u的第一个出度                             j变为下一个出度
        if (pre[j.adj] == -1) {//如果当前结点未被访问,j.adj为出度所连的结点
            pre[j.adj] = u;  //u是这一步的前驱结点
            if (j.adj == v)
                return true;

            if (DFS(G, j.adj, v, pre))
                return true;

            pre[j.adj] = -1; //为了保持pre数组的“干净”,可以有这句。这句去掉不影响算法。
        }
    }

    return false;
}

6.hamilton通路

#include <stdio.h>
#include <stdbool.h>

#include "auxf.h"
#include "graph.h"

bool visited[MAX_VERTEX_NUM];
int path[MAX_VERTEX_NUM];
int n = 0;

void DisplayPath(Graph *G) {
    for (int j = 0; j < G->vexnum; ++j)
        printf("%3c", G->vertex[path[j]].data);
    putchar('\n');
}

bool DepthFirstSearch(Graph *G, int v0) {
    visited[v0] = true;
    path[n++] = v0;

    if (n == G->vexnum) {
        DisplayPath(G);
        return true;
    }

    Arc w = FirstAdjVertex(G, v0);
    while (w.adj != -1) {
        if (!visited[w.adj] && DepthFirstSearch(G, w.adj))
            return true;
        
        w = NextAdjVertex(G, v0, w);
    }

    visited[v0] = false;
    --n;
    return false;
}

void ResetPath(Graph *G) {
    n = 0;
    for (int j = 0; j < G->vexnum; ++j) visited[j] = false;
}

void Hamilton(Graph *G) {
    for (int i = 0; i < G->vexnum; ++i) {
        ResetPath(G);
        if (DepthFirstSearch(G, i)) return;
    }

    printf("Hamilton path doesn't exist.\n");
}

int main(int argc, char *argv[]) {
    choose_graph_model("hamilton", argc >= 2, argv, NULL);
    graph_figure();

    Graph G;
    CreateGraph(&G);

    Hamilton(&G);

    DestroyGraph(&G);

    close_model();

    return 0;
}
n个顶点的图,有n-1条边,多一条就会形成回路了

7.生成树,最小生成树

7.1.prim算法

画表,第一行为所有顶点,第二行为代价,第三行为起始顶点

7.2.kruskal算法

分成不同数组,先按边找最小边(这条边的两个点为一个数组),找最小边的方法:权值最小且定点属于不同该数组,之后同化为新的数组

8.DAG 有向无环图

  1. 拓扑排序:先发生的事件排在前面

  2. 入度为0的结点可以先做,做完后将邻接点的入度-1,入度减为零的时候入栈,

  3. 复习邻接矩阵和表求出度的方法

    出度:矩阵->找第i列;表->数i后面所连的结点个数

    (邻接表:用一个数组存储所有结点,数组每个元素后面按顺序连接起所有邻接点)

8.1.拓扑排序
//基于邻接表 栈
bool TopoSort(AdjList *G) {
    Stack *S;
    int indegree[MAX_VERTEX_NUM];//用数组记录每个结点的入度
    int i, count, k;
    ArcNode *p;

    FindID(G, indegree);
    InitStack(&S);

    for (i = 0; i < G->vexnum; ++i)
        if (indegree[i] == 0)
            Push(S, i);//将初始入度为0的结点入栈

    count = 0;
    while (!IsEmpty(S)) {
        Pop(S, &i);
        printf("%s  ", Vertex2Name(G->vertex[i].data));
        ++count; /*输出所弹出的i号顶点并计数*/
        p = G->vertex[i].firstarc;//p是顶点i的边(在下面会更新),即i邻接点的入度
        while (p != NULL) {
            k = p->adjvex;//k是p的邻接点
            --indegree[k]; /*i号顶点(p)的每个邻接点k的入度减1*/
            if (indegree[k] == 0)
                Push(S, k); /*若入度减为0,则入栈*/
            p = p->nextarc;
        }
    } /*while*/

    ClearStack(S);

    return count < G->vexnum ? false : true; /*false意味着该有向图含有回路*/
}

在这里插入图片描述

AOV网:以顶点表示活动网络

AOE网:以边表示活动网络(有权图)

注:前一步最大权重的决定下一步开始时间(已做完的分支会等着)

有权图中计算event(结点)最早和最晚开始时间,activity(边)最早和最晚开始时间 看教材

最早开始时间和最晚开始时间不一样,有时间差

最早开始时间和最晚开始时间一样,没有时间差,此边称为关键路径中一条边

8.2.寻找关键路径

两个栈一个用于正向拓扑排序,一个用于存储从第一个栈弹出的结点,即是拓扑的逆序

基于邻接表

问题:为什么邻接点之间时间步可能不同

bool TopoOrder(AdjList *G, Stack *T) {
    int count, i, j, k;
    ArcNode *p;
    int indegree[MAX_VERTEX_NUM]; /*各顶点入度数组*/
    Stack *S;

    InitStack(&T);
    InitStack(&S);       /*初始化栈T,  S*/
    FindID(G, indegree); /*求各个顶点的入度*///id作为数组对应每个顶点id
//将初始id=0的点入栈
    for (i = 0; i < G->vexnum; ++i)
        if (indegree[i] == 0)
            Push(S, i);
//初始化count为0,最早发生时间数组为0
    count = 0;
    for (i = 0; i < G->vexnum; ++i)
        ve[i] = 0; /*初始化最早发生时间*/
//弹出S中初始入度为0的点,并压入栈T,这样循环下去T中元素恰是拓扑排序的逆向顺序
    while (!IsEmpty(S)) {
        Pop(S, &j);
        Push(T, j);
        ++count;
        p = G->vertex[j].firstarc;
        while (p != NULL) {
            k = p->adjvex;//k为p邻接点
            if (--indegree[k] == 0)
                Push(S, k); /*若顶点的入度减为0,则入栈S*/
            if (ve[j] + p->info.weight > ve[k])//ve最早被初始化为0,而在这一步可以将ve逐步累积
                ve[k] = ve[j] + p->info.weight;
            p = p->nextarc;
        }
    }

    ClearStack(S);

    return count >= G->vexnum;
}



bool CriticalPath(AdjList *G) {//计算最晚开始时间,对比最早最晚时间,判断是否包含于关键路径中
    ArcNode *p;
    int i, j, k, dut, ei, li;
    char tag;
    int vl[MAX_VERTEX_NUM]; /*每个顶点的最迟发生时间*/
    Stack *T;

    InitStack(&T);
    if (!TopoOrder(G, T))
        return false;

    for (i = 0; i < G->vexnum; ++i)
        vl[i] = ve[G->vexnum - 1]; /*初始化顶点事件的最迟发生时间*/

    while (!IsEmpty(T)) {        /*按逆拓扑顺序求各顶点的vl值*/
        Pop(T, &j);
        p = G->vertex[j].firstarc;
        while (p != NULL) {
            k = p->adjvex;
            dut = p->info.weight;
            if (vl[k] - dut < vl[j])
                vl[j] = vl[k] - dut;
            p = p->nextarc;
        }
    }

    for (j = 0; j < G->vexnum; ++j) { /*求ei,li和关键活动*/
        p = G->vertex[j].firstarc;//边
        while (p != NULL) {
            k = p->adjvex;//接点
            dut = p->info.weight;
            ei = ve[j];
            li = vl[k] - dut;
            tag = (ei == li) ? '*' : ' ';//如果包含于关键路径则加星号
            printf("%s,%s,%d,%d,%d,%c\n", Vertex2Name(G->vertex[j].data), Vertex2Name(G->vertex[k].data), dut, ei, li, tag);
            p = p->nextarc;
        }
    }
    ClearStack(T);

    return true;
}

在这里插入图片描述

路由router:终端信号发到最近路由器上,路由找下一个最近路由直至到达服务器

8.3.dijkstra算法和贪心寻找

每次找到距离前一个点(或说源顶点)最近的点,然后更新

void ShortestPath_DJS(Graph *G, int v0, WeightType dist[], VertexSet path[]) {
    int i;
    // 初始化dist[i]和path[i]
    for (i = 0; i < G->vexnum; ++i) {
        InitList2(&path[i]);//初始化path数组
        dist[i] = G->arcs[v0][i].adjvex;//起始顶点v0到i的距离
        if (dist[i] < INFINITY) {//如果可达,加入path数组,path的每个元素都是从v0到这的路径,可能是链表或顺序表
            AddTail(&path[i], G->vertex[v0].data);
            AddTail(&path[i], G->vertex[i].data);
        }
    }

    VertexSet s, *S = &s; // s为已找到最短路径的终点集合
    InitList2(S);
    AddTail(S, G->vertex[v0].data);

    int k, min;
    for (int t = 1; t <= G->vexnum - 1; ++t) {
        min = INFINITY;
        for (k = -1, i = 0; i < G->vexnum; ++i)
            if (!Member(G->vertex[i].data, S) && dist[i] < min) {//member用于监测i点是否已经在s中了
                k = i;
                min = dist[i];
            }
        if (k == -1) continue;
        
        AddTail(S, G->vertex[k].data);
        // 更新距离k更远的顶点的最短路径
        for (i = 0; i < G->vexnum; ++i) /*修正dist[i],  i∈V-S*/
             // 如果顶点i尚未被处理,并且存在从k到i的边,且该路径更短,则更新dist[i]和path[i]
            if (!Member(G->vertex[i].data, S) && G->arcs[k][i].adjvex != INFINITY && (dist[k] + G->arcs[k][i].adjvex < dist[i])) {
                dist[i] = dist[k] + G->arcs[k][i].adjvex;
                path[i] = path[k];
                AddTail(&path[i], G->vertex[i].data); /* path[i]=path[k]∪{Vi} */
            }
    }
}
  1. 初始化
    • 使用循环遍历所有顶点(i),将dist[i]设置为G->arcs[v0][i].adjvex的值,表示从v0i的初始路径长度。如果路径长度不是无穷大(INFINITY),则在path[i]列表中添加v0i的顶点数据。
    • 初始化一个名为s的顶点集合S,用于存储已找到最短路径的顶点,并将起始顶点v0添加到S中。
  2. Dijkstra算法核心
    • 使用一个外层循环(t从1到G->vexnum - 1),在每次迭代中找到当前未被包含在S中的顶点中距离v0最近的一个顶点,记为k
    • 将找到的最近顶点k添加到S中。
    • 再次遍历所有顶点(i),检查未被包含在S中的顶点。如果通过k到达i的路径比当前已知的最短路径更短,就更新dist[i]的值,并更新path[i],将k的路径与i合并。
  3. 结束条件
    • 当所有顶点都已被包含在S中(即找到所有顶点的最短路径),或者没有未访问顶点时,算法结束。

在函数执行完毕后,dist[]数组包含了从起始顶点v0到图中每个顶点的最短路径长度,而path[]数组则包含了对应的最短路径的顶点顺序。这个算法适用于没有负权重边的图。

外层循环:遍历除了源顶点的每个顶点,
内层循环:在每次外层循环之间遍历图中每个顶点,查找未被包含在集合 S 中且距离源顶点最近的顶点。
8.4.弗洛伊德算法
void ShortestPath_Floyd(AdjMatrix *G,
                        WeightType dist[][MAX_VERTEX_NUM],
                        VertexSet path[][MAX_VERTEX_NUM]) {
    int i, j, k;
    for (i = 0; i < G->vexnum; ++i)
        for (j = 0; j < G->vexnum; ++j) {
            InitList2(&path[i][j]);
            dist[i][j] = G->arcs[i][j].adjvex;
            if (dist[i][j] < INFINITY) {
                AddTail(&path[i][j], G->vertex[i].data);
                AddTail(&path[i][j], G->vertex[j].data);
            }
        }

    for (k = 0; k < G->vexnum; k++)
        for (i = 0; i < G->vexnum; i++)
            for (j = 0; j < G->vexnum; j++)
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    JoinList(&path[i][k], &path[k][j], &path[i][j]);
                }//三重循环,不断找有没有中间结点k使距离更短
}

图的表示法(掌握理解)

图的搜索

最小生成树算法(理解)(克鲁斯卡尔算法和普利姆算法)

拓扑,关键路径(理解)

迪弗算法

嵌入式多接触linux

图的表示复习:

//邻接矩阵
typedef struct Graph {
    ArcNode arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //邻接矩阵的二维数组
    VertexNode vertex[MAX_VERTEX_NUM];            //创建顶点向量
    int vexnum, arcnum;                           //图的顶点数和弧数
    GraphKind kind;                               //图的种类标志
} AdjMatrix, Graph;

//邻接表
typedef struct {
    int adj; 
    struct ArcNode *arc; 
} Arc;

//邻接表中找到弧,找到j的第一个出度
p = G->vertex[j].firstarc;
p = p->nextarc

//邻接表中找到弧线相连的顶点
 k = p->adjvex;
  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值