簡介
在現實生活中,有許多應用場景會包含很多點以及點點之間的連接,而這些應用場景我們都可以用即將要學習的圖這種數據結構去解決。
地圖:
我們生活中經常使用的地圖,基本上是由城市以及連接城市的道路組成,如果我們把城市看做是一個一個的點,把道路看做是一條一條的連接,那么地圖就是我們將要學習的圖這種數據結構。
電路圖:
下面是一個我們生活中經常見到的集成電路板,它其實就是由一個一個觸點組成,並把觸點與觸點之間通過線進行連接,這也是我們即將要學習的圖這種數據結構的應用場景
圖的定義及分類:
定義:圖是由一組頂點和一組能夠將兩個頂點相連的邊組成的

特殊的圖:
- 自環:即一條連接一個頂點和其自身的邊;
- 平行邊:連接同一對頂點的兩條邊;

圖的分類:
按照連接兩個頂點的邊的不同,可以把圖分為以下兩種:
無向圖:邊僅僅連接兩個頂點,沒有其他含義;
有向圖:邊不僅連接兩個頂點,並且具有方向;
無向圖
圖的相關術語
相鄰頂點:當兩個頂點通過一條邊相連時,我們稱這兩個頂點是相鄰的,並且稱這條邊依附於這兩個頂點。
度:某個頂點的度就是依附於該頂點的邊的個數
子圖:是一幅圖的所有邊的子集(包含這些邊依附的頂點)組成的圖;
路徑:是由邊順序連接的一系列的頂點組成
環:是一條至少含有一條邊且終點和起點相同的路徑

連通圖:如果圖中任意一個頂點都存在一條路徑到達另外一個頂點,那么這幅圖就稱之為連通圖
連通子圖:一個非連通圖由若干連通的部分組成,每一個連通的部分都可以稱為該圖的連通子圖

圖的存儲結構
要表示一幅圖,只需要表示清楚以下兩部分內容即可:
- 圖中所有的頂點;
- 所有連接頂點的邊;
常見的圖的存儲結構有兩種:鄰接矩陣和鄰接表
鄰接矩陣
- 使用一個V*V的二維數組int[V][V] adj,把索引的值看做是頂點;
- 如果頂點v和頂點w相連,我們只需要將adj[v][w]和adj[w][v]的值設置為1,否則設置為0即可。
很明顯,鄰接矩陣這種存儲方式的空間復雜度是V^2的,如果我們處理的問題規模比較大的話,內存空間極有可能不夠用。
鄰接表
1.使用一個大小為V的數組 Queue[V] adj,把索引看做是頂點;
2.每個索引處adj[v]存儲了一個隊列,該隊列中存儲的是所有與該頂點相鄰的其他頂點
很明顯,鄰接表的空間並不是是線性級別的,所以后面我們一直采用鄰接表這種存儲形式來表示圖。
代碼實現
圖API設計:
| 類名 | Graph |
|---|---|
| 構造方法 | Graph(int V):創建一個包含V個頂點但不包含邊的圖 |
| 成員方法 | 1.public int V():獲取圖中頂點的數量 2.public int E():獲取圖中邊的數量 3.public void addEdge(int v,int w):向圖中添加一條邊 v-w 4.public Queue adj(int v):獲取和頂點v相鄰的所有頂點 |
| 成員變量 | 1.private final int V: 記錄頂點數量 2.private int E: 記錄邊數量 3.private Queue[] adj: 鄰接表 |
/**
* @author wen.jie
* @date 2021/8/27 10:43
* 無向圖
*/
public class Graph {
//頂點數目
final int V;
//邊的數目
int E;
//鄰接表
Queue<Integer>[] adj;
//初始化
public Graph(int v) {
this.V = v;
this.E = 0;
this.adj = new Queue[v];
for (int i = 0; i < adj.length; i++) {
adj[i] = new Queue<>();
}
}
public int V(){
return V;
}
public int E(){
return E;
}
//向圖中添加一條邊 v-w
public void addEdge(int v, int w) {
adj[v].enqueue(w);
adj[w].enqueue(v);
E++;
}
//獲取和頂點v相鄰的所有頂點
public Queue<Integer> adj(int v) {
return adj[v];
}
}
圖的搜索
在很多情況下,我們需要遍歷圖,得到圖的一些性質,例如,找出圖中與指定的頂點相連的所有頂點,或者判定某個頂點與指定頂點是否相通,是非常常見的需求。
有關圖的搜索,最經典的算法有深度優先搜索和廣度優先搜索,接下來我們分別講解這兩種搜索算法。
深度優先搜索
所謂的深度優先搜索,指的是在搜索時,如果遇到一個結點既有子結點,又有兄弟結點,那么先找子結點,然后找 兄弟結點。
很明顯,在由於邊是沒有方向的,所以,如果4和5頂點相連,那么4會出現在5的相鄰鏈表中,5也會出現在4的相鄰鏈表中,那么為了不對頂點進行重復搜索,應該要有相應的標記來表示當前頂點有沒有搜索過,可以使用一個布爾類型的數組 boolean[V] marked,索引代表頂點,值代表當前頂點是否已經搜索,如果已經搜索,標記為true, 如果沒有搜索,標記為false;
api設計:
| 類名 | DepthFirstSearch |
|---|---|
| 構造方法 | DepthFirstSearch(Graph G,int s):構造深度優先搜索對象,使用深度優先搜索找出G圖中s頂點 的所有相通頂點 |
| 成員方法 | 1.private void dfs(Graph G, int v):使用深度優先搜索找出G圖中v頂點的所有相通頂點 2.public boolean marked(int w):判斷w頂點與s頂點是否相通 3.public int count():獲取與頂點s相通的所有頂點的總數 |
| 成員變量 | 1.private boolean[] marked: 索引代表頂點,值表示當前頂點是否已經被搜索 2.private int count:記錄有多少個頂點與s頂點相通 |
代碼實現:
/**
* dfs:深度優先搜索
* @author wen.jie
* @date 2021/8/27 11:08
*/
public class DepthFirstSearch {
//索引代表頂點,值代表當前頂點是否已經被搜索
private boolean[] marked;
//記錄有多少頂點與s頂點相通
private int count;
public DepthFirstSearch(Graph G, int s) {
this.marked = new boolean[G.V()];
dfs(G, s);
}
//深度優先搜索找出G圖中v頂點的所有相通頂點
private void dfs(Graph G, int v) {
//標為已搜索
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked(w))
dfs(G, w);
}
count++;
}
//判斷w頂點與s頂點是否相通
public boolean marked(int w){
return marked[w];
}
//獲取與頂點s相通的所有頂點的總數
public int count() {
return this.count;
}
}
構造如下的圖,並測試:
private Graph graph = new Graph(13);
{
graph.addEdge(0, 5);
graph.addEdge(0, 1);
graph.addEdge(0, 2);
graph.addEdge(0, 6);
graph.addEdge(6, 4);
graph.addEdge(4, 3);
graph.addEdge(4, 5);
graph.addEdge(5, 3);
graph.addEdge(7, 8);
graph.addEdge(9, 10);
graph.addEdge(9, 11);
graph.addEdge(9, 12);
graph.addEdge(11, 12);
}
@Test
public void test1() {
DepthFirstSearch dfs = new DepthFirstSearch(graph, 0);
int count = dfs.count();
System.out.println("與起點0相通的頂點數量為:"+ count);
System.out.println(dfs.marked(5));
System.out.println(dfs.marked(7));
}

廣度優先搜索
所謂的廣度優先搜索,指的是在搜索時,如果遇到一個結點既有子結點,又有兄弟結點,那么先找兄弟結點,然后找子結點。
api設計:
| 類名 | BreadthFirstSearch |
|---|---|
| 構造方法 | BreadthFirstSearch(Graph G,int s):構造廣度優先搜索對象,使用廣度優先搜索找出G圖中s頂點的所有相鄰頂點 |
| 構造方法 | 1.private void bfs(Graph G, int v):使用廣度優先搜索找出G圖中v頂點的所有相鄰頂點 2.public boolean marked(int w):判斷w頂點與s頂點是否相通 3.public int count():獲取與頂點s相通的所有頂點的總數 |
| 成員變量 | 1.private boolean[] marked: 索引代表頂點,值表示當前頂點是否已經被搜索 2.private int count:記錄有多少個頂點與s頂點相通 3.private Queue waitSearch: 用來存儲待搜索鄰接表的點 |
代碼實現:
/**
* @author wen.jie
* @date 2021/8/27 13:43
* bfs:廣度優先遍歷
*/
public class BreadthFirstSearch {
//索引代表頂點,值代表當前頂點是否已經被搜索
private boolean[] marked;
//記錄有多少頂點與s頂點相通
private int count;
//用來存儲待搜索鄰接表的點
private Queue<Integer> waitSearch;
public BreadthFirstSearch(Graph G, int s) {
this.marked = new boolean[G.V()];
this.waitSearch = new Queue<>();
bfs(G, s);
}
//廣度優先搜索找出G圖中v頂點的所有相通頂點
private void bfs(Graph G, int v) {
count++;
marked[v] = true;
//入隊列,待搜索
waitSearch.enqueue(v);
while (!waitSearch.isEmpty()) {
//出隊列
Integer wait = waitSearch.dequeue();
for (Integer w : G.adj(wait)) {
if (!marked[w]) {
waitSearch.enqueue(w);
marked[w] = true;
count++;
}
}
}
}
//判斷w頂點與s頂點是否相通
public boolean marked(int w){
return marked[w];
}
//獲取與頂點s相通的所有頂點的總數
public int count() {
return this.count;
}
}
測試:
@Test
public void test2() {
BreadthFirstSearch bfs = new BreadthFirstSearch(graph, 0);
int count = bfs.count();
System.out.println("與起點0相通的頂點數量為:"+ count);
System.out.println(bfs.marked(5));
System.out.println(bfs.marked(7));
}
路徑查找
在實際生活中,地圖是我們經常使用的一種工具,通常我們會用它進行導航,輸入一個出發城市,輸入一個目的地城市,就可以把路線規划好,而在規划好的這個路線上,會路過很多中間的城市。這類問題翻譯成專業問題就是: 從s頂點到v頂點是否存在一條路徑?如果存在,請找出這條路徑。

例如在上圖上查找頂點0到頂點4的路徑用紅色標識出來,那么我們可以把該路徑表示為 0-2-3-4。
深度優先遍歷查找路徑
| 類名 | DepthFirstPaths |
|---|---|
| 構造方法 | DepthFirstPaths(Graph G,int s):構造深度優先搜索對象,使用深度優先搜索找出G圖中起點為 s的所有路徑 |
| 構造方法 | 1.private void dfs(Graph G, int v):使用深度優先搜索找出G圖中v頂點的所有相鄰頂點 2.public boolean hasPathTo(int v):判斷v頂點與s頂點是否存在路徑 3.public Stack pathTo(int v):找出從起點s到頂點v的路徑(就是該路徑經過的頂點) |
| 成員變量 | 1.private boolean[] marked: 索引代表頂點,值表示當前頂點是否已經被搜索 2.private int s:起點 3.private int[] edgeTo:索引代表頂點,值代表從起點s到當前頂點路徑上的最后一個頂點 |
思路:
我們實現路徑查找,最基本的操作還是得遍歷並搜索圖,所以,我們的實現暫且基於深度優先搜索來完成。其搜索的過程是比較簡單的。我們添加了edgeTo[]整型數組,這個整型數組會記錄從每個頂點回到起點s的路徑。 如果我們把頂點設定為0,那么它的搜索可以表示為下圖:



代碼實現如下:
public class DepthFirstPaths {
// 索引代表頂點,值表示當前頂點是否已經被搜索
private boolean[] marked;
//起點
private int s;
//索引代表頂點,值代表從起點s到當前頂點路徑上的最后一個頂點
private Integer[] edgeTo;
public DepthFirstPaths(Graph G,int s){
this.marked = new boolean[G.V()];
this.s = s;
this.edgeTo = new Integer[G.V()];
dfs(G, s);
}
private void dfs(Graph G, int v){
marked[v] = true;
for (Integer w : G.adj(v)) {
if(!marked[w]) {
//到達頂點w的路徑上的最后一個頂點是v
edgeTo[w] = v;
dfs(G, w);
}
}
}
//判斷v頂點與s頂點是否存在路徑
public boolean hasPathTo(int v){
return marked[v];
}
//找出從起點s到頂點v的路徑(就是該路徑經過的頂點)
public Stack<Integer> pathTo(int v){
if (!hasPathTo(v))
return null;
Stack<Integer> path = new Stack<>();
path.push(v);
while (edgeTo[v] != null) {
int p = edgeTo[v];
path.push(p);
v = p;
}
return path;
}
}
測試:
private Graph graph = new Graph(6);
{
graph.addEdge(0,2);
graph.addEdge(2,1);
graph.addEdge(2,3);
graph.addEdge(0,1);
graph.addEdge(0,5);
graph.addEdge(3,5);
graph.addEdge(3,4);
graph.addEdge(2,4);
}
@Test
public void test() {
DepthFirstPaths paths = new DepthFirstPaths(graph, 0);
Stack<Integer> path = paths.pathTo(4);
for (Integer integer : path) {
System.out.println(integer);
}
}

廣度優先遍歷查找路徑
深度優先搜索得到的路徑不僅取決於圖的結構,還取決於圖的表示和遞歸調用的性質。
對於找出最短路徑的那條,我們可以使用廣度優先遍歷(BFS)。
結合上面的深度優先遍歷查找路徑和前面廣度優先搜索代碼,不難得出廣度優先遍歷查找路徑的代碼:
/**
* @author wen.jie
* @date 2021/8/27 14:52
*/
public class BreadthFirstPaths {
//索引代表頂點,值代表當前頂點是否已經被搜索
private boolean[] marked;
//用來存儲待搜索鄰接表的點
private Queue<Integer> waitSearch;
//索引代表頂點,值代表從起點s到當前頂點路徑上的最后一個頂點
private Integer[] edgeTo;
public BreadthFirstPaths(Graph G, int s) {
this.marked = new boolean[G.V()];
this.waitSearch = new Queue<>();
this.edgeTo = new Integer[G.V()];
bfs(G, s);
}
//深度優先搜索找出G圖中v頂點的所有相通頂點
private void bfs(Graph G, int v) {
//標記起點
marked[v] = true;
//入隊列,待搜索
waitSearch.enqueue(v);
while (!waitSearch.isEmpty()) {
//出隊列
Integer wait = waitSearch.dequeue();
for (Integer w : G.adj(wait)) {
if (!marked[w]) { //對於每個未被標記的相鄰頂點
edgeTo[w] = wait; //保存最短路徑的最后一條邊
marked[w] = true; //標記,因為最短路徑已知
waitSearch.enqueue(w); //入隊列
}
}
}
}
//判斷w頂點與s頂點是否相通
public boolean hasPathTo(int w){
return marked[w];
}
//找出從起點s到頂點v的路徑(就是該路徑經過的頂點)
public Stack<Integer> pathTo(int v){
if (!hasPathTo(v))
return null;
Stack<Integer> path = new Stack<>();
path.push(v);
while (edgeTo[v] != null) {
int p = edgeTo[v];
path.push(p);
v = p;
}
return path;
}
}
測試:
BreadthFirstPaths paths = new BreadthFirstPaths(graph, 1);
Stack<Integer> path = paths.pathTo(3);
for (Integer integer : path) {
System.out.println(integer);
}
下面是廣度優先遍歷的處理樣圖:



命題:對於從s可達的任意頂點v,廣度優先搜索都能找到一條從s到v的最短路徑(沒有其他從s到v的路徑所含的邊比這條路徑更少)
證明:由歸納易得隊列總是包含零個或多個到起點的距離為k的頂點,之后是零個或多個到起點的舉例為k+1的頂點,其中k為整數,起始值為0。這意味着頂點是按照它們和s的距離的順序加入隊列或者離開隊列的。從頂點v加入隊列到它離開隊列之前,不可能找出到v的更短的路徑,而在v離開隊列之后發現的所有能夠到達v的路徑不可能短於v在樹中的路徑長度。
本篇所有代碼均已上傳至:https://gitee.com/wj204811/algorithm
