1. 程式人生 > 實用技巧 >Java資料結構(十七)—— 圖

Java資料結構(十七)—— 圖

圖的基本介紹

為什麼要有圖?

  1. 線性表侷限於一個直接前去和一個直接後繼的關係

  2. 樹也只能有一個直接前去也就是父節點

  3. 當我們表示 多對多的關係是=時,就用到了圖

圖的舉例說明

  • 圖是一種資料結構,其中節點可以具有零個或多個相鄰元素。

  • 兩節點之間的連線稱為邊,節點稱為頂點

如圖:

圖的常用概念

  • 頂點:節點

  • 邊:頂點間的連線

  • 路徑:節點A到節點B的通路

  • 無向圖:頂點之間的連線沒有方向

  • 有向圖:頂點之間的連線有方向

  • 帶權圖:邊帶有權值的圖

圖的表示方式

  1. 鄰接矩陣(二維陣列表示)

    • 鄰接矩陣:是表示圖形中頂點之間相鄰關係的矩陣,對於 n個頂點的圖而言,矩陣是row和col表示的是1……n個點

    • 0表示不直接連通,1表示直接連通

  2. 鄰接表(連結串列表示)

    • 鄰接矩陣需要為每隔頂點都分配n個邊的空間,其實有很多邊都是不存在的,會造成空間損失

    • 鄰接表只關心存在的邊

    • 鄰接表由陣列 + 連結串列組成

    • 二維陣列表示各個節點標號,連結串列儲存與之連通的節點標號

圖的建立

要求

  1. 程式碼實現如下圖

思路分析

  1. 儲存頂點 String 使用 ArrayList

  2. 儲存鄰接矩陣矩陣,使用二維陣列

程式碼實現

package com.why.graph;

import java.util.ArrayList;
import java.util.Arrays;

/**
* @Description TODO 圖
* @Author why
* @Date 2020/12/7 17:42
* Version 1.0
**/
public class Graph {

private ArrayList<String> vertexList;//儲存頂點

private int[][] edges;//儲存圖的鄰接矩陣

private int numOfEdges;//邊的個數

public static void main(String[] args) {

//測試建立圖
int n = 5;//節點個數
String[] Vertexs = {"A","B","C","D","E"};
//建立圖
Graph graph = new Graph(n);
//迴圈新增節點
for (String value : Vertexs
) {
graph.insertVertex(value);
}
//新增邊
//A-B,A-C,B-C,B-D,B-E互相連線
graph.insertEdge(0,1,1);
graph.insertEdge(0,2,1);
graph.insertEdge(1,2,1);
graph.insertEdge(1,3,1);
graph.insertEdge(1,4,1);
//列印
graph.showGraph();
}

/**
* 構造器
* @param n 頂點個數
*/
public Graph(int n) {
//初始化矩陣和頂點列表
edges = new int[n][n];
vertexList = new ArrayList<>(n);
numOfEdges = 0;
}

/**
* 插入節點
* @param vertex
*/
public void insertVertex(String vertex){
vertexList.add(vertex);
}

/**
* 新增邊
* @param v1 節點下標
* @param v2 節點下標
* @param weight 權值
*/
public void insertEdge(int v1,int v2,int weight){
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}

/**
* 返回節點數目
* @return
*/
public int getNumOfVertex(){
return vertexList.size();
}

/**
* 返回邊的數目
* @return
*/
public int getNumOfEdges(){
return numOfEdges;
}

/**
* 返回下標為i的節點
* @param i
* @return
*/
public String getValueByIndex(int i){
return vertexList.get(i);
}

/**
* 返回v1和v2之間的權值
* @param v1
* @param v2
* @return
*/
public int getWeight(int v1,int v2){
return edges[v1][v2];
}

/**
* 顯示圖的矩陣
*/
public void showGraph(){
//遍歷二維陣列
for (int[] link : edges
) {
System.err.println(Arrays.toString(link));
}
}
}

圖的遍歷

圖的遍歷,即是對節點的訪問

深度優先(Depth First Search , DFS)遍歷

基本思想

  1. 深度優先遍歷,從初始訪問節點出發,初始訪問節點可能有多個鄰接節點,深度優先遍歷的策略是首先訪問第一個鄰接節點,然後在一這個被訪問的鄰接節點作為初始節點,訪問他的第一個鄰接節點。可以這樣理解,每次都在訪問完當前節點後首先訪問當前節點的第一個鄰接節點

  2. 這樣的策略是優先往縱向挖掘深入,而不是對一個節點的所有鄰接節點進行橫向訪問

  3. 深度優先搜尋是一個遞迴的過程

演算法步驟

  1. 訪問初始節點v,並標記節點v為已訪問

  2. 查詢節點v的第一個鄰接點w

  3. 若w存在,則繼續執行4,若不存在,則回到第1步,將從v的下一個節點繼續

  4. 若w未被訪問,對w進行深度優先遍歷遞迴

  5. 查詢節點v的w鄰接節點的下一個鄰接節點

具體案例

程式碼實現

  1. 建立標誌陣列,標誌節點是否被訪問

    private boolean[] isVisited ;//標誌節點是否被訪問
  2. dfs,深度優先遍歷及其方法

    /**
    * 得到第一個鄰接節點的下標
    * @return
    */
    public int getFirstNeighbor(int index){
    for (int i = 0; i < vertexList.size(); i++) {
    if (edges[index][i] > 0){//存在鄰接節點返回 下標
    return i;
    }
    }
    return -1;
    }

    /**
    * 根據上一個鄰接節點的下標獲取下一個鄰接節點
    * @param v1 當前節點
    * @param v2 當前節點已被訪問的鄰接節點
    * @return
    */
    public int getNextNeighbor(int v1,int v2){
    for (int i = v2 + 1; i < vertexList.size(); i++) {
    if (edges[v1][i] > 0){//返回下一個鄰接節點的下標
    return i;
    }
    }
    return -1;
    }

    /**
    * 深度優先遍歷
    * @param isVisited 標誌陣列
    * @param i 第一次就是0
    */
    private void dfs(boolean[] isVisited,int i){
    //首先訪問該節點
    System.out.print(getValueByIndex(i) + "->");
    //將該節點設定為已訪問
    isVisited[i] = true;

    //以i下標的節點為當前節點進行深度遍歷
    //尋找第一個鄰接節點
    int w = getFirstNeighbor(i);
    while (w != -1){//找到鄰接節點
    if (!isVisited[w]){//沒有被訪問
    dfs(isVisited,w);
    }
    //如果w節點已經被訪問,查詢鄰接節點的下一個節點
    w = getNextNeighbor(i,w);
    }
    }

    /**
    * 過載dfs,遍歷所有的節點,對節點進行dfs深度優先遍歷
    */
    public void dfs(){
    //遍歷所有節點
    for (int i = 0; i < getNumOfVertex(); i++) {
    if (!isVisited[i]){//沒有被訪問過,進行深度遍歷
    dfs(isVisited,i);
    }
    }
    }
  3. 測試

    //測試深度優先遍歷
    System.out.println("深度優先遍歷:");
    graph.dfs();

廣度優先(Broad First Search,BFS)遍歷

基本介紹

  1. 類似於分層搜尋的過程

  2. 使用一個佇列以保持訪問過的節點的順序,以便按這個順序來訪問這些節點的鄰接節點

演算法步驟

  1. 訪問初始結點 v 並標記結點 v 為已訪問。

  2. 結點 v 入佇列

  3. 當佇列非空時,繼續執行,否則演算法結束。

  4. 出佇列,取得隊頭結點 u。

  5. 查詢結點 u 的第一個鄰接結點 w。

  6. 若結點 u 的鄰接結點 w 不存在,則轉到步驟 3;否則迴圈執行以下三個步驟:

    6.1 若結點 w 尚未被訪問,則訪問結點 w 並標記為已訪問。

    6.2 結點 w 入佇列

    6.3 查詢結點 u 的繼 w 鄰接結點後的下一個鄰接結點 w,轉到步驟 6。

程式碼實現

/**
* 對一個節點進行廣度優先遍歷
* @param isVisited 標誌陣列
* @param i 節點
*/
private void bfs(boolean[] isVisited,int i){
int u;//表示佇列的頭節點對應的下標
int w;//鄰接節點w
//佇列,記錄節點訪問的順序
LinkedList queue = new LinkedList();
//訪問當前節點,輸出資訊
System.out.print(getValueByIndex(i) + "->");
//標記為已訪問
isVisited[i] = true;
//將節點加入佇列
queue.addLast(i);
while (!queue.isEmpty()) {//佇列非空
//取出佇列的頭節點下標
u = (Integer) queue.removeFirst();
//得到第一個鄰接點下標
w = getFirstNeighbor(u);
while (w != -1) {//w存在
//是否訪問過
if (!isVisited[w]) {//沒有訪問過
System.out.print(getValueByIndex(w) + "->");
//標記已經訪問
isVisited[w] = true;
//入佇列
queue.addLast(w);
}
//以u為前驅點,找w後面的下一個鄰接點
w = getNextNeighbor(u,w);//體現出廣度優先
}
}
}

/**
* 過載,遍歷所有的節點,對其都進行廣度優先搜尋
*/
public void bfs() {
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isVisited[i]) {//沒有被訪問過
bfs(isVisited, i);
}
}
}

深度優先 VS 廣度優先 

圖的程式碼彙總

package com.why.graph;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.jar.JarEntry;

/**
* @Description TODO 圖
* @Author why
* @Date 2020/12/7 17:42
* Version 1.0
**/
public class Graph {

private ArrayList<String> vertexList;//儲存頂點

private int[][] edges;//儲存圖的鄰接矩陣

private int numOfEdges;//邊的個數

private boolean[] isVisited ;//標誌節點是否被訪問

public static void main(String[] args) {

//測試建立圖
int n = 5;//節點個數
String[] Vertexs = {"A","B","C","D","E"};
//建立圖
Graph graph = new Graph(n);
//迴圈新增節點
for (String value : Vertexs
) {
graph.insertVertex(value);
}
//新增邊
//A-B,A-C,B-C,B-D,B-E互相連線
graph.insertEdge(0,1,1);
graph.insertEdge(0,2,1);
graph.insertEdge(1,2,1);
graph.insertEdge(1,3,1);
graph.insertEdge(1,4,1);
//列印
graph.showGraph();

// //測試深度優先遍歷
// System.out.println("深度優先遍歷:");
// graph.dfs();
// System.out.println();

//測試廣度優先遍歷
System.out.println("廣度優先遍歷:");
graph.bfs();
}

/**
* 構造器
* @param n 頂點個數
*/
public Graph(int n) {
//初始化矩陣和頂點列表
edges = new int[n][n];
vertexList = new ArrayList<>(n);
numOfEdges = 0;
isVisited = new boolean[n];
}

/**
* 插入節點
* @param vertex
*/
public void insertVertex(String vertex){
vertexList.add(vertex);
}

/**
* 新增邊
* @param v1 節點下標
* @param v2 節點下標
* @param weight 權值
*/
public void insertEdge(int v1,int v2,int weight){
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}

/**
* 返回節點數目
* @return
*/
public int getNumOfVertex(){
return vertexList.size();
}

/**
* 返回邊的數目
* @return
*/
public int getNumOfEdges(){
return numOfEdges;
}

/**
* 返回下標為i的節點
* @param i
* @return
*/
public String getValueByIndex(int i){
return vertexList.get(i);
}

/**
* 返回v1和v2之間的權值
* @param v1
* @param v2
* @return
*/
public int getWeight(int v1,int v2){
return edges[v1][v2];
}

/**
* 顯示圖的矩陣
*/
public void showGraph(){
//遍歷二維陣列
for (int[] link : edges
) {
System.err.println(Arrays.toString(link));
}
}

/**
* 得到第一個鄰接節點的下標
* @return
*/
public int getFirstNeighbor(int index){
for (int i = 0; i < vertexList.size(); i++) {
if (edges[index][i] > 0){//存在鄰接節點返回 下標
return i;
}
}
return -1;
}

/**
* 根據上一個鄰接節點的下標獲取下一個鄰接節點
* @param v1 當前節點
* @param v2 當前節點已被訪問的鄰接節點
* @return
*/
public int getNextNeighbor(int v1,int v2){
for (int i = v2 + 1; i < vertexList.size(); i++) {
if (edges[v1][i] > 0){//返回下一個鄰接節點的下標
return i;
}
}
return -1;
}

/**
* 深度優先遍歷
* @param isVisited 標誌陣列
* @param i 第一次就是0
*/
private void dfs(boolean[] isVisited,int i){
//首先訪問該節點
System.out.print(getValueByIndex(i) + "->");
//將該節點設定為已訪問
isVisited[i] = true;

//以i下標的節點為當前節點進行深度遍歷
//尋找第一個鄰接節點
int w = getFirstNeighbor(i);
while (w != -1){//找到鄰接節點
if (!isVisited[w]){//沒有被訪問
dfs(isVisited,w);
}
//如果w節點已經被訪問,查詢鄰接節點的下一個節點
w = getNextNeighbor(i,w);
}
}

/**
* 過載dfs,遍歷所有的節點,對節點進行dfs深度優先遍歷
*/
public void dfs(){
//遍歷所有節點
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isVisited[i]){//沒有被訪問過,進行深度遍歷
dfs(isVisited,i);
}
}
}

/**
* 對一個節點進行廣度優先遍歷
* @param isVisited 標誌陣列
* @param i 節點
*/
private void bfs(boolean[] isVisited,int i){
int u;//表示佇列的頭節點對應的下標
int w;//鄰接節點w
//佇列,記錄節點訪問的順序
LinkedList queue = new LinkedList();
//訪問當前節點,輸出資訊
System.out.print(getValueByIndex(i) + "->");
//標記為已訪問
isVisited[i] = true;
//將節點加入佇列
queue.addLast(i);
while (!queue.isEmpty()) {//佇列非空
//取出佇列的頭節點下標
u = (Integer) queue.removeFirst();
//得到第一個鄰接點下標
w = getFirstNeighbor(u);
while (w != -1) {//w存在
//是否訪問過
if (!isVisited[w]) {//沒有訪問過
System.out.print(getValueByIndex(w) + "->");
//標記已經訪問
isVisited[w] = true;
//入佇列
queue.addLast(w);
}
//以u為前驅點,找w後面的下一個鄰接點
w = getNextNeighbor(u,w);//體現出廣度優先
}
}
}

/**
* 過載,遍歷所有的節點,對其都進行廣度優先搜尋
*/
public void bfs() {
for (int i = 0; i < getNumOfVertex(); i++) {
if (!isVisited[i]) {//沒有被訪問過
bfs(isVisited, i);
}
}
}
}

所有原始碼都可在gitee倉庫中下載:https://gitee.com/vvwhyyy/java_algorithm