图的基本算法及其C语言的实现

数据结构

“图”的数据结构有两种:

  • 邻接表
    邻接表适用于稀疏图(边的数量远远小于顶点的数量),它的抽象描述如下:
    adjacency list

    上图是一个含有四个顶点的无向图,四个顶点V0,V1,V2及V3用一个数组来存取,借用后面的结构体定义来描述,数组元素的类型为VertexNode,一个字段info用来保存顶点的信息,另一个字段firstEdge指向与该顶点有关的边结点,类型为EdgeNode,边结点的toAdjVex字段表示这条边的另一个顶点结点的数组下标,next字段仅仅用来指向另一个边结点,所有通过next链接起来的边结点都有相同的起始顶点.
    注意:当用一个邻接表描述图时,就已经确定了该图的遍历顺序,以上图的邻接表为例,从 V0顶点开始遍历时,无论是深度优先搜索 (DFS)还是广度优先搜索 (BFS),都会先找到 V1结点,而从左边的图来看,其实V1,V2,V3都可以为下一个遍历的结点。

邻接表结构体定义如下:

typedef struct EdgeNode
{
    int toAdjVex;                               // The index of vertex array which this edge points to.
    float weight;                               // The edge weight.
    struct EdgeNode *next;                      // The next edge, note that it only means the next edge also links to the vertex which this edge links to.
} EdgeNode;

typedef struct VertexNode
{
    VERTEX_DATA_TYPE info;                      // The vertex info,.
    struct EdgeNode* firstEdge;                 // The first edge which the vertex points to.
} VertexNode;

typedef struct
{
    VertexNode adjList[VERTEX_NUM];             // Adjacency list, which stores the all vertexes of the graph.
    int vertextNum;                             // The number of vertex.
    int edgeNum;                                // The number of edge.
} AdjListGraph;
  • 邻接矩阵
    邻接矩阵适用于稠密图(边的数量比较多),它的抽象描述如下:


    adjacency matrix

    上图是个无向无权图,仅用0、1来表示两个顶点是否相连,当然,通常做法是将边的权值来代替1,用一个十分大的数字(如∞)来代替0,表示不相连。

邻接矩阵的结构体定义如下:

typedef struct
{
    int number;
    VERTEX_DATA_TYPE info;
} Vertex;

typedef struct
{
    float edges[VERTEX_NUM][VERTEX_NUM];        // The value of this two dimensional array is the weight of the edge.
    int vertextNum;                             // The number of vertex.
    int edgeNum;                                // The number of edge.
    Vertex vex[VERTEX_NUM];                     // To store vertex.
} MGraph;

算法

深度优先搜索 (Depth First Search)

一句话描述就是“一条路走到黑”,它的递归与非递归的代码如下:

  • 递归
void dfsRecursion(AdjListGraph* graph, int startVertexIndex, bool visit[])
{
    printf("%c ", (graph -> adjList[startVertexIndex]).info);
    visit[startVertexIndex] = true;
    EdgeNode* edgeIndex = (graph -> adjList[startVertexIndex]).firstEdge;
    while (edgeIndex != NULL)
    {
        if (visit[edgeIndex -> toAdjVex] == false)
            dfsRecursion(graph, edgeIndex -> toAdjVex, visit);
        edgeIndex = edgeIndex -> next;
    }
}

提示:DFS的递归遍历有些类似于二叉树的前序遍历。

  • 非递归

借用了额外的数据结构——栈。

void dfsNonRecursion(AdjListGraph* graph, int startVertextIndex, bool visit[])
{
    linked_stack* stack = NULL;
    init_stack(&stack);

    // Visit the start vertex.
    printf("%c ", (graph -> adjList[startVertextIndex]).info);
    visit[startVertextIndex] = true;
    EdgeNode* edgeNode = (graph -> adjList[startVertextIndex]).firstEdge;
    if (edgeNode != NULL)
        push(stack, edgeNode);
    while (!isEmptyStack(stack))
    {
        edgeNode = ((EdgeNode*)pop(stack)) -> next;
        while (edgeNode != NULL && !visit[edgeNode -> toAdjVex])
        {
            printf("%c ", (graph -> adjList[edgeNode -> toAdjVex]).info);
            visit[edgeNode -> toAdjVex] = true;
            push(stack, edgeNode);
            edgeNode = (graph -> adjList[edgeNode -> toAdjVex]).firstEdge;
        }
    }
}

广度优先搜索 (Breadth First Search)

BFS是“一圈一圈往外找”的算法,借助了“循环队列”来实现:

void bfs(AdjListGraph* graph, int startVertexIndex, bool visit[])
{
    // Loop queue initialization.
    LoopQueue loopQ;
    loopQ.front = 0;
    loopQ.rear = 0;
    LoopQueue* loopQueue = &loopQ;
    enqueue(loopQueue, &(graph -> adjList[startVertexIndex]));
    printf("%c ", (graph -> adjList[startVertexIndex]).info);
    visit[startVertexIndex] = true;
    while (!isEmpty(loopQueue))
    {
        VertexNode* vertexNode = dequeue(loopQueue);
        EdgeNode* edgeNode = vertexNode -> firstEdge;
        while(edgeNode != NULL)
        {
            if (visit[edgeNode -> toAdjVex] == false)
            {
                printf("%c ", (graph -> adjList[edgeNode -> toAdjVex]).info);
                visit[edgeNode -> toAdjVex] = true;
                enqueue(loopQueue, &(graph -> adjList[edgeNode -> toAdjVex]));
            }
            edgeNode = edgeNode -> next;
        }
    }
}

提示:

  1. BFS算法类似于二叉树的层次遍历。
  2. BFS遍历的最后一个结点是离起始结点“最远”的结点。

最小生成树 (Minimum Spanning Tree)

Prim

  • 算法

    1. 从图中任意选择一个顶点startVertex,作为生成树的起始结点;
    2. 从生成树集合(所有已经加入生成树的顶点所组成的集合)外的结点中,选择一个距离生成树集合代价最小的点,将其加入到生成树集合中(更新treeSet[]),此时由于新结点的加入,可能使新的生成树到其他结点的距离发生变化,因此要更新lowCost[]
    3. 不断重复步骤2,直到所有顶点加入到生成树集合中。
  • 代码

float prim(MGraph* graph, int startVertex)
{
    float totalCost = 0;
    float lowCost[VERTEX_NUM];              // The value of lowCost[i] represents the minimum distance from vertex i to current spanning tree.
    bool treeSet[VERTEX_NUM];               // The value of treeSet[i] represents whether the vertex i has been merged into the spanning tree.

    // Initialization
    for (int i = 0; i < (graph -> vertextNum); i++)
    {
        lowCost[i] = graph -> edges[startVertex][i];        // Init all cost from i to startVertex.
        treeSet[i] = false;                                 // No vertex is in the spanning tree set at first.
    }

    treeSet[startVertex] = true;                            // Merge the startVertex into the spanning tree set.
    printf("%c ", (graph -> vex[startVertex]).info);
    for (int i = 0; i < (graph -> vertextNum); i++)
    {
        int minCost = MAX_COST;                             // MAX_COST is a value greater than any other edge weight.
        int newVertex = startVertex;

        // Find the minimum cost vertex which is out of the spanning tree set.
        for (int j = 0; j < (graph -> vertextNum); j++)
        {
            if (!treeSet[j] && lowCost[j] < minCost)
            {
                minCost = lowCost[j];
                newVertex = j;
            }
        }
        treeSet[newVertex] = true;                          // Merge the new vertex into the spanning tree set.

        /*

            Some ops, for example you can print the vertex so you will get the sequence of node of minimum spanning tree.

        */
        if (newVertex != startVertex)
        {
            printf("%c ", (graph -> vex[newVertex]).info);
            totalCost += lowCost[newVertex];
        }


        // Judge whether the cost is change between the new spanning tree and the remaining vertex.
        for (int j = 0; j < (graph -> vertextNum); j++)
        {
            if (!treeSet[j] && lowCost[j] > graph -> edges[newVertex][j])
                lowCost[j] = graph -> edges[newVertex][j];  // Update the cost between the spanning tree and the vertex j.
        }
    }
    return totalCost;
}

并查集 (Union Find Set)

  • 介绍
    并查集可以很容易判断几个不同的元素是否属于同一个集合,正是因为这个特性,并查集是克鲁斯卡尔算法( Kruskal's algorithm)的重要工具。
    在这个例子中,我们用数组来作为并查集工具的“集”,数组的下标代表集合里元素的序列号,而该下标的值表示这个结点的父结点在该数组的下标。这个数组是一个大集合,大集合里还划分了许多小集合,划分的依据是这些元素是否属于同一个根结点,这个根节点的值为负数,其绝对值的大小为该集合里元素的个数,因此通过不断寻找两个元素的父结点,直至找到根结点,进而判断根结点是否相等来判定这两个元素是否属于同一个集合(在克鲁斯卡尔算法中,也就是通过此来判断新加入的结点是否会形成环)。

  • 代码

int findRootInSet(int array[], int x)
{
    if (array[x] < 0)
    {
        // Find the root index.
        return x;
    }
    else
    {
        // Recursively find its parent until find the root,
        // then recursively update the children node so that they will point to the root.
        return array[x] = findRootInSet(array, array[x]);
    }
}

// For merging the one node into the other set.
bool unionSet(int array[], int node1, int node2)
{
    int root1 = findRootInSet(array, node1);
    int root2 = findRootInSet(array, node2);
    if (root1 == root2)
    {
        // It means they are in the same set
        return false;
    }

    // The value of array[root] is negative and the absolute value is its children numbers,
    // when merging two sets, we choose to merge the more children set into the less one.
    if (array[root1] > array[root2])
    {
        array[root1] += array[root2];
        array[root2] = root1;
    }
    else
    {
        array[root2] += array[root1];
        array[root1] = root2;
    }
    return true;
}

注意上面的 unionSet方法是用来合并集合的,多作为克鲁斯卡尔算法的辅助工具,该方法执行成功说明选的两个结点及其所在的集合可以构成一个无环连通分量,每次成功执行该方法,都要更新各个集合的状态(因为有新的结点加入),以便后续的判断。

最短路径算法

单源最短路径算法——迪杰斯特拉算法(Dijkstra Algorithm)

迪杰斯特拉算法是用来计算图中所有结点到某个特定结点的最短距离的算法,因为执行一次迪杰斯特拉算法,只是计算出所给特定的“源结点”到其他结点的最短距离,因此该算法也属于“单源最短路径算法”。

  • 步骤

    1. 将图的各个结点分为两个集合,一个是已经访问的集合(visited set),另一个是未访问的集合(unvisited set)。算法初始阶段,将需要计算的给定“源结点”加入到visited set
    2. unvisited set里选择一个距离源结点最近的结点,并将其加入到visited set
    3. 将新加入的结点当做“跳板”,重新计算unvisited set里的结点与源结点间的最短距离(由于新加入的结点可能会缩短源结点到unvisited set里的结点的距离,因此要特别注意)。
    4. 重复步骤2直到所有结点都已加入到visited set
  • 代码

void dijkstra(MGraph* graph, int startVertexIndex)
{
    // For storing the minimum cost from the arbitrary node to the start vertex.
    float minCostToStart[VERTEX_NUM];

    // For marking whether the node is in the set.
    bool set[VERTEX_NUM];

    // Initialization
    for (int i = 0; i < VERTEX_NUM; i++)
    {
        minCostToStart[i] = graph -> edges[i][startVertexIndex];
        set[i] = false;
    }

    // Add the start vertex into the set.
    set[startVertexIndex] = true;
    int minNodeIndex = startVertexIndex;

    for (int count = 1; count < VERTEX_NUM; count++)
    {
        int minCost = MAX_COST;

        // Find the adjacent node which is nearest to the startVertexIndex.
        for (int i = 0; i < VERTEX_NUM; i++)
        {
            if (!set[i] && minCostToStart[i] < minCost)
            {
                minCost = minCostToStart[minNodeIndex];
                minNodeIndex = i;
            }
        }

        // Add the proper node into the set
        set[minNodeIndex] = true;

        // After the new node is added into the set, update the minimum cost of each node which is out of the set.
        for (int i = 0; i < VERTEX_NUM; i++)
        {
            if (!set[i] && (graph -> edges[i][minNodeIndex]) < MAX_COST)
            {
                // The new cost of each node to source = the cost of new added node to source + the cost of node i to new added node.
                float newCost = minCostToStart[minNodeIndex] + graph -> edges[i][minNodeIndex];

                if (newCost < minCostToStart[i])
                    minCostToStart[i] = newCost;
            }
        }
    }

    printf("The cost of %c to each node:\n", (graph -> vex[startVertexIndex]).info);
    for (int i = 0; i < VERTEX_NUM; i++)
    {
        if (i != startVertexIndex)
            printf("-----> %c : %f\n", (graph -> vex[i]).info, minCostToStart[i]);
    }
}

提示:迪杰斯特拉算法与普利姆算法十分类似,特别是步骤 2。迪杰斯特拉算法总是从 unvisited set里选择距离 源结点最近的结点加入到 visited set里,而普利姆算法总是从生成树集合外,选择一个距离 生成树这个集合最近的结点加入到生成树中。

多源最短路径算法——弗洛伊德算法(Floyd Algorithm)

与迪杰斯特拉算法不同的是,调用一次弗洛伊德算法可以计算任意两个结点间的最短距离,当然,你也可以通过多次调用迪杰斯特拉算法来计算多源最短路径。

  • 步骤

    1. 初始化minCost[i][j]minCost[i][j]表示结点i到j的距离。
    2. 引入第三结点(跳板)k,查看通过结点k能否使minCost[i][j]得值变小,如果能,则更新minCost[i][j]的值。
    3. 重复步骤2直到所有结点都曾经被当做k为止。
  • 代码

void floyd(MGraph* graph)
{
    float minCost[VERTEX_NUM][VERTEX_NUM];          // Store the distance between any two nodes.
    int path[VERTEX_NUM][VERTEX_NUM];               // Store the intermediate node between the two nodes.
    int i, j, k;

    // Initialization
    for (i = 0; i < VERTEX_NUM; i++)
    {
        for (j = 0; j < VERTEX_NUM; j++)
        {
            minCost[i][j] = graph -> edges[i][j];
            path[i][j] = -1;
        }
    }

    // Find if there is another k node, it makes the distance dis[i][k] + dis[k][j] < dis[i][j];
    for (k = 0; k < VERTEX_NUM; k++)
        for (i = 0; i < VERTEX_NUM; i++)
            for (j = 0; j < VERTEX_NUM; j++)
            {
                if (minCost[i][j] > minCost[i][k] + minCost[k][j])
                {
                    minCost[i][j] = minCost[i][k] + minCost[k][j];
                    path[i][j] = k;
                }
            }

    for (i = 0; i < VERTEX_NUM; i++)
        for (j = 0; j < VERTEX_NUM; j++)
        {
            if (i != j && minCost[i][j] != MAX_COST)
                printf("%c ---> %c, the minimum cost is %f\n", (graph -> vex[i]).info, (graph -> vex[j]).info, minCost[i][j]);
        }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容

  • 1)这本书为什么值得看: Python语言描述,如果学的Python用这本书学数据结构更合适 2016年出版,内容...
    孙怀阔阅读 12,407评论 0 15
  • 1. 图的定义和基本术语 线性结构中,元素仅有线性关系,每个元素只有一个直接前驱和直接后继;树形结构中,数据元素(...
    yinxmm阅读 5,404评论 0 3
  • 图的基本概念 图由结点的有穷集合V和边的集合E组成。图中常常将结点成为顶点,边是顶点的有序偶对。若两个顶点之间存在...
    桔子满地阅读 1,370评论 0 0
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,636评论 0 13
  • RNG NIUBI
    fhtest阅读 131评论 0 0