1. 程式人生 > >圖的鄰接矩陣與鄰接表儲存方式及優缺點對比

圖的鄰接矩陣與鄰接表儲存方式及優缺點對比

概述

記錄一些圖的基本概念,以及圖的兩種表示方式(鄰接表和鄰接矩陣)的程式碼實現,最後總結了兩種方式的優缺點,還簡單介紹了十字連結串列和逆鄰接表

圖的部分基本概念(我記不住的)

1、完全圖

一個無向圖,任意兩個頂點之間有且僅有一條邊,則稱為無向完全圖。若一個無向完全圖有 n 個頂點,那麼它就有 n(n1)/2 條邊。
一個有向圖,任意兩個頂點之間有且僅有方向相反的兩條邊,則稱為有向完全圖。若一個有向完全圖有 n 個頂點,那麼它就有 n(n1) 條邊。

2、鄰接頂點

無向圖中,頂點 u 與 v 之間有一條邊(u,v),則稱 u 和 v 互為鄰接頂點
有向圖

中,頂點 u 和 v 之間有一條邊<u, v>,則頂點 u 鄰接到頂點 v,頂點 v 鄰接自頂點 u,並稱邊<u,v>與頂點 u、v 相關聯。

3、頂點的度

頂點的度就是與它相關聯的邊的條數,入度表示邊的終點指向頂點的個數,出度表示邊的起點指向頂點的個數。

有向圖中,頂點的度等於入度與出度的和。

無向圖中,頂點的度 =入度 = 出度。

4、連通圖:

無向圖中,兩個頂點之間有路徑,則稱這兩個頂點是連通的。如果如中任意一對頂點都是連通的,則稱此圖為連通圖

在有向圖中,若任意一對頂點 vi 和 vj,都有從 vi 到 vj 的路徑,則稱此圖為強連通圖

5、生成樹:

一個連通圖(無向圖)的最小子圖稱為該圖的生成樹。有 n 個頂點的連通圖的生成樹有 n - 1 條邊。最小生成樹就是權值和最小的生成樹。

鄰接矩陣表示法

在一個一維陣列中儲存所有的點,在一個二維陣列中儲存頂點之間的邊的權值,以及一個布林值標記是否是有向圖,預設是無向圖:

    bool        isDirected;  // 標識是否是有向圖,預設為false
    vector<V>       vertex;  // 儲存頂點
    vector<vector<W> > edge; // 儲存邊

graph.h

#ifndef GRAPH_H
#define GRAPH_H #include <vector> #include <cassert> #include <iostream> using namespace std; // V -- 圖頂點的資料型別 // W -- 圖邊權值的型別 template <typename V, typename W> class GraphMatrix { public: GraphMatrix(const V* _vertex, size_t _size, bool _isDirected = false); // 列印圖中的所有邊 void printEdge(); // 向圖中新增一條邊 void addEdge(const V& v1, const V& v2, const W& weight); private: // 獲取邊所在的下標 int getIndexOfVertex(const V& _vertex); private: bool isDirected; // 標識是否是有向圖,預設為false vector<V> vertex; // 儲存頂點 vector<vector<W> > edge; // 儲存邊 }; #endif //GRAPH_H

graph.cpp

#include "graph.h"


/*
*   public 函式
*/

template<typename V, typename W>
GraphMatrix<V,W>::GraphMatrix(const V* _vertex, size_t _size, bool _isDirected = false)
{
    // 開闢空間並初始化資料
    this->vertex.resize(_size);
    this->edge.resize(_size);
    this->isDirected = _isDirected;

    for (int idx = 0; idx < _size; ++idx)
    {
        this->vertex[idx] = _vertex[idx];
        this->edge[idx].resize(_size);
    }
}

template<typename V, typename W>
void GraphMatrix<V, W>::addEdge(const V& v1, const V& v2, const W& weight)
{
    int start = getIndexOfVertex(v1);
    int end = getIndexOfVertex(v2);

    edge[start][end] = weight;

    // 如果是無向圖還需要新增對稱的一遍
    if (!isDirected)
        edge[end][start] = weight;
}

template<typename V, typename W>
void GraphMatrix<V, W>::printEdge()
{
    for (int idx = 0; idx < vertex.size(); ++idx)
        cout <<  "" <<  vertex[idx];
    cout << endl;

    for (int idx_row = 0; idx_row < edge.size(); ++idx_row)
    {
        cout << vertex[idx_row] << " ";
        for (int idx_col = 0; idx_col < edge.size(); ++idx_col)
        {
            cout <<  " " << edge[idx_row][idx_col] ;
        }
        cout << endl;
    }
    cout << endl;
}

/*
*   private 函式
*/

template<typename V, typename W>
int GraphMatrix<V, W>::getIndexOfVertex(const V& v)
{
    for (int idx = 0; idx < vertex.size(); idx++)
    {
        if (vertex[idx] == v)
            return idx;
    }

    // 如果沒有找到就說明發生了錯誤
    assert(false);
    return -1;
}

test.cpp

#include "graph.cpp"

#include <string>
#include <vector>
#include <iostream>
using namespace std;

void TestGraphMatrix()
{
    // 無向圖
    const char* str = "ABCDE";
    GraphMatrix<char, int> graph(str, strlen(str));
    graph.addEdge('A', 'D', 8);
    graph.addEdge('A', 'E', 9);
    graph.addEdge('B', 'E', 2);
    graph.addEdge('A', 'B', 6);
    graph.printEdge();

    cout << "--------------------------------" << endl;

    // 有向圖
    GraphMatrix<char, int> graph1(str, strlen(str), true);
    graph1.addEdge('A', 'D', 8);
    graph1.addEdge('A', 'E', 9);
    graph1.addEdge('B', 'E', 2);
    graph1.addEdge('E', 'C', 6);
    graph1.printEdge();


}

int main()
{
    TestGraphMatrix();
    return 0;
}

輸出結果:

  A B C D E
A 0 6 0 8 9
B 6 0 0 0 2
C 0 0 0 0 0
D 8 0 0 0 0
E 9 2 0 0 0

-----------

  A B C D E
A 0 0 0 8 9
B 0 0 0 0 2
C 0 0 0 0 0
D 0 0 0 0 0
E 0 0 6 0 0

鄰接表表示法

鄰接表是把頂點之間的邊當做連結串列上的結點,其中邊結點資料成員有:

  • 起點的索引
  • 終點的索引
  • 邊所在權值
  • 指向下一個結點的指標
// W -- 邊對應權值的型別
template <typename W>
struct EdgeNode
{
    W                weight;  // 邊所對應權值
    unsigned int startIndex;  // 邊起點的索引
    unsigned int   endIndex;  // 邊終點的索引
    EdgeNode<W>*   nextNode;  // 指向下個結點
};

用一個一維陣列來儲存所有的頂點。一個一維陣列來儲存頂點所對應的連結串列的頭指標(結點表示以當前頂點為起點的邊)。

bool                isDirected; 
vector<V>               vertex; // 儲存所有頂點
vector<EdgeNode<W>*> linkTable; // 儲存頂點的邊

graph.h

#ifndef GRAPH_H
#define GRAPH_H

#include <vector>
#include <cassert>
#include <iostream>
using namespace std;

// W -- 邊對應權值的型別
template <typename W>
struct EdgeNode
{
    W                weight;  // 邊所對應權值
    size_t startIndex;  // 邊起點的索引
    size_t   endIndex;  // 邊終點的索引
    EdgeNode<W>*   nextNode;  // 指向下個結點
    EdgeNode(size_t start, size_t end, const W& _weight)
        : startIndex(start)
        , endIndex(end)
        , weight(_weight)
    {}
};

// V -- 圖頂點的資料型別
// W -- 圖邊權值的型別
template <typename V, typename W>
class GraphLink
{
public:
    typedef EdgeNode<W> node;

    GraphLink(const V* _vertex, size_t _size, bool _isDirected = false);
    // 列印圖中的邊
    void printEdge();
    // 向圖中新增一條邊
    void addEdge(const V& v1, const V& v2, const W& weight);

private:
    // 獲取頂點所在索引
    size_t getIndexOfVertex(const V& v);
    // 新增一條邊
    void __addEdge(size_t startIndex, size_t endIndex, const W& weight);

private:
    bool         isDirected; 
    vector<V>        vertex; // 儲存所有頂點
    vector<node*> linkTable; // 儲存頂點的邊
};


#endif //GRAPH_H

graph.cpp

#include "graph.h"


/*
*   public 函式
*/

template<typename V, typename W>
GraphLink<V, W>::GraphLink(const V* _vertex, size_t _size, bool _isDirected)
{
    // 開闢空間並初始化資料
    this->vertex.resize(_size);
    this->linkTable.resize(_size);
    this->isDirected = _isDirected;

    for (size_t i = 0; i < _size; i++)
    {
        this->vertex[i] = _vertex[i];
    }
}

template<typename V, typename W>
void GraphLink<V, W>::printEdge()
{
    for (size_t idx = 0; idx < vertex.size(); ++idx)
    {
        cout << vertex[idx] << ": ";

        node* pEdge = linkTable[idx];
        while (pEdge)
        {
            cout << pEdge->weight << "[" << vertex[pEdge->endIndex] << "]-->";
            pEdge = pEdge->nextNode;
        }
        cout << "NULL" << endl;
    }
    cout << endl;
}

template<typename V, typename W>
void GraphLink<V, W>::addEdge(const V& v1, const V& v2, const W& weight)
{
    size_t startIndex = getIndexOfVertex(v1);
    size_t endIndex   = getIndexOfVertex(v2);

    // 防止填加自己指向自己的邊
    assert( startIndex!=endIndex);

    __addEdge(startIndex, endIndex, weight);

    // 無向圖需要新增對稱的一條邊
    if (!isDirected)
        __addEdge(endIndex, startIndex, weight);

}



/*
*   private 函式
*/



template <typename V, typename W>
void GraphLink<V, W>::__addEdge(size_t startIndex, size_t endIndex, const W& weight)
{
    // 頭插的方式新增邊到連結串列中
    node* pNewEdge = new node(startIndex, endIndex, weight);
    pNewEdge->nextNode = linkTable[startIndex];
    linkTable[startIndex] = pNewEdge;
}

template<typename V, typename W>
size_t GraphLink<V, W>::getIndexOfVertex(const V& v)
{
    for (int idx = 0; idx < vertex.size(); idx++)
    {
        if (vertex[idx] == v)
            return idx;
    }

    // 如果沒有找到就說明發生了錯誤
    assert(false);
    return -1;
}

test.cpp

#include "graph.cpp"
#include <string>
#include <vector>
#include <iostream>
using namespace std;


void TestGraphLink()
{
    // 無向圖
    char* str = "ABCDE";
    GraphLink<char, int> graph1(str, strlen(str)); 
    graph1.addEdge('A', 'C', 2);
    graph1.addEdge('D', 'B', 6);
    graph1.addEdge('A', 'B', 4);
    graph1.addEdge('E', 'D', 9);
    graph1.printEdge();

    cout << "------------" << endl;
    // 有向圖

    GraphLink<char, int> graph2(str, strlen(str), true);
    graph2.addEdge('D', 'C', 2);
    graph2.addEdge('B', 'E', 6);
    graph2.addEdge('A', 'D', 4);
    graph2.addEdge('E', 'D', 9);
    graph2.printEdge();
}

int main()
{

    TestGraphLink();
    return 0;
}

輸出結果:

A: 4[B]-->2[C]-->NULL
B: 4[A]-->6[D]-->NULL
C: 2[A]-->NULL
D: 9[E]-->6[B]-->NULL
E: 9[D]-->NULL

----------------------

A: 4[D]-->NULL
B: 6[E]-->NULL
C: NULL
D: 2[C]-->NULL
E: 9[D]-->NULL

總結與對比

1、在鄰接矩陣表示中,無向圖的鄰接矩陣是對稱的。矩陣中第 i 行或 第 i 列有效元素個數之和就是頂點的讀。

在有向圖中 第 i 行有效元素個數之和是頂點的出度,第 i 列有效元素個數之和是頂點的入度。

2、在鄰接表的表示中,無向圖的同一條邊在鄰接表中儲存的兩次。如果想要知道頂點的讀,只需要求出所對應連結串列的結點個數即可。

有向圖中每條邊在鄰接表中只出現一此,求頂點的出度只需要遍歷所對應連結串列即可。求出度則需要遍歷其他頂點的連結串列。

3、鄰接矩陣與鄰接表優缺點

鄰接矩陣的優點是可以快速判斷兩個頂點之間是否存在邊,可以快速新增邊或者刪除邊。而其缺點是如果頂點之間的邊比較少,會比較浪費空間。因為是一個 nn 的矩陣。

而鄰接表的優點是節省空間,只儲存實際存在的邊。其缺點是關注頂點的度時,就可能需要遍歷一個連結串列。還有一個缺點是,對於無向圖,如果需要刪除一條邊,就需要在兩個連結串列上查詢並刪除。

擴充套件

逆鄰接表

在鄰接表中對於有向圖有一個很大的缺陷,如果我們比較關心頂點入度那麼就需要遍歷所有連結串列。為了避免這種情況出現,我們可以採用逆鄰接表來儲存,它儲存的連結串列是別的頂點指向它。這樣就可以快速求得頂點的入度。

如何對有向圖的入度和出度都關心,那麼久可以採取十字連結串列的方式。相當於每一個頂點對應兩個連結串列,一個是它指向別的頂點,一個是別的頂點指向它。

全文完