1 圖的兩種存儲方式
1.1 鄰接矩陣(Adjacency Matrix)
1.1.1 原理
用一維數組存儲圖中頂點信息;用二維數組(矩陣)存儲圖中的邊和弧的信息。對於無向圖來說,如果頂點i與頂點j之間有邊,就將A[i][j]和A[j][i]標記為1;對於有向圖來說,如果頂點i和頂點j之間,有一條箭頭從頂點i指向頂點j的邊,就將A[i][j]標記為1,有箭頭從頂點j指向頂點i的邊,就將A[j][i]標記為1。對於有權圖,數組中存儲相應權重。
鄰接矩陣可表示為:
vertex[4] = {v0, v1, v2, v3};
edge[4][4] = { {0, 1, 1, 0},
{1, 0, 0, 1},
{1, 0, 0, 1},
{0, 1, 1, 0} };
1.1.2 優缺點
1.1.2.1 優點
1)基於數組,存儲方式簡單、直接,獲取頂點關系時非常高效;
2)計算方便,鄰接矩陣的方式存儲圖,可以將很多圖的運算轉換成矩陣之間的運算。
1.1.2.2 缺點
浪費存儲空間。對於無向圖來說,如果A[i][j]為1,那么A[j][i]也為1,實際上,我們只需要存儲一個就可以了;如果我們存儲的是稀疏圖(sparse matrix),也就是頂點很多,但每個頂點的邊不多,那就更加浪費空間了。
1.1.3 適用場景
1)要求較高速的計算速率;2)運行內存充足;3)圖的頂點個數n值較小;4)有向圖且為稠密圖;
1.1.4 鄰接矩陣存儲圖的代碼實現
#ifndef GRAPH_H #define GRAPH_H #define MAX_VERTEX (10) //鄰接矩陣圖 class AdjacencyMatrixGraph { private: int m_nVertex[MAX_VERTEX]; //頂點數組 int m_nEdge[MAX_VERTEX][MAX_VERTEX]; //鄰接矩陣 int m_nCurrentVertex; //當前圖中頂點個數 int m_nCurrentEdge; //當前圖中邊的個數 bool visited[MAX_VERTEX]; //訪問數組 public: void CreatGraph(); //創建鄰接矩陣圖 void DisplayGraph(); //以鄰接矩陣形式顯示圖 void MatrixDFS(); //深度優先遍歷 void MDFS(int i); //深度優先遍歷遞歸調用 void MatrixBFS(); //廣度優先遍歷 }; #endif
/* 創建鄰接矩陣圖 * 1----2 * / \ / |\ * 6 /\ | 3 * \ / \|/ * 5---- 4 * 頂點 6 個、邊為 1-2/1-4/1-6/2-3/2-4/2-5/3-4/4-5/5-6共9條 */ void AdjacencyMatrixGraph::CreatGraph() { //輸入頂點個數、邊數 std::cout << "輸入頂點個數、邊數:" << std::endl; std::cin >> m_nCurrentVertex >> m_nCurrentEdge; //初始化頂點數組,數組值為下表加1 for (int i = 0; i < m_nCurrentVertex; ++i) { m_nVertex[i] = i + 1; } //初始化鄰接矩陣 for (int i = 0; i < m_nCurrentVertex; ++i) { for (int j = 0; j < m_nCurrentVertex; ++j) { m_nEdge[i][j] = 0; } } //初始化圖 int m, n; for (int i = 0; i < m_nCurrentEdge; ++i) { std::cout << "輸入頂點m、鄰接點n" << std::endl; std::cin >> m >> n; m_nEdge[m - 1][n - 1] = m_nEdge[n - 1][m - 1] = 1; } } //以鄰接矩陣形式顯示圖 void AdjacencyMatrixGraph::DisplayGraph() { //顯示頂點 std::cout << "頂點有:" << std::endl; for (int i = 0; i < m_nCurrentVertex; ++i) { std::cout << m_nVertex[i] << " | "; } std::cout << std::endl; /*顯示鄰接矩陣 * 1 2 3 4 5 6 * 1 0 1 0 1 0 1 * 2 1 0 1 1 1 0 * 3 0 1 0 1 0 0 * 4 1 1 1 0 1 0 * 5 0 1 0 1 0 1 * 6 1 0 0 0 1 0 */ std::cout << " "; for (int i = 0; i < m_nCurrentVertex; ++i) { std::cout << m_nVertex[i] << ' '; } std::cout << std::endl; for (int i = 0; i < m_nCurrentVertex; ++i) { std::cout << i + 1 << ' '; for (int j = 0; j < m_nCurrentVertex; ++j) { std::cout << m_nEdge[i][j] << ' '; } std::cout << std::endl; } }
1.2 鄰接表(Adjacency List)
1.2.1 原理
鄰接表類似於散列表的拉鏈表示法。每個頂點對應一條鏈表,鏈表中存儲的是與這個頂點相連接的其他頂點。以上圖為例,鄰接表可表示為:
每條鏈表的頭結點組成頂點的數組。
1.2.2 優缺點
1)優點:節省內存空間;2)缺點:鄰接表使用起來比較耗時,鏈表的存儲方式堆緩存不友好。
1.2.3 使用場景
1)有內存限制;2)對運行速度要求不高;3)頂點個數較大時;
1.2.4 鄰接表存儲圖的代碼實現
#ifndef GRAPH_H #define GRAPH_H //鄰接表圖 typedef struct AdjacencyNode { //鄰接點元素 int m_nAdjacencyNode; //鄰接點值 AdjacencyNode* m_pNextNode; }* AdjacencyNodePtr; typedef struct VertexNode { //頂點數組元素 int m_nVertex; //頂點值 AdjacencyNodePtr m_pFirstAdjacencyNode; //頂點的第一鄰接點 }* VertexNodePtr; class AdjacencyListGraph { private: VertexNodePtr m_pVertexArr; //指向頂點數組 int m_nCurrentVertex; int m_nCurrentEdge; bool* visited; //指向訪問數組 public: void CreatGraph(); //創建鄰接表圖 void DisplayGraph(); //以鄰接表形式顯示圖 void ListDFS(); //鄰接表的深度優先遍歷 void LDFS(int n); //深度優先遍歷遞歸函數 void ListBFS(); //鄰接表的廣度優先遍歷 }; #endif
/* 創建鄰接矩陣圖 * 1----2 * / \ / |\ * 6 /\ | 3 * \ / \|/ * 5---- 4 * 頂點 6 個、邊為 1-2/1-4/1-6/2-3/2-4/2-5/3-4/4-5/5-6共9條 */ void AdjacencyListGraph::CreatGraph() { std::cout << "輸入頂點個數、邊數" << std::endl; std::cin >> m_nCurrentVertex >> m_nCurrentEdge; m_pVertexArr = new VertexNode[m_nCurrentVertex]; //初始化頂點數組 for (int i = 0; i < m_nCurrentVertex; ++i) { m_pVertexArr[i].m_nVertex = i + 1; m_pVertexArr[i].m_pFirstAdjacencyNode = nullptr; } //初始化鄰接表 for (int i = 0; i < m_nCurrentEdge; ++i) { int m, n; std::cout << "輸入邊(m, n)" << std::endl; std::cin >> m >> n; //頂點m的鄰接表 AdjacencyNodePtr pNewAdjacencyNode = new AdjacencyNode; pNewAdjacencyNode->m_pNextNode = m_pVertexArr[m - 1].m_pFirstAdjacencyNode; pNewAdjacencyNode->m_nAdjacencyNode = n; m_pVertexArr[m - 1].m_pFirstAdjacencyNode = pNewAdjacencyNode; //頂點n的鄰接表 pNewAdjacencyNode = new AdjacencyNode; pNewAdjacencyNode->m_pNextNode = m_pVertexArr[n - 1].m_pFirstAdjacencyNode; pNewAdjacencyNode->m_nAdjacencyNode = m; m_pVertexArr[n - 1].m_pFirstAdjacencyNode = pNewAdjacencyNode; } } //已鄰接表顯示圖 void AdjacencyListGraph::DisplayGraph() { for (int i = 0; i < m_nCurrentVertex; ++i) { //顯示樣例:1->2->4->6 std::cout << m_pVertexArr[i].m_nVertex << "->"; AdjacencyNodePtr pWorkNode = m_pVertexArr[i].m_pFirstAdjacencyNode; while (pWorkNode) { std::cout << pWorkNode->m_nAdjacencyNode; pWorkNode = pWorkNode->m_pNextNode; if (pWorkNode) std::cout << "->"; } std::cout << std::endl; } }
2 深度優先遍歷(DFS)
2.1 原理
深度優先遍歷(depth first search)圖的方式類似於先序遍歷二叉樹。從圖中的某個頂點出發,訪問次頂點,然后從該頂點的未被訪問的鄰接點出發深度優先遍歷,直到圖中所有和該頂點相通的點都被訪問到。
2.2 實現及關鍵點
2.2.1 關鍵點
1)由原理可知,深度優先遍歷的實現依賴於遞歸思想,因此特別注意遞歸公式和遞歸終止條件;
2)由於二叉樹的層次關系,先序遍歷二叉樹時是層層遞進的,不會重復訪問已訪問過的節點;但是圖並沒有層次關系,所以要有標記—訪問數組,記錄已訪問過的頂點,防止頂點的重復訪問。
2.2.2 實現
//Adjacency Matrix //深度優先遍歷 void AdjacencyMatrixGraph::MDFS(int i) { visited[i] = true; std::cout << m_nVertex[i] << " | "; //打印路徑 for (int j = 0; j < m_nCurrentVertex; ++j) { if (!visited[j] && m_nEdge[i][j]) { //鄰接點未被訪問且存在邊 MDFS(j); } } } void AdjacencyMatrixGraph::MatrixDFS() { //設置訪問數組,訪問過的頂點設置為true for (int i = 0; i < m_nCurrentVertex; ++i) { visited[i] = false; } //從其中一個頂點開始遍歷圖 MDFS(0); std::cout << std::endl; } //Adjacency List void AdjacencyListGraph::LDFS(int n) { visited[n] = true; //當前遍歷頂點置true,表示已訪問過 std::cout << m_pVertexArr[n].m_nVertex << " | "; //遍歷當前頂點各個鄰接點 AdjacencyNodePtr pWorkNode = m_pVertexArr[n].m_pFirstAdjacencyNode; int vertex; while (pWorkNode) { vertex = pWorkNode->m_nAdjacencyNode; //鄰接點 //如果頂點還未被訪問 if (!visited[vertex - 1]) { LDFS(vertex - 1); } else { pWorkNode = pWorkNode->m_pNextNode; } } } void AdjacencyListGraph::ListDFS() { //初始化訪問數組 visited = new bool[m_nCurrentVertex]; for (int i = 0; i < m_nCurrentVertex; ++i) { visited[i] = false; } //任選一個頂點開始遍歷 LDFS(0); std::cout << std::endl; }
2.3 復雜度分析
2.3.1 鄰接矩陣存儲方式
1)時間復雜度
遍歷每個頂點時,都要判斷其余頂點是否與該頂點相鄰,所以要遍歷整個鄰接矩陣中的所有元素,故時間復雜度為O(V2),V為頂點個數。
2)空間復雜度
遍歷過程中需要訪問數組來標記頂點是否被訪問,訪問數組大小等於頂點個數V,所以空間復雜度為O(V)。
2.3.2 鄰接表存儲方式
1)時間復雜度
鄰接表的存儲方式會先訪問頂點,然后再判斷鏈表中的每個鄰接點(邊),每條會訪問2次,所以時間復雜度為O(V+2E),V為頂點個數、E為邊的個數。
2)空間復雜度
同鄰接矩陣,空間復雜度為O(V)。
3 廣度優先遍歷(BFS)
3.1 原理
廣度優先遍歷(breadth first search)可以類比為二叉樹的層序遍歷;先查找離起始頂點最近的,然后是次進的,依次往外搜索。
3.2 實現及關鍵點
3.2.1 關鍵點
1)借助隊列將當前遍歷頂點的鄰接點存儲起來,使得搜索路徑有層次;
2)同深度優先搜索中,需要借助訪問數組標記已訪問頂點;
2)頂點元素入隊時要將訪問數組相關頂點標記為已訪問,出隊時執行會造成頂點的重復訪問。
//Adjacency Matrix void AdjacencyMatrixGraph::MatrixBFS() { //初始化訪問數組 for (int i = 0; i < m_nCurrentVertex; ++i) { visited[i] = false; } std::queue<int> vertexQueue; //存放已遍歷頂點的鄰接點 int frontVertex = 0; //隊列中首元素 visited[0] = true; vertexQueue.push(0); //將頂點1放入隊列中 while (!vertexQueue.empty()) { frontVertex = vertexQueue.front(); //打印首元素 std::cout << frontVertex + 1 << " | "; //將鄰接點加入隊列 for (int i = 0; i < m_nCurrentVertex; ++i) { if (!visited[i] && m_nEdge[frontVertex][i]) { visited[i] = true; //注意!訪問數組先置位再將頂點下標放入隊列,否則會造成重復 vertexQueue.push(i); } } //首元素出列 vertexQueue.pop(); } std::cout << std::endl; } //Adjacency List void AdjacencyListGraph::ListBFS() { //初始化訪問數組 visited = new bool[m_nCurrentVertex]; for (int i = 0; i < m_nCurrentVertex; ++i) { visited[i] = false; } std::queue<int> vertexQueue; //存放已訪問頂點的鄰接點 int vertex = m_pVertexArr[0].m_nVertex; vertexQueue.push(vertex); //將遍歷起始點放入隊列 visited[vertex - 1] = true; std::cout << vertex << " | "; while (!vertexQueue.empty()) { vertex = vertexQueue.front(); vertexQueue.pop(); AdjacencyNodePtr pWorkNode = m_pVertexArr[vertex - 1].m_pFirstAdjacencyNode; while (pWorkNode) { vertex = pWorkNode->m_nAdjacencyNode; if (!visited[vertex - 1]) { vertexQueue.push(vertex); visited[vertex - 1] = true; std::cout << vertex << " | "; } pWorkNode = pWorkNode->m_pNextNode; } } std::cout << std::endl; }
2.4 復雜度分析
2.4.1 鄰接矩陣存儲方式
1)時間復雜度
遍歷過程中,每個頂點出隊時,都要判斷與該頂點相鄰的所有頂點是否已訪問,因為遍歷整個圖,所以所有頂點都要有入隊、出隊操作,所以時間復雜度為O(V2)。
2)空間復雜度
遍歷操作用到訪問數組與隊列,隊列長度不超過頂點個數,所以空間復雜為O(V)。
2.4.2 鄰接表存儲方式
1)時間復雜度
同樣的,每個頂點出隊時,都要遍歷其鄰接點鏈表(邊),所以時間復雜度為O(V+2E)。
2)空間復雜度
同鄰接矩陣存儲方式,空間復雜度為O(V)。
該篇博客是自己的學習博客,水平有限,如果有哪里理解不對的地方,希望大家可以指正!