第6章 圖的學習總結(鄰接矩陣&鄰接表)


我覺得圖這一章的學習內容更有難度,其實圖可以說是樹結構更為普通的表現形式,它的每個元素都可以與多個元素之間相關聯,所以結構比樹更復雜,然而越復雜的數據結構在現實中用途就越大了,功能與用途密切聯系,所以,圖結構非常重要,學習起來也是有點難度的,在於圖的存儲結構和邏輯結構,以及它與其他輔助數據結構相結合(鏈表,隊列等),這需要很清晰的邏輯思維,才能把知識貫通。

這么重要的圖,它的特別重要應用(最小生成樹、最短路徑、拓撲排序、關鍵路徑),還有這些應用中一些著名算法,圖的這章內容的豐富,讓我大開眼界!

學習圖最基礎的內容,也是實現其他操作最基礎、最關鍵的部分,就是圖的存儲結構,圖的遍歷。這里我准備總結一下在做題目時候對鄰接矩陣、鄰接表,深度優先搜索遍歷、廣度優先搜索遍歷的理解,而對於應用的各種算法,還需要繼續學習,才有更深刻的理解。

PTA上題目:列出連通集

 

給定一個有N個頂點和E條邊的無向圖,請用DFS和BFS分別列出其所有的連通集。假設頂點從0到N1編號。進行搜索時,假設我們總是從編號最小的頂點出發,按編號遞增的順序訪問鄰接點。

 

輸入格式:

 

輸入第1行給出2個整數N(0<N10)和E,分別是圖的頂點數和邊數。隨后E行,每行給出一條邊的兩個端點。每行中的數字之間用1空格分隔。

 

輸出格式:

 

按照 “ { v1, v2, v3, ... ,vk } ”的格式,每行輸出一個連通集。先輸出DFS的結果,再輸出BFS的結果。

輸入樣例:                         輸出樣例:

8 6            { 0 1 4 2 7 }
0 7            { 3 5 }
0 1            { 6 }
2 0            { 0 1 2 7 4 }
4 1            { 3 5 }
2 4            { 6 }
3 5

跟據這道題題意,可明顯以看出用鄰接矩陣的存儲結構較容易,而且輸入中沒有頂點名,直接用數組下標就可以,所以存儲輸入數據還是比較簡單實現的,下面是鄰接矩陣存儲定義:

typedef int ArcType; //
type char VerTexType;//頂點名字,這道題不需要用到
typedef struct
{
    VerTexType vexs[100];//頂點表
        ArcType arcs[100][100];//鄰接矩陣 
    int vexnum,arcnum;//頂點數和邊數 
}AMGraph; 
View Code

 

接下來創建無向圖:

void create(AMGraph &G)//鄰接矩陣建立無向圖 
{
    int i,j,k;
    int x,y;
    cin>>G.vexnum>>G.arcnum;
    for(i=0;i<G.vexnum;i++)//初始化矩陣元素值為0 
    {
        for(j=0;j<G.arcnum;j++)
        G.arcs[i][j]=0;
    }
    for(k=0;k<G.arcnum;k++)//輸入一條邊的兩個端點 
    {
        cin>>x>>y;
        G.arcs[x][y]=1;//並將對應矩陣中元素值置1,表示此兩點間存在邊 
        G.arcs[y][x]=1;//無向圖的矩陣為對稱的 
    }
}
View Code

因為題目的特殊性,找頂點對應的下標的LocateVex函數就省去了,直接用輸入數據,所以簡單很多。繼續是圖的遍歷,先DFS算法,對於非連通圖,需要兩個函數來完成圖的遍歷,剛好這道題目也是輸入連通分量,DFSAM函數是對一個連通分量的遍歷,DFS是對整個圖,有幾個連通分量就調用幾次DFSAM:

void DFSAM(AMGraph G,int v)//一個連通分量的深度優先搜索遍歷 
{
    int w;
    cout<<v<<" ";visit[v]=true;//輸出第一個點,同時記錄已經訪問過 
    for(w=0;w<G.vexnum;w++)
    {
        if(G.arcs[v][w]!=0&&(!visit[w]))     DFSAM(G,w);//對尚未訪問過且與上一個頂點間存在邊的頂點遞歸調用 
    }
}

void DFS(AMGraph G)//非連通圖的深度優先搜索遍歷
{
    int v; 
    for(v=0;v<G.vexnum;v++)
        visit[v]=false;//標記數組初始化 
    for(v=0;v<G.vexnum;v++)
    {
        if(!visit[v])//對於未訪問的頂點調用DFSAMG函數 
        {    cout<<"{ ";    
            DFSAM(G,v); 
            cout<<"}"<<'\n';
        }
     } 
}
View Code

繼續是廣度優先搜索遍歷(BFS),感覺相對於DFS在算法上更難一點,需要借助隊列的存儲結構來完成,這跟上一章樹的層次遍歷差不多。上課時候討論過有兩種方法可以實現,一是先將頂點入隊,再訪問;二是先訪問頂點,再入隊,后者更優,它避免了將已經訪問過的頂點進行多余的入隊操作。

void BFSAM(AMGraph G,int v)//一個連通分量的廣度優先搜索遍歷 
{
    queue<int> q;
    int w,x;
    cout<<v<<" ";//采用先訪問再入隊方法 
    q.push(v); 
    visit[v]=true;//入隊第一個點,同時記錄已經訪問過
    while(!q.empty())//循環下面操作,直到隊空 
    {
        x=q.front();//記錄此時隊頭頂點,再出隊 
        q.pop();
        for(w=0;w<G.vexnum;w++)//遍歷查找鄰接點 
        {
            if(G.arcs[x][w]!=0&&(!visit[w]))//若兩點間有邊且未訪問    
            {
                cout<<w<<" "; //輸入此頂點 
                visit[w]=true;//標記已經訪問過
                q.push(w);//將此點入隊 
            }
         } 
    }
}

void BFS(AMGraph G)//非連通圖的深廣度優先搜索遍歷
{
    int v; 
    for(v=0;v<G.vexnum;v++)
        visit[v]=false;
    for(v=0;v<G.vexnum;v++)
    {
        if(!visit[v])//對於未訪問的頂點調用DFSALG函數 
        {    cout<<"{ ";    
            BFSAM(G,v); 
            cout<<"}"<<'\n';
        }
     } 
}
View Code

對於上面鄰接矩陣查找頂點的下一個鄰接點和判斷是否有邊存在,是通過遍歷所有頂點和一個if語句完成,而在鄰接表中,這一步操作就不一樣了。

鄰接矩陣比較熟悉,容易操作,但它適合用在稠密圖,空間復雜度高O(n2),稀疏圖中尤其浪費空間,所以有時需要采用鄰接表。因此,這道題我准備試一下用鄰接表,順便加深一下對算法的理解。鄰接表存儲結構的定義復雜許多,如下:

typedef struct Arcnode
{
    int ad;//頂點所在位置(下標)
    struct Arcnode *next;//指向下一條邊的指針
}Arcnode;

typedef struct Vnode//頂點信息(表)
{
    Arcnode *first;
}Vnode,Al[100];

typedef struct//鄰接表
{
    Al ver;//頂點數組
    int vexnum,arcnum;//頂點數和邊數
}ALGraph;
View Code

表頭結點和邊結點:

然后創建無向圖就是對每個指針指向的操作:

void create(ALGraph &G)
{
    int i,j,k,x,y;
    Arcnode *p,*p1;
    cin>>G.vexnum>>G.arcnum;
    for(i=0;i<G.vexnum;i++)
    G.ver[i].first=NULL;
    for(k=0;k<G.arcnum;k++)//輸入一條邊的兩個端點 
    {
        cin>>x>>y;
        p=new Arcnode;//無向圖的兩點互相指向對方 
        p->ad=y;p->next=G.ver[x].first;G.ver[x].first=p;
        p1=new Arcnode;
        p1->ad=x;p1->next=G.ver[y].first;G.ver[y].first=p1;
    } 
}
View Code

開始我在新結點那里出錯,出現很奇怪的結果,鏈式結構的存儲確實很容易出現小錯誤,接着在DFSAL中跟矩陣判斷條件有所不同,需要一個指針指向起始位置,在while循環里還有修改指針指向:

void DFSAL(ALGraph G,int v)//一個連通分量的深度優先搜索遍歷 
{
    int w;
    cout<<v<<" ";visit[v]=true;
    Arcnode *p2;
    p2=new Arcnode;
    //輸出第一個點,同時記錄已經訪問過 
    p2=G.ver[v].first;
    while(p2)
    {
        w=p2->ad;
        if(!visit[w])     DFSAL(G,w);//對尚未訪問過且與上一個頂點間存在邊的頂點遞歸調用 
        p2=p2->next;
    }
}
View Code

鄰接表的BFS算法操作有點難,我在這里卡了,出現許多錯誤,這里有隊列,有鏈表,在判斷和指針操作有些問題,開始問題出現有:沒有定義指針變量指向訪問頂點;在while循環之后未修改指針指向,導致死循環;再修改錯誤過程中,忘記將元素出隊還有出隊位置不對,運行出錯。經過一波修改,最后終於成功了

void BFSAL(ALGraph G,int v)//一個連通分量的廣度優先搜索遍歷 
{
    queue<int> q;
    int w,x;
    cout<<v<<" ";//采用先訪問再入隊方法 
    q.push(v); 
    visit[v]=true;//入隊第一個點,同時記錄已經訪問過
    while(!q.empty())//循環下面操作,直到隊空 
    {    
        x=q.front();//記錄此時隊頭頂點,再出隊 
        Arcnode *p3;
        p3=new Arcnode;
        p3=G.ver[x].first;
        q.pop();
        while(p3)
        {
            w=p3->ad;
            if(!visit[w])//若兩點間有邊且未訪問    
            {
                cout<<w<<" "; //輸入此頂點 
                visit[w]=true;//標記已經訪問過
                q.push(w);//將此點入隊 
            }
            p3=p3->next;
        }
    }
}
View Code

然后又發現一個問題,整個程序運行沒有問題,但是與題目輸出結果在第一個連通分量順序不同,那究竟在哪有錯誤,我仔細看了一遍,畫一下創建鄰接表時候每個點的關系,最后發現是因為存儲結構不同,就鏈表來說,這個創建的時候是前插法,輸出順序與輸入順序有關系,這道題要求“從編號最小的頂點出發,按編號遞增的順序訪問鄰接點”,就僅限於用鄰接矩陣方法實現。

但在這個過程中發現問題,解決問題,更明白許多,特別對不太熟悉的鄰接表方法,有更深的理解。下面是完整代碼,雖然暫時不適應解決這道題,但以后可能要用到這種思想。

#include<iostream>
#include<queue>
using namespace std; 
bool visit[100];//訪問標記數組 
typedef int ArcType; //

typedef struct Arcnode
{
    int ad;
    struct Arcnode *next;
}Arcnode;

typedef struct Vnode
{
    Arcnode *first;
}Vnode,Al[100];

typedef struct
{
    Al ver;
    int vexnum,arcnum;
}ALGraph;

void create(ALGraph &G)
{
    int i,j,k,x,y;
    Arcnode *p,*p1;
    cin>>G.vexnum>>G.arcnum;
    for(i=0;i<G.vexnum;i++)
    G.ver[i].first=NULL;
    for(k=0;k<G.arcnum;k++)//輸入一條邊的兩個端點 
    {
        cin>>x>>y;
        p=new Arcnode;
        p->ad=y;p->next=G.ver[x].first;G.ver[x].first=p;
        p1=new Arcnode;
        p1->ad=x;p1->next=G.ver[y].first;G.ver[y].first=p1;
    } 
}

void DFSAL(ALGraph G,int v)//一個連通分量的深度優先搜索遍歷 
{
    int w;
    cout<<v<<" ";visit[v]=true;
    Arcnode *p2;
    p2=new Arcnode;
    //輸出第一個點,同時記錄已經訪問過 
    p2=G.ver[v].first;
    while(p2)
    {
        w=p2->ad;
        if(!visit[w])     DFSAL(G,w);//對尚未訪問過且與上一個頂點間存在邊的頂點遞歸調用 
        p2=p2->next;
    }
}

void DFS(ALGraph G)//非連通圖的深度優先搜索遍歷
{
    int v; 
    for(v=0;v<G.vexnum;v++)
        visit[v]=false;//標記數組初始化 
    for(v=0;v<G.vexnum;v++)
    {
        if(!visit[v])//對於未訪問的頂點調用DFSAMG函數 
        {    cout<<"{ ";    
            DFSAL(G,v); 
            cout<<"}"<<'\n';
        }
     } 
} 

void BFSAL(ALGraph G,int v)//一個連通分量的廣度優先搜索遍歷 
{
    queue<int> q;
    int w,x;
    cout<<v<<" ";//采用先訪問再入隊方法 
    q.push(v); 
    visit[v]=true;//入隊第一個點,同時記錄已經訪問過
    while(!q.empty())//循環下面操作,直到隊空 
    {    
        x=q.front();//記錄此時隊頭頂點,再出隊 
        Arcnode *p3;
        p3=new Arcnode;
        p3=G.ver[x].first;
        q.pop();
        while(p3)
        {
            w=p3->ad;
            if(!visit[w])//若兩點間有邊且未訪問    
            {
                cout<<w<<" "; //輸入此頂點 
                visit[w]=true;//標記已經訪問過
                q.push(w);//將此點入隊 
            }
            p3=p3->next;
        }
    }
}

void BFS(ALGraph G)//非連通圖的深度優先搜索遍歷
{
    int v; 
    for(v=0;v<G.vexnum;v++)
        visit[v]=false;//標記數組初始化 
    for(v=0;v<G.vexnum;v++)
    {
        if(!visit[v])//對於未訪問的頂點調用DFSAMG函數 
        {    cout<<"{ ";    
            BFSAL(G,v); 
            cout<<"}"<<'\n';
        }
     } 
} 

int main()
{
    ALGraph g;
    create(g);
    DFS(g);
    BFS(g);
    return 0;
}
View Code

 這一章還有很多需要學習,這里只是對圖的理解和最基礎的的操作,后面許多算法還只停留在理解,實際應用還實現不了,接下來需要對圖的應用那部分內容有更多的學習和探索。


免責聲明!

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



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