圖的鄰接矩陣與鄰接表儲存方式及優缺點對比
概述
記錄一些圖的基本概念,以及圖的兩種表示方式(鄰接表和鄰接矩陣)的程式碼實現,最後總結了兩種方式的優缺點,還簡單介紹了十字連結串列和逆鄰接表。
圖的部分基本概念(我記不住的)
1、完全圖
一個無向圖,任意兩個頂點之間有且僅有一條邊,則稱為無向完全圖。若一個無向完全圖有 n 個頂點,那麼它就有
一個有向圖,任意兩個頂點之間有且僅有方向相反的兩條邊,則稱為有向完全圖。若一個有向完全圖有 n 個頂點,那麼它就有
2、鄰接頂點
在無向圖中,頂點 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、鄰接矩陣與鄰接表優缺點:
鄰接矩陣的優點是可以快速判斷兩個頂點之間是否存在邊,可以快速新增邊或者刪除邊。而其缺點是如果頂點之間的邊比較少,會比較浪費空間。因為是一個
而鄰接表的優點是節省空間,只儲存實際存在的邊。其缺點是關注頂點的度時,就可能需要遍歷一個連結串列。還有一個缺點是,對於無向圖,如果需要刪除一條邊,就需要在兩個連結串列上查詢並刪除。
擴充套件
逆鄰接表
在鄰接表中對於有向圖有一個很大的缺陷,如果我們比較關心頂點入度那麼就需要遍歷所有連結串列。為了避免這種情況出現,我們可以採用逆鄰接表來儲存,它儲存的連結串列是別的頂點指向它。這樣就可以快速求得頂點的入度。
如何對有向圖的入度和出度都關心,那麼久可以採取十字連結串列的方式。相當於每一個頂點對應兩個連結串列,一個是它指向別的頂點,一個是別的頂點指向它。
全文完