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

作者: SegmentFault博客  更新时间:2019-08-14 17:05:49  原文链接


数据结构

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

  • 邻接表

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

上图是一个含有四个顶点的无向图,四个顶点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;
  • 邻接矩阵

邻接矩阵适用于稠密图(边的数量比较多),它的抽象描述如下:

上图是个无向无权图,仅用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. 从图中任意选择一个顶点,作为生成树的起始结点;
    2. 从生成树集合(所有已经加入生成树的顶点所组成的集合)外的结点中,选择一个距离 生成树集合 代价最小的点,将其加入到生成树集合中;
    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]);
        }
}