【數據結構】——圖的四種存儲方式(補)

以下包含一種順序存儲(鄰接矩陣)和三種鏈式存儲(鄰接表、十字鏈表、鄰接多重表)。

一、鄰接矩陣

鄰接矩陣是利用矩陣不同位置來表示任意兩個頂點間的關係。對於含有nn個頂點(v1,v2,...vnv_1,v_2,...v_n)的圖或網,需要一個nnn*n的矩陣用來存儲。(爲方便給這樣的矩陣命名爲A)

對於圖,若存儲的是無向圖,如果兩個頂點vivjv_i,v_j之間存在邊(vivjv_i,v_j),則令A[i][j]=1A[i][j]=1,否則爲0。因此無向圖的鄰接矩陣一定沿左右對角線對稱。若存儲的是有向圖,如果兩個頂點vivjv_i,v_j之間存在弧<vivjv_i,v_j>,則令A[i][j]=1A[i][j]=1,否則爲0。

對於網,也就是帶權圖,若存儲的是無向網,如果兩個頂點vivjv_i,v_j之間存在邊(vivjv_i,v_j),則令A[i][j]A[i][j]等於這條邊或弧上的權值,否則用++\infty表示。若存儲的是有向網,如果兩個頂點vivjv_i,v_j之間存在弧<vivjv_i,v_j>,則令A[i][j]A[i][j]等於這條邊或弧上的權值,否則用++\infty表示。

注意弧帶有方向性。
兩個頂點間存在邊或弧指的是這兩個頂點被直接聯繫而不是間接。

用鄰接矩陣創建無向圖

算法步驟:

  1. 輸入總頂點數和總邊數。
  2. 依次輸入所有頂點的信息存入頂點數組中。
  3. 初始化矩陣,所有位置權值初始化爲0。
  4. 構造鄰接矩陣,依次輸入每條邊依附的兩個頂點,並根據兩個頂點在頂點數組中的位置將對應的矩陣位置處值修改爲1。

例如對於下面這個圖,分別輸入頂點與邊總數5,6,所有頂點A,B,C,D,E,再依次輸入每條邊依附的兩個頂點(A,B),(A,D),(B,C),(B,E),(C,D),(C,E),就會得到右面的鄰接矩陣。
在這裏插入圖片描述

import java.util.Scanner;

public class MatrixUDG {

	private char[] vertex; 			// 頂點數組
	private int[][] matrix; 		// 二維矩陣
	private Scanner sc;

	public MatrixUDG() {
		sc = new Scanner(System.in);

		System.out.println("分別輸入頂點總數與邊數,空格隔開:");
		int VerNums = sc.nextInt(); 		// 獲取頂點總數
		int edgeNums = sc.nextInt(); 		// 獲取邊總數
		matrix = new int[VerNums][VerNums];
		vertex = new char[VerNums];

		// 所有頂點存入數組
		System.out.println("輸入所有頂點,頂點間空格隔開:");
		vertexInArray();

		// 創建矩陣,因爲二維數組屬於成員變量帶有默認值,所以這裏省略了初始化0
		System.out.println("依次輸入每條邊依附的兩個頂點,每輸入一對回車,頂點間空格隔開:");
		creatMatrix(edgeNums);

		// 打印鄰接矩陣
		System.out.println("該圖的鄰接矩陣:");
		printMatrix();
	}

	private void vertexInArray() {
		// 先讀取一次取走緩存中的回車符,這裏與算法無關
		sc.nextLine();
		String str = sc.nextLine().replaceAll(" ", "");
		// 輸入時每個頂點只能以一個字符表示
		for (int i = 0; i < vertex.length; i++) {
			vertex[i] = str.charAt(i);
		}
	}

	private void creatMatrix(int edgeNums) {
		String str;
		int m, n;
		for (int i = 0; i < edgeNums; i++) {
			str = sc.nextLine().replaceAll(" ", "");
			m = locate(str.charAt(0));
			n = locate(str.charAt(1));
			matrix[m][n] = matrix[n][m] = 1;
		}
	}

	// 返回頂點在頂點數組中的位置
	private int locate(char c) {
		int i = 0;
		for (; i < vertex.length; i++) {
			if (c == vertex[i])
				break;
		}
		return i;
	}

	private void printMatrix() {
		for (int i = 0; i < matrix.length; i++) {
			for (int j = 0; j < matrix.length; j++) {
				System.out.print(matrix[i][j] + "\t");
			}
			System.out.println();
		}
	}

	public static void main(String[] args) {
		MatrixUDG m = new MatrixUDG();
		m.sc.close();
	}
}

時間複雜度跟根據具體的算法實現來說,上面的執行時間主要用在了定位頂點在頂點數組中的位置,所以跟總邊數及具體的查找算法相關。如果存儲的是網那麼二維數組的初始化也要考慮進去。

鄰接矩陣表示法的優缺點

優點:

  • 方便判斷兩頂點間是否有邊,直接根據A[i][j]的值判斷。
  • 方便計算圖中頂點的度,對於頂點 i,無向圖中第 i 行或第 i 列的元素之和就是 i 的度。有向圖中第 i 行元素之和爲 i 的出度,第 i 列元素之和爲 i 的入度。網因爲存儲的是權值所以沒有這種關係。

缺點:

  • 不方便增加或刪除結點,因爲要重新修改二維數組。
  • 不方便統計邊的數目,必須要遍歷整個二維數組才能確定。
  • 空間複雜度較高,存儲n個頂點的圖的空間複雜度O(n2)Ο(n^2)

二、鄰接表

鄰接表的存儲結構概括來講就是“數組+鏈表”形式,對應分別存儲圖的頂點與邊。數組中存放圖的所有頂點,各頂點又作爲表頭結點,後面以單鏈表的形式鏈接上它的所有鄰接點,以頭結點和鄰接點兩個頂點表示一條邊。

這個單鏈表也叫做邊表,爲什麼說它存的是邊可以這樣看,下面的A頂點與之相連的爲(0,3)(0,1)兩條邊,B頂點與之相連的爲(1,4)(1,2)(1,0)三條邊。

比如下面左邊這個無向圖對應的鄰接表,後面的數字表示每個鄰接點在數組中的位置:
在這裏插入圖片描述

用鄰接表創建無向圖

算法步驟:

  1. 輸入頂點總數和邊總數。
  2. 依次輸入所有頂點的信息存入頂點數組中作爲單鏈表的表頭結點。如果指針域沒有默認值還需要初始化爲null。
  3. 創建鄰接表,依次輸入每條邊依附的兩個頂點,根據這兩個頂點在頂點數組中的位置插入到對應頭結點的單鏈表中。例如頂點 i 與頂點 j,i 插在頭結點 j 的單鏈表中,j 插在頭結點 i 的單鏈表中。(從這可以看出兩個頂點 i, j 間如果有邊,則 i 的鏈表中有 j,j 的鏈表中有 i )

下面代碼中分別輸入頂點與邊總數5,6,所有頂點A,B,C,D,E,再依次輸入每條邊依附的兩個頂點(A,B),(A,D),(B,C),(B,E),(C,D),(C,E),就會得到上圖中右面的鄰接表。

import java.util.Scanner;

//表頭結點
class VertexNode {
	char data;
	EdgeNode next;

	public VertexNode(char verName) {
		data = verName;
	}
}

//邊結點
class EdgeNode {
	int index;
	EdgeNode next;

	public EdgeNode(int num) {
		index = num;
	}
}

public class AdjacencyListUDG {

	VertexNode[] array; 			// 存表頭結點的數組
	Scanner sc;

	public AdjacencyListUDG() {
		sc = new Scanner(System.in);
		System.out.println("輸入頂點總數和邊數,空格隔開:");
		int verNums = sc.nextInt();
		int edgeNums = sc.nextInt();
		array = new VertexNode[verNums];

		System.out.println("輸入所有頂點,頂點間空格隔開:");
		vertexInArray();

		System.out.println("依次輸入每條邊依附的兩個頂點,每輸入一對回車,頂點間空格隔開:");
		creatList(edgeNums);

		System.out.println("鄰接表爲:");
		printList();
	}

	private void vertexInArray() {
		sc.nextLine();
		String str = sc.nextLine().replaceAll(" ", "");
		for (int i = 0; i < array.length; i++) {
			array[i] = new VertexNode(str.charAt(i));
		}
	}

	private void creatList(int edgeNums) {
		String str;
		int front, behind;
		EdgeNode node;
		for (int i = 0; i < edgeNums; i++) {
			str = sc.nextLine().replaceAll(" ", "");
			front = locate(str.charAt(0));
			behind = locate(str.charAt(1));

			// 這裏採用的是頭插法
			node = new EdgeNode(front);
			node.next = array[behind].next;
			array[behind].next = node;

			node = new EdgeNode(behind);
			node.next = array[front].next;
			array[front].next = node;
		}
	}

	private void printList() {
		EdgeNode node;
		for (int i = 0; i < array.length; i++) {
			System.out.print(i + ":" + array[i].data);
			node = array[i].next;
			while (node != null) {
				System.out.print("-->" + node.index);
				node = node.next;
			}
			System.out.println();
		}
	}

	private int locate(char c) {
		int i = 0;
		for (; i < array.length; i++) {
			if (c == array[i].data)
				break;
		}
		return i;
	}

	public static void main(String[] args) {
		AdjacencyListUDG lt = new AdjacencyListUDG();
		lt.sc.close();
	}
}

時間複雜度O(n+e)Ο(n+e),n爲頂點總數,e爲邊總數。分別對應初始化數組中的所有頭結點和將邊結點插入到單鏈表的操作。

鄰接表表示法的優缺點

優點:

  • 便於增刪頂點,所有的鏈表結構都方便增刪操作,只需要修改指針。
  • 方便統計邊的數目。邊的總數 = 所有單鏈表中邊結點總數 x 12\frac 12,因爲每條邊會插入兩個邊結點。
  • 空間複雜度較低,無向圖存儲需要n個頭結點+2e個邊結點的空間,有向圖存儲需要n個頭結點+e個邊結點的空間,空間複雜度都爲O(n+e)Ο(n+e)

空間複雜度較低不是指一定比鄰接矩陣存儲低,雖然相比鄰接矩陣,鄰接表只爲存在邊的鄰接點建立存儲空間,但因爲結點結構中含有鏈域,如果是網還需要增加存儲權值的空間,所以鄰接表只適用於存儲稀疏圖。具體稀疏到什麼地步,可以根據用矩陣存儲和用鄰接表存儲各自佔據的空間之間的大小來判斷。

缺點:

  • 不方便判斷兩頂點之間是否有邊,這需要掃描兩頂點中任一個頂點作爲頭結點的單鏈表,最壞情況下時間複雜度O(n)Ο(n)
  • 不方便統計有向圖中頂點的度。對於無向圖頂點 i 的度等於以頂點 i 爲頭結點的單鏈表中的邊結點數,可以遍歷這個單鏈表得到。而對於有向圖,一條單鏈表中的邊結點數只等於頭結點頂點的出度,求入度需要遍歷所有頭結點的單鏈表。

三、十字鏈表

十字鏈表主要是爲了解決鄰接表中不方便計算有向圖入度的問題, 與鄰接表相比頭結點和邊結點都多增加了一個指向入度弧的指針域,在原來鄰接表的基礎上將每個頭結點頂點的所有入度弧以單鏈表形式和這個頭結點鏈接起來。所以十字鏈表可以看成是鄰接表與逆鄰接表的結合,用來存儲有向圖。

其中頭結點由數據域、入度弧指針域、出度弧指針域三部分組成,邊結點由弧尾位置域、弧頭位置域、入度弧指針域、出度弧指針域四部分組成,如果存儲的是網還需要增加存儲權值的空間。

例如對於下面左邊的這個有向圖,對應的十字鏈表爲(藍線指向下一個出度弧,紅線指向下一個入度弧):
在這裏插入圖片描述
用十字鏈表創建有向圖

算法步驟與鄰接表類似,唯一區別是在除了將新創建的弧鏈接在以它弧尾頂點爲頭結點的單鏈表中作爲這個頂點的出度弧之外,還需要將這條弧鏈接在以它弧頭頂點爲頭結點的單鏈表中作爲該頂點的入度弧。

下面代碼中分別輸入頂點與弧總數4,7,所有頂點A,B,C,D,再依次輸入每條弧的弧尾弧頭兩個頂點(A,B),(A,C),(D,A),(D,B),(D,C),(C,A),(C,D),就會得到上圖中右面的十字鏈表。

每個單鏈表中弧出現的先後順序可能和上面有些不同,這是因爲鏈表的插入方式導致。

import java.util.Scanner;

class VertexNode {
	char data;
	EdgeNode inNext;
	EdgeNode outNext;

	public VertexNode(char verName) {
		data = verName;
	}
}

class EdgeNode {
	int tailIndex;				// 弧尾頂點位置
	int headIndex;				// 弧頭頂點位置
	EdgeNode inNext;			// 入度弧指針域
	EdgeNode outNext;			// 出度弧指針域

	public EdgeNode(int tail, int head) {
		tailIndex = tail;
		headIndex = head;
	}
}

public class OrthogonalListDG {

	VertexNode[] array;
	Scanner sc;

	public OrthogonalListDG() {
		sc = new Scanner(System.in);
		System.out.println("輸入頂點總數和邊數,空格隔開:");
		int verNums = sc.nextInt();
		int edgeNums = sc.nextInt();
		array = new VertexNode[verNums];

		System.out.println("輸入所有頂點,頂點間空格隔開:");
		vertexInArray();

		System.out.println("依次輸入每條弧的弧尾和弧頭頂點,每輸入一對回車,頂點間空格隔開:");
		creatList(edgeNums);

		System.out.println("十字鏈表爲:");
		printList();
	}

	private void vertexInArray() {
		sc.nextLine();
		String str = sc.nextLine().replaceAll(" ", "");
		for (int i = 0; i < array.length; i++) {
			array[i] = new VertexNode(str.charAt(i));
		}
	}

	private void creatList(int edgeNums) {
		String str;
		int head, tail;
		EdgeNode node;
		for (int i = 0; i < edgeNums; i++) {
			str = sc.nextLine().replaceAll(" ", "");
			tail = locate(str.charAt(0));
			head = locate(str.charAt(1));

			node = new EdgeNode(tail, head);
			// 前插法插入出度弧
			node.outNext = array[tail].outNext;
			array[tail].outNext = node;

			// 前插法插入入度弧,也是相對創建鄰接表唯一不同的地方
			node.inNext = array[head].inNext;
			array[head].inNext = node;
		}
	}

	private void printList() {
		EdgeNode node;
		for (int i = 0; i < array.length; i++) {
			System.out.print("出度弧:" + i + ":" + array[i].data);
			node = array[i].outNext;
			while (node != null) {
				System.out.print("-->(" + node.tailIndex + "," + node.headIndex + ")");
				node = node.outNext;
			}
			System.out.println();

			System.out.print("入度弧:" + i + ":" + array[i].data);
			node = array[i].inNext;
			while (node != null) {
				System.out.print("-->(" + node.tailIndex + "," + node.headIndex + ")");
				node = node.inNext;
			}
			System.out.println();
		}
	}

	private int locate(char c) {
		int i = 0;
		for (; i < array.length; i++) {
			if (c == array[i].data)
				break;
		}
		return i;
	}

	public static void main(String[] args) {
		OrthogonalListDG lt = new OrthogonalListDG();
		lt.sc.close();
	}
}

時間複雜度和創建鄰接表相同,爲O(n+e)Ο(n+e)。n爲頂點總數,e爲弧總數。

四、鄰接多重表

鄰接多重表主要是爲了解決鄰接表存儲無向圖時邊重複存儲的問題。解決方法是每讀取一行輸入獲取到兩個邊頂點時不是像鄰接表那樣直接創建兩個邊結點分別插入到兩個單鏈表中,而是隻創建一條邊,讓以邊兩端頂點爲頭結點的兩個單鏈表都鏈接上這條邊。因爲不同頭結點的鄰邊不同,所以勢必要給邊結點增加一個指針域,用來構建另一個頭結點的單鏈表。

與鄰接表相比頭結點結構不變,邊結點結構爲兩個用來存儲邊兩端頂點位置的域 + 兩個分別用來保存不同頭結點的單鏈表指針的域,有時涉及到遍歷可能還需要設置一個標記域,標記是否被訪問。

下面是它的一個鄰接多重表,對應的鄰接多重表不唯一。

在這裏插入圖片描述
代碼實現中將邊插入到兩個鏈表的部分還有些衝突,暫時沒有想到好的解決辦法,搞明白了回來補上。

類似於鄰接多重表,個人覺得也可以直接分別用一個頂點數組和一個邊數組來存放圖的所有頂點和邊,只是相比於鄰接多重表,這種存儲方式沒有構建頂點與其鄰邊之間的鏈表,不方便統計頂點的鄰邊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章