C语言数据结构与算法笔记(图结构)

图结构

图也是由多个节点连接而成的,但一个节点可同时连接多个其他节点,多个节点也可以同时指向一个节点。多对多的关系。

基本概念

图一般由两个集合共同组成,一个是非空但有限的顶点集合V,另一个是描述顶点之间连接关系的边集合E。一个图正是由这些顶点(节点)和对应的边组成。图表示为:G=(V,E)

一个图可表示为,一种为无向图(仅仅是连线,不指明方向),有向图(表明了方向)
集合V={A,B,C,D},集合E={(A,B),(B,C),(C,D),(D,A),(C,A)},

每个节点的度就是与其连接的边数,每条边可以包含权值,也可以不包含。

集合V={A,B,C,D},集合E={<A,B>,<B,C>,<C,D>,<D,A>,<C,A>}
如果无向图的一条边(A,B),A、B互为邻接点;有向图的一条边<A,B>,起点A邻接到终点B,有向图的每个节点分为入度和出度,其中入度为与顶点相连且指向该顶点的边的个数,出度则是从该顶点指向邻接顶点的边的个数
在这里插入图片描述

只要图中不出现回路边或是重边,则称其为简单图
在这里插入图片描述
在无向图中,任意两个顶点都有一条边相连,该图为无向完全图
在这里插入图片描述
在有向图中,任意两个顶点之间都有由方向互为相反的两条边连接,该图为有向完全图
在这里插入图片描述
任意两点都是连通的,称这个图为连通图。对有向图,如果图中任意顶点A和B,即从A到B的路径,从B到A的路径,该有向图为强连通图。

对图G=(V,E)和G’=(V’,E’),若满足V’是V的子集,并且E’是E的子集,则称G是G‘的子图。

无向图的极大连通子图称为连通分量,有向图的极大连通子图称为强连通分量。连通子图是原图的子图,并且子图也是连通图,同时应该具有最大的顶点数,即再加入原图的其他顶点,导致子图的不连通,拥有极大顶点数的同时也要包含依附于这点顶点所有边才行。
在这里插入图片描述
图1和图3为连通分量,图2不是连通分量

存储结构

邻接矩阵

邻接矩阵为矩阵表示图中各顶点之间的邻接关系和权值。
对不带权值的图
G i j = { 1 , 无向图的 ( v i , v j ) 或有向图的 < v i , v j > 是图中的边 0 , 无向图的 ( v i , v j ) 或有向图的 < v i , v j > 不是图中的边 G_{ij}=\left\{ \begin{array}{c} 1,\text{无向图的}\left( v_i,v_j \right) \text{或有向图的}<v_i,v_j>\text{是图中的边}\\ 0,\text{无向图的}\left( v_i,v_j \right) \text{或有向图的}<v_i,v_j>\text{不是图中的边}\\ \end{array} \right. Gij={1,无向图的(vi,vj)或有向图的<vi,vj>是图中的边0,无向图的(vi,vj)或有向图的<vi,vj>不是图中的边
对带权值得图
G i j = { w i j , 无向图的 ( v i , v j ) 或有向图的 < v i , v j > 是图中的边 0 或 ∞ , 无向图的 ( v i , v j ) 或有向图的 < v i , v j > 不是图中的边 G_{ij}=\left\{ \begin{array}{c} w_ij,\text{无向图的}\left( v_i,v_j \right) \text{或有向图的}<v_i,v_j>\text{是图中的边}\\ 0或\infty ,\text{无向图的}\left( v_i,v_j \right) \text{或有向图的}<v_i,v_j>\text{不是图中的边}\\ \end{array} \right. Gij={wij,无向图的(vi,vj)或有向图的<vi,vj>是图中的边0,无向图的(vi,vj)或有向图的<vi,vj>不是图中的边

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由于没有自回路顶点,主对角线上得元素全是0,顶点之间是相互连接。
总结

  • 无向图的邻接矩阵必定是一个对称矩阵。
  • 无向图,邻接矩阵的第i行非0(或非 ∞ \infty )的个数就是第i个顶点的度
  • 有向图,邻接矩阵的第i行非0(或非 ∞ \infty )的个数就是第i个顶点的出度(横着),入度(竖的)

代码实现有向图
构建二维数组存放顶点

#define MaxVertex 5 // 最大顶点数

typedef char E; // 顶点存放的数据类型

typedef struct MatrixGraph
{
    int vertexCount, edgeCount; // 顶点和边
    int matrix[MaxVertex][MaxVertex]; // 邻接矩阵
    E data[MaxVertex]; // 各个顶点对应的数据
} * Graph;

创建图(初始化)、加入顶点函数和加入边函数。若创建无向图,得到其邻接矩阵,只需在加入边时,将[a][b]和[b][a]都为1

// 创建图
Graph create()
{
    Graph graph = malloc(sizeof(struct MatrixGraph));
    graph->vertexCount = graph->edgeCount = 0; // 初始化顶点和边
    for(int i = 0 ; i < MaxVertex; ++i) // 对二维矩阵(矩阵)初始化
    {
        for(int j = 0; j < MaxVertex; ++j)
        {
            graph->matrix[i][j] = 0;
        }
    }
    return graph;
}
// 加入顶点
void addVertex(Graph graph, E element)
{
    if(graph->vertexCount >= MaxVertex)
    { // 顶点数量大于最大
        return;
    }
    else
    {   // 依次放入顶点
        graph->data[graph->vertexCount++] = element;
    }
}
// 加入边
void addEdge(Graph graph, int a, int b)
{
    if(graph->matrix[a][b] == 0) // A->B边不存在时
    {
        graph->matrix[a][b] = 1; // 创建边,有向图
        // graph->matrix[b][a] = 1; // 无向图需要加入这
        graph->edgeCount++; // 边的数量加1
    }
}

将邻接图打印输出

// 将图的邻接矩阵打印
void printGraph(Graph graph)
{
    for(int i = -1; i < graph->vertexCount; ++i)
    {
        for(int j = -1; j < graph->vertexCount; ++j)
        {
            if(j == -1)
            {
                printf("%c", 'A'+i);
            }
            else if(i == -1)
            {
                printf("%3c",'A'+j); // 占位为3
            }
            else
            {
                printf("%3d",graph->matrix[i][j]);
            }
        }
        putchar('\n');
    }
}

测试

    //创建有向图(顶点和边)
    Graph graph = create();
    for(int c = 'A'; c <= 'D';++c)
    {
        addVertex(graph,(char) c);
    }
    addEdge(graph,0,1); //A->B
    addEdge(graph,1,2); //B->C
    addEdge(graph,2,3); //C->D
    addEdge(graph,3,0); //D->A
    addEdge(graph,2,0); //C->A
    printGraph(graph);

邻接表

通过数组存放图的信息,在容量上局限。比如当遇到稀疏图时,边数比较少时,大量的位置实际上为0.造成了浪费。
因此考虑链式结构。对图中的每个顶点,建立一个数组,存放一个头结点,将与其邻接的顶点,通过一个链表记录。
在这里插入图片描述
在这里插入图片描述
代码实现有向图的邻接表
定义

#define MaxVertex 5 // 最大顶点数

typedef char E; // 顶点存放的数据类型

typedef struct Node // 普通节点记录邻接顶点信息
{
    int nextVertex;
    struct Node * next;
} * Node;

struct HeadNode // 头结点记录元素
{
    E element;
    struct Node * next;
};
// 定义邻接表
typedef struct AdjacencyGraph
{
    int vertexCount, edgeCount; // 顶点和边
    int matrix[MaxVertex][MaxVertex]; // 邻接矩阵
    struct HeadNode vertex[MaxVertex]; // 头结点
} * Graph;

创建邻接表,加入顶点和边,并打印邻接表函数

// 创建邻接表
Graph create()
{
    Graph graph = malloc(sizeof(struct AdjacencyGraph));
    graph->vertexCount = graph->edgeCount = 0;
    return graph;
}
// 加入顶点
void addVertex(Graph graph, E element)
{
    graph->vertex[graph->vertexCount].element = element;
    graph->vertex[graph->vertexCount].next = NULL;
    graph->vertexCount++;
}
// 加入边
void addEdge(Graph graph, int a, int b)
{
    // 拿到头结点的next
    Node node = graph->vertex[a].next;
    Node newNode = malloc(sizeof(struct Node));
    newNode->next = NULL;
    newNode->nextVertex = b; // 新创建的节点指向b
    if(!node) // 如果头结点下一个没有,则直接连上去
    {
        graph->vertex[a].next = newNode;
    }
    else // 否则说明当前顶点已经连接至少一个其他顶点,
    {
        do
        {
            if(node->nextVertex == b)
            {
                return free(newNode); // 已经连接对应的顶点
            }
            if(node->next)
            {
                node = node->next; // 向后遍历
            }
            else
            {
                break; // 没有下个,最后一个节点,结束
            }
        } while (1);
        node->next = newNode;
    }
    graph->edgeCount++;
}
// 打印邻接表
void printGraph(Graph graph)
{
    for(int i = 0 ; i < graph->vertexCount; ++i)
    {
        printf("%d | %c",i,graph->vertex[i].element);
        Node node = graph->vertex[i].next;
        while (node)
        {
            printf(" -> %d", node->nextVertex);
            node = node->next;
        }
        putchar('\n');
    }
}

但存在的问题是:只能快速得到某个顶点指向了哪些顶点,只能计算到顶点的出度,但无法快速计算顶点的入度。因此建立一个逆邻接表,以表示所有指向当前顶点的顶点列表
在这里插入图片描述

图练习题

  1. 在n个顶点中有向图,若所有顶点的出度之和为s,则所有顶点的入度之和为s。
  2. 一个具有n个顶点的无向完全图中,所含的边数为
    任意两个顶点都有一条边相连,那每个顶点都会有n-1条与其连接的边,总数为n*(n-1),但在无向图中,边数为 n × ( n − 1 ) 2 \frac{n\times \left( n-1 \right)}{2} 2n×(n1)
  3. 把n个顶点连接为一个连通图,则至少需要几条边?n-1
    只需要找一个每个节点都与其相连的,连成一根直线(树)
  4. 对一个具有n个顶点和e条边的无向图,在其对应的邻接表中,所含边结点有多少
    对无向图。结点个数等于边数的两倍,对有向图,结点数等于边数

图的遍历

类似迷宫图。图的搜索从图的某一顶点出发,寻找图中对应顶点位置。

深度优先搜索(DFS)

类似前序遍历
在这里插入图片描述
一路向前,走到死胡同,倒回去走其他方向,不断重复寻找,直到目标
在这里插入图片描述
使用邻接表表示图,邻接表直接保存相邻顶点,在遍历相邻顶点会更快(O(V+E))阶,而使用邻接矩阵,需要遍历完整二维数组(O(V^2))阶

/*深度优先搜索算法
 *@param graph 图
 *@param startVertex 起点顶点下标
 *@param targetVertex 目标顶点下标
 *@param visited 已经达到过顶点数组
*/ 
void dfs(Graph graph, int startVertex, int targetVertex, int * visited)
{
    printf("%c -> ", graph->vertex[startVertex].element); // 打印当前顶点值
    visited[startVertex] = 1; // 标记顶点
    Node node = graph->vertex[startVertex].next; // 先取第一个节点开始,遍历当前顶点所有的分支
    while (node)
    {
        if(!visited[node->nextVertex]) // 排除已经访问过的顶点
        {
            dfs(graph, node->nextVertex, targetVertex,visited); //搜索其他顶点
        }
        node = node->next; //取下一个节点
    } 
}

测试

    Graph graph = create();
    for(int c = 'A'; c <= 'F';++c)
    {
        addVertex(graph,(char) c);
    }
    addEdge(graph,0,1); //A->B
    addEdge(graph,1,2); //B->C
    addEdge(graph,1,3); //B->D
    addEdge(graph,1,4); //D->E
    addEdge(graph,4,5); //E->F
    
    int arr[graph->vertexCount];
    for(int i = 0 ; i < graph->vertexCount; ++i)
    {
        arr[i] = 0; // 初始化
    }
    dfs(graph, 0 , 5, arr);

搜索结果返回

_Bool dfs(Graph graph, int startVertex, int targetVertex, int * visited)
{
    printf("%c -> ", graph->vertex[startVertex].element); // 打印当前顶点值
    visited[startVertex] = 1; // 标记顶点
    Node node = graph->vertex[startVertex].next; // 先取第一个节点开始,遍历当前顶点所有的分支
    if(startVertex == targetVertex)
    {
        return 1; // 当前顶点为寻找节点,直接返回
    }
    while (node)
    {
        if(!visited[node->nextVertex]) // 排除已经访问过的顶点
        {
            if(dfs(graph, node->nextVertex, targetVertex,visited))
            {
                return 1;
            } //搜索其他顶点
        }
        node = node->next; //取下一个节点
    }
    return 0; 
}

测试,找到顶点,返回1

    Graph graph = create();
    for(int c = 'A'; c <= 'F';++c)
    {
        addVertex(graph,(char) c);
    }
    addEdge(graph,0,1); //A->B
    addEdge(graph,1,2); //B->C
    addEdge(graph,1,3); //B->D
    addEdge(graph,1,4); //D->E
    addEdge(graph,4,5); //E->F
    
    int arr[graph->vertexCount];
    for(int i = 0 ; i < graph->vertexCount; ++i)
    {
        arr[i] = 0; // 初始化
    }
    printf("\n%d ",dfs(graph, 0 , 5, arr));

广度优先搜索(BFS)

类似层序遍历,优先将每一层进行遍历。在图的搜索中,先探索顶点的所有分支,依次看分支的所有分支
首先A到B,B有三条路,依次访问三个顶点
在这里插入图片描述
从第一个顶点H开始,同样方式,只有一个分支,找到C,继续记录,把C添加到队列:有G、K、C
回去看第二个顶点G,由于C已经看过,找到F和D,记录下K、C、F、D
K已经是死胡同,接着看C,将E记录进去:F、D、E,接着看D、F
最后剩E,看I和J顶点
广度优先遍历,尽可能扩展范围。使用队列

typedef int T; // 顶点下标作为元素

struct QueueNode
{
    T element;
    struct QueueNode * next;
};

typedef struct QueueNode * QNode;

struct Queue
{
    QNode front, rear;
};

typedef struct Queue * LinkedQueue;

_Bool initQueue(LinkedQueue queue)
{
    QNode node = malloc(sizeof(struct QueueNode));
    if(node == NULL)
    {
        return 0;
    }
    else
    {
        queue->front = queue->rear = node;
    }
    return 1;
}
_Bool offerQueue(LinkedQueue queue, T element)
{
    QNode node = malloc(sizeof(struct QueueNode));
    if(node == NULL)
    {
        return 0;
    }
    else
    {
        node->element = element;
        queue->rear->next = node;
        queue->rear = node;
        return 1;
    }
}
_Bool isEmpty(LinkedQueue queue)
{
    return queue->front == queue->rear;
}
T pollQueue(LinkedQueue queue)
{
    T e = queue->front->next->element;
    QNode node = queue->front->next;
    queue->front->next = queue->front->next->next;
    if(queue->rear == node)
    {
        queue->rear = queue->front;
    }
    free(node);
    return e;
}

广度优先搜索
在这里插入图片描述
遍历

/*
 * @param graph 
 * @param startVertex 起点顶点下标
 * @param targetVertex 目标顶点下标
 * @param visited 已经达到过顶点数组
 * @param queue 辅助队列
*/
void bfs(Graph graph, int startVertex, int targetVertex, int * visited, LinkedQueue queue)
{
    offerQueue(queue, startVertex); // 将起点放进队列
    visited[startVertex] = 1; // 标记起始位置已走过
    while (!isEmpty(queue))
    {
        int next = pollQueue(queue); // 队列中取出节点
        printf("%c - >", graph->vertex[next].element); // 从队列中取出一个顶点进行打印
        Node node = graph->vertex[next].next;
        while (node)
        {
            if(!visited[node->nextVertex]) // 若没有走过,直接入队
            {
                offerQueue(queue, node->nextVertex);
                visited[node->nextVertex] = 1; // 标记为1
            }
            node = node->next;
        }
        
    }
    
}

测试

    Graph graph = create();
    for(int c = 'A'; c <= 'F';++c)
    {
        addVertex(graph,(char) c);
    }
    addEdge(graph,0,1); //A->B
    addEdge(graph,1,2); //B->C
    addEdge(graph,1,3); //B->D
    addEdge(graph,1,4); //D->E
    addEdge(graph,4,5); //E->F
    // addEdge(graph,3,6); 
    
    int arr[graph->vertexCount];
    for(int i = 0 ; i < graph->vertexCount; ++i)
    {
        arr[i] = 0; // 初始化
    }
    struct Queue queue;
    initQueue(&queue);
    bfs(graph, 0 ,5,arr, &queue);

查找目标元素,找到即返回

_Bool bfs(Graph graph, int startVertex, int targetVertex, int * visited, LinkedQueue queue)
{
    offerQueue(queue, startVertex); // 将起点放进队列
    visited[startVertex] = 1; // 标记起始位置已走过
    while (!isEmpty(queue))
    {
        int next = pollQueue(queue); // 队列中取出节点
        printf("%c - >", graph->vertex[next].element); // 从队列中取出一个顶点进行打印
        Node node = graph->vertex[next].next;
        while (node)
        {
            if(node->nextVertex == targetVertex)
            {
                return 1;
            }
            if(!visited[node->nextVertex]) // 若没有走过,直接入队
            {
                offerQueue(queue, node->nextVertex);
                visited[node->nextVertex] = 1; // 标记为1
            }
            node = node->next;
        }
        
    }
    return 0;
}

测试

printf("\n%d ",bfs(graph, 0 ,3,arr, &queue));

图练习提

  1. 若一个图的边集为:{(A,B),(A,C),(B,D),(C,F),(D,E),(D,F)},对图进行深度优先搜索,得到的顶点序列可能是
    圆括号是一个无向图,ACFDEB
    在这里插入图片描述
  2. 若以一个图边集为{(A,B),(A,C),(B,D),(C,F),(D,E),(D,F)},对图进行广度优先搜索,顶点序列为
    按照规则ACBFDE,先C则下一层先F
  3. 对无向连通图,从顶点A开始对图进行广度优先遍历,顶点序列可能为
    在这里插入图片描述
    A,B,F,C,D,G,E

图应用

  • 如果原图本身不连通,那么其连通分量(强连通分量)不止一个
  • 如果原图本身连通,那么其连通分量(强连通分量)就是其本身

生成树

极小连通子图,边数的极小。要求原图的子图是连通的,具有最大的顶点数和最小的边数,再去掉任意一条边导致图的不连通。理解为极大连通子图尽可能去掉能去掉的边
针对极小连通子图,一般讨论无向图。(对于有向图,不存在极小强连通子图)

原图本身就是连通图
在这里插入图片描述
右侧两图与左边图相比,包含相同的顶点数量,但边数被去掉,若再去掉任意一条边,会导致不连通。极小连通图不唯一
无论去掉哪些边情况,到最后一定只留下N-1条边(其中N是顶点数),每个顶点有且仅有一条路径相连,包含原图全部N个顶点的极小连通子图,称其为生成树。边数和顶点数满足定义,不存在回路情况。

如果原图本身不连通,则出现多个连通分量,得到一片生成森林,森林中的树的数量就是其连通分量的数量
在这里插入图片描述

通过深度优先搜索和广度优先搜索可以得到生成树,且生成树是不唯一的。
最小生成树:如果给一个无向图的边加上权值,要求生成树边的权值总和最小,称这棵树为最小生成树(也不唯一)

普利姆算法(Prim)

从任意一个顶点开始,往尽可能小的方向延伸
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
省去权重大的边或者导致回路的边

克鲁斯卡尔算法(Kruskal)

主动去选择那些小的边,而不是像Prim算法那样被动扩展延伸。任意一条边都可以选择,并不是只有顶点旁边才可选择。也可能出现多棵树,但最后一定连成一棵树,最后形成一棵最小生成树。
比如直接找到最小边
在这里插入图片描述
在这里插入图片描述

最短路径问题

如地铁考虑换乘和成本问题

单源最短路径

从一个顶点出发,到其他顶点的最短路径
迪杰斯特拉算法
在这里插入图片描述
从A出发
在这里插入图片描述
dist记录A到其他顶点的最短路径,path记录最短路径所邻接的顶点,
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

如果需要求每一对顶点之间的最短距离,需要将所有顶点执行迪杰斯特拉算法,很麻烦。故采用弗洛伊德算法

弗洛伊德算法

对有向图,根据邻接矩阵
在这里插入图片描述

  • 从1开始,一直到n(n是顶点数)的一个矩阵序列A1、A2、A3…从最初邻接矩阵开始往后推
  • 每一轮更新非对角线、i行i列以外的元素(类似于求伴随矩阵),判断水平和垂直方向投影的两个元素之和是否比原值小,是则更新为新的值。
  • 经历n轮后,得到最终最短的距离
    在这里插入图片描述
    在这里插入图片描述

得到所有顶点之间的最短距离,但没有记录哪个方向到达此顶点的。
编写程序弗洛伊德算法
在这里插入图片描述
定义无穷大小和邻接矩阵维数

#define INF 10000000
#define N 4

定义取最小函数

int min(int a, int b)
{
    return a > b ? b : a;
}

弗洛伊德算法

void floyd(int matrix[N][N], int n)
{
    for(int k = 0 ; k < n; ++k) // 执行K轮
    {
        for(int i = 0 ; i < n; ++i) // 行
        {
            for(int j = 0 ; j < n; ++j) // 列
            {
                matrix[i][j] = min(matrix[i][k] + matrix[k][j] , matrix[i][j]);
            }
        }
    }
}

测试输出

int main()
{
    int matrix[N][N] = {{0,1,INF,INF},
                        {4,0,INF,5},
                        {INF,2,0,INF},
                        {3,INF,7,0}};  
    floyd(matrix,N);
    for(int i = 0 ; i < N; ++i)
    {
        for(int j = 0 ; j < N; ++j)
        {
            printf("%d ", matrix[i][j]);
        }
        putchar('\n');
    }
}

拓扑排序

在这里插入图片描述
有向无环图,或流程图

拓扑排序将一个有向无环图进行排序得到有序的线性序列。只有完成前置任务在后续任务之前完成,该排序不唯一
利用队列完成,将入度为0的顶点,丢进队列中(丢进去之后更新一下图中其他顶点的入度)
在这里插入图片描述
所有入度数为0的顶点进入队列后,开始出队。出队时直接打印,开始排序。查看当顶点离开图后,会不会右其他顶点的入读变为0,如果有,将其他顶点入队。比如A从图中移除,B变成了入度为0的顶点,即将B丢进队列

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

检查一个有向图是否为有向无环图。如果顶点还没遍历完就队空,说明一定出现问题。

关键路径

在以上的有向无环图为每个任务添加一个权重,表示任务需要花费的时间,则后续任务就需要前置任务按时间完成后才能继续
在这里插入图片描述
计算事件最早完成事件(完成这个事件最快要多久)和事件最晚开始时间(事件不影响工期的情况下最晚可以多久开始),按拓扑排序的顺序进行。从起点A直接开始,最早和最晚时间都是0,按AOE图的顺序,计算任务B和C的最早和最晚时间。
在这里插入图片描述
在这里插入图片描述
最早完成时间为8天,活动最晚开始时间,从终点倒着往回看。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
关键路径即为最早和最晚时间都一样的顶点,连成的路线。A->C->D->F。关键路径上的所有活动为关键活动,加快关键活动来缩短整个项目工期。

算法实战

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值