圖的基本算法及其C語言的實現

數據結構

「圖」的數據結構有兩種:node

  • 鄰接表

鄰接表適用於稀疏圖(邊的數量遠遠小於頂點的數量),它的抽象描述以下:
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,表示不相連。數據結構

鄰接矩陣的結構體定義以下:less

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的遞歸遍歷有些相似於二叉樹的前序遍歷。oop

  • 非遞歸

借用了額外的數據結構——棧。this

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是「一圈一圈往外找」的算法,藉助了「循環隊列」來實現:spa

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;
        }
    }
}

提示:3d

  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]);
        }
}
相關文章
相關標籤/搜索