圖的建立、廣度優先遍歷和深度優先遍歷


圖的建立、廣度優先遍歷和深度優先遍歷

         圖分為有向圖和無向圖,再根據是否有權重又可以分為有權重圖和無權重圖。圖常用的表示方式有鄰接矩陣和鄰接表。這里我們處理的圖是有向、無權重圖,采用的表示方式是鄰接表。

         圖的數據保存在文件中,比如:

                                                        a 1 b

                                                        b 2 c e

                                                        c 1 f

                                                        d 2 c f

                                                        e 1 a

                                                        f 0

         其中,第一個元素表示圖中節點的名字,第二元素表示其可以直接到達的節點個數,后面緊跟着直接可以達到的節點。

         我們采用的表示方式是鄰接表,鄰接表首先針對圖中的節點定義一個數組,用來記錄每個節點,數組中的每個節點元素后面跟着一個鏈表,在該鏈表中記錄着其可以直接到達的節點。

         節點的定義有以下幾個部分:節點的名字,指向下一個節點的指針,是否被訪問的標示符。

         節點名字保存原始的字符串,這樣在表示節點時,直接用字符串表示即可。也可以建立字符串到數字的映射以及數字到字符串的映射,即字符串和數字之間的雙向映射,這里我們沒有利用數字指代字符串來表示節點,而是直接使用的字符串。

         表示圖中節點是否被訪問的標示符,我們將其放在節點中,也可以另外建立一個節點是否被訪問的數組,如果我們放在節點中,那么鏈表中的節點也含有該標示符,但是我們只關注鄰接表數組中的標示符,鏈表中的標示符不考慮,不過這樣造成了鏈表中的標示符閑置,浪費了空間,這樣做僅僅是為了描述方便。

         圖的遍歷需要對圖中節點記錄是否已經被訪問了,因為圖中有可能存在環,即便不會倒回去,也有可能造成循環訪問,如果添加了訪問標識符,可以避免循環訪問的情況。樹的遍歷則不需要添加訪問標識符,因為書中不存在環,不會導致循環訪問,而且不管圖的遍歷還是樹的遍歷,都不存在倒回去的情形。

         上述文件中描述的有向圖為:

         我們想根據給定的節點,輸出其可以達到的其他節點。這是一個圖遍歷問題,可以采用廣度優先遍歷也可以采用深度優先遍歷。

         圖的廣度優先遍歷類似於樹的廣度優先遍歷,也是利用隊列進行遍歷,不同點在於圖是用鄰接矩陣或鄰接表等表示,樹是采用其特有的樹結構來表示。不過樹也可以用圖的方式來表示,因為樹本身就可以看作為圖,圖也可以用樹來表示,圖和樹之間的差別就在於圖比樹多了一些邊,樹比圖少了一些邊。圖的遍歷和樹的遍歷差別在於圖為了防止循環訪問的情形,需要一個節點訪問標識符,而樹不需要。

         圖的深度優先遍歷同樣也類似於樹的深度優先遍歷。也是多了一個節點訪問標示符。

         廣度優先遍歷需要借助於隊列來實現,因為廣度優先遍歷的邏輯符合隊列先進先出的特點。而深度優先遍歷需要借助於棧來實現,因為深度優先遍歷的邏輯符合棧后進先出的特點。注意在深度優先遍歷的過程中有兩訪問方式,第一種是在按照入棧的順序訪問,第二種是按照出棧的順序訪問。而隊列的入隊列和出隊列順序都是一樣的。在實際實現的深度優先遍歷中並不需要顯式的棧,而是采用的函數遞歸調用,借助於函數遞歸調用中參數的隱式的棧。深度優先遍歷雖然沒有使用顯式的棧,但是由於遞歸調用,還是采用了符合棧的邏輯特點。

         下面我們將給出具體的程序實現,其中主要包含以下幾個部分:

         1.圖節點的定義和生成

         2.圖的表示方式——鄰接表

         3.隊列的定義和操作函數的實現

         4.一些模塊函數的封裝

         5.設置和查詢節點的訪問標識符

         6.根據節點名字查找節點在鄰接表數組中的索引

         7.讀取數據文件,並建立圖對應的鄰接表,並打印圖

         8.圖的廣度優先遍歷

         9.圖的深度優先遍歷

         10.相關已建立結構的釋放

         11.測試

         相關細節請查看代碼和注釋說明。

// 圖的建立、廣度遍歷和深度遍歷
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define M (100 + 1)

// 定義節點結構體
typedef struct node_t
{
    char* name;          // 節點名
    int   visited;       // 表示是否被訪問,0表示未被訪問,1表示被訪問
    struct node_t* next; // 指向下一個節點
} NODE;

// 實現一個隊列,用於后續操作
typedef struct queue_t
{
    NODE** array; // array是個數組,其內部元素為NODE*型指針
    int    head;  // 隊列的頭
    int    tail;  // 隊列的尾
    int    num;   // 隊列中元素的個數
    int    size;  // 隊列的大小
} QUEUE;

// 內存分配函數
void* util_malloc(int size)
{
    void* ptr = malloc(size);

    if (ptr == NULL) // 如果分配失敗,則終止程序
    {
        printf("Memory allocation error!\n");
        exit(EXIT_FAILURE);
    }

    // 分配成功,則返回
    return ptr;
}

// 字符串賦值函數
// 對strdup函數的封裝,strdup函數直接進行字符串賦值,不用對被賦值指針分配空間
// 比strcpy用起來方便,但其不是標准庫里面的函數
// 用strdup函數賦值的指針,在最后也是需要free掉的
char* util_strdup(char* src)
{
    char* dst = strdup(src);

    if (dst == NULL) // 如果賦值失敗,則終止程序
    {
        printf ("Memroy allocation error!\n");
        exit(EXIT_FAILURE);
    }

    // 賦值成功,返回
    return dst;
}

// 對fopen函數封裝
FILE* util_fopen(char* name, char* access)
{
    FILE* fp = fopen(name, access);

    if (fp == NULL) // 如果打開文件失敗,終止程序
    {
        printf("Error opening file %s!\n", name);
        exit(EXIT_FAILURE);
    }

    // 打開成功,返回
    return  fp;
}

// 實現隊列的操作
QUEUE* QUEUEinit(int size)
{
    QUEUE* qp;

    qp = (QUEUE*)util_malloc(sizeof (QUEUE));
    qp->size  = size;
    qp->head  = qp->tail = qp->num = 0;
    qp->array = (NODE**)util_malloc(size * sizeof (NODE*));

    return qp;
}

// 入隊列
int QUEUEenqueue(QUEUE* qp, NODE* data)
{
    if (qp == NULL || qp->num >= qp->size) // qp未初始化或已滿
    {
        return 0; // 入隊失敗
    }

    qp->array[qp->tail] = data; // 入隊,tail一直指向最后一個元素的下一個位置
    qp->tail = (qp->tail + 1) % (qp->size); // 循環隊列
    ++qp->num;
    return 1;
}

// 出隊列
int QUEUEdequeue(QUEUE* qp, NODE** data_ptr)
{
    if (qp == NULL || qp->num <= 0) // qp未初始化或隊列內無元素
    {
        return 0;
    }

    *data_ptr = qp->array[qp->head]; // 出隊
    qp->head = (qp->head + 1) % (qp->size); // 循環隊列
    --qp->num;

    return 1;
}

// 檢測隊列是否為空
int QUEUEempty(QUEUE* qp)
{
    if (qp == NULL || qp->num <= 0)
    {
        return 1;
    }

    return 0;
}

// 銷毀隊列
void QUEUEdestroy(QUEUE* qp)
{
    free(qp->array);
    free(qp);
}
// 以上是隊列的有關操作實現

// 生成圖中的節點
NODE* create_node()
{
    NODE* q = NULL;

    q = (NODE*)util_malloc(sizeof (NODE));
    q->name    = NULL;
    q->visited = 0;
    q->next    = NULL;

    return q;
}

// 設置訪問標示visited
void set_visited(char name[M], NODE* graph, int n)
{
    int i = 0;
    for (i = 0; i < n; ++i)
    {
        if (strcmp(name, graph[i].name) == 0)
        {
            graph[i].visited = 1;
            return;
        }
    }
}

// 查找是否已經被訪問,返回0表示未被訪問,1表示被訪問
int is_visited(char name[M], NODE* graph, int n)
{
    int i = 0;
    for (i = 0; i < n; ++i)
    {
        if (strcmp(name, graph[i].name) == 0)
        {
            if (graph[i].visited == 1) // 被訪問
            {
                return 1;
            }
            else // 未被訪問
            {
                return 0;
            }
        }
    }
    return 0;
}

// 根據節點名,返回節點在鄰接表中的索引
int find_index(char name[M], NODE* graph, int n)
{
    int i = 0;

    for (i = 0; i < n; ++i)
    {
        if (strcmp(name, graph[i].name) == 0)
        {
            return i;
        }
    }
    return -1;
}

// 讀取文件,建立鄰接表
void read_file(NODE** graph, int* count, char* filename)
{
    char name[M], adj[M];
    int    n = 0, i = 0, j = 0;
    FILE* fp = NULL;
    NODE* p1 = NULL, *p2 = NULL;

    *graph = (NODE*)util_malloc(M * sizeof (NODE));
    *count = 0;

    fp = util_fopen(filename, "r"); // 打開文件

    while (fscanf(fp, "%s %d", name, &n) != EOF)
    {
        (*graph)[i].name    = util_strdup(name);
        (*graph)[i].visited = 0;
        (*graph)[i].next    = NULL;

        p1 = &((*graph)[i]);

        for (j = 0; j < n; ++j)
        {
            fscanf(fp, "%s", adj);
            p2 = create_node();
            p2->name = util_strdup(adj);

            //// 與文件中的節點順序相反
            //p2->next = p1->next;
            //p1->next = p2;

            //按照文件中的節點順序
            p1->next = p2;
            p1 = p2;
        }
        ++i;
    }
    *count = i; // 總共i個節點

    fclose(fp); // 讀取完畢
}

void print_graph(NODE* graph, int n)
{
    int i = 0;
    NODE* p = NULL;

    for (i = 0; i < n; ++i)
    {
        fprintf(stdout, "%s ", graph[i].name);

        p = graph[i].next;

        while (p != NULL) // not if (p != NULL)
        {
            fprintf(stdout, "%s ", p->name);
            p = p->next;
        }
        fprintf(stdout, "\n");
    }
}

// 根據給定的節點查找到其能到達的其他節點
// 廣度優先遍歷
void func(char name[M], NODE* graph, int n)
{
    NODE* p1 = NULL, *p2 = NULL;
    int index = 0, i = 0;
    QUEUE* q = NULL;

    // 將訪問標識都置為0
    for (i = 0; i < n; ++i)
    {
        graph[i].visited = 0;
    }

    q = QUEUEinit(100); // 初始化隊列

    index = find_index(name, graph, n);
    
    fprintf(stdout, "Reachable node:");

    if (graph[index].next == NULL)
    {
        fprintf(stdout, "-\n");
        return;
    }

    // 如果后面有節點
    p1 = &(graph[index]);

    // 將該節點入隊列
    QUEUEenqueue(q, p1);
    // 該節點算作已經被訪問了
    graph[index].visited = 1;

    while (QUEUEempty(q) == 0) // 如果隊列不為空
    {
        // 出隊列
        QUEUEdequeue(q, &p1);

        p1 = p1->next; // ※這一步保證每次都不訪問隊列中的節點

        while (p1 != NULL)
        {
            index = find_index(p1->name, graph, n); // 查找該節點的索引
            if (graph[index].visited == 1) // 如果已經被訪問過
            {
                // 不做處理
                ;
            }
            else // 如果還沒有被訪問
            {
                // 輸出該節點
                fprintf(stdout, "%s ", p1->name);
                // 將該節點設置為被訪問過
                graph[index].visited = 1;
                // 將該節點入隊列
                QUEUEenqueue(q, &graph[index]);
            }
            p1 = p1->next;
        }
    }
    fprintf(stdout, "\n");

    // 消毀隊列
    QUEUEdestroy(q);
}

// 深度優先遍歷
void function2(char name[M], NODE* graph, int n, int* flag)
{
    int index = 0;
    NODE* p1  = NULL, *p2 = NULL;

    index = find_index(name, graph, n);
    graph[index].visited = 1; // 一開始就被設置被訪問,所以后面的設置visited可以忽略

    p1 = graph[index].next;   // 這一步很關鍵,不考慮鄰接表數組中的元素,而是直接考慮數組中的元素后面鏈表中的元素

    if (p1 == NULL)
    {
        return;
    }

    index = find_index(p1->name, graph, n);

    if (graph[index].visited == 1)
    {
        return;
    }

    while (p1 != NULL && graph[index].visited != 1)
    {
        *flag = 1; // 設置存有后續節點標識

        fprintf(stdout, "%s ", p1->name);
        // 設置訪問標示
        index = find_index(p1->name, graph, n);
        // graph[index].visited = 1; // 這里可以被忽略,因為在函數開始出被設置了

        p2 = &graph[index]; // 下一個深度的節點
        function2(p2->name, graph, n, flag);

        p1 = p1->next;
        if (p1 != NULL)
        {
            index = find_index(p1->name, graph, n); // p1變了,index也要變
        }
        else
        {
            break;
        }
    }
}

// 對深度優先遍歷function2封裝
void func2(char name[M], NODE* graph, int n)
{
    int i = 0, flag = 0;

    for (i = 0; i < n; ++i) // 重置訪問標示
    {
        graph[i].visited = 0;
    }

    fprintf(stdout, "Reachable node-2:");

    function2(name, graph, n, &flag);

    if (flag == 0)
    {
        fprintf(stdout, "-");
    }
    fprintf(stdout, "\n");
}

// 消毀圖
void free_graph(NODE* graph, int n)
{
    int i = 0;
    NODE* p1 = NULL, *p2 = NULL;

    if (graph == NULL)
    {
        return;
    }

    for (i = 0; i < n; ++i)
    {
        p1 = graph[i].next;
        free(graph[i].name);
        while (p1 != NULL)
        {
            p2 = p1->next;

            free(p1->name);
            free(p1);

            p1 = p2;
        }
    }

    free(graph);
}

int main(int argc, char* argv[])
{
    NODE* graph = NULL;
    char  name[M];
    int   count = 0;

    if (argc != 2)
    {
        fprintf(stderr, "Missing parameters!\n");
        exit(EXIT_FAILURE);
    }

    read_file(&graph, &count, "data.txt");

    // 打印鄰接表
    // print_graph(graph, count);

    fprintf(stdout, ">Vertex:");
    fscanf(stdin, "%s", name);


    while (strcmp(name, "end") != 0)
    {
        func(name, graph, count);

        func2(name, graph, count);

        fprintf(stdout, ">Vertex:");
        fscanf(stdin, "%s", name);
    }

    free_graph(graph, count);

    return EXIT_SUCCESS;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM