圖是一種比線性表和樹更復雜的數據結構,在圖中,結點之間的關系是任意的,任意兩個數據元素之間都可能相關。圖是一種多對多的數據結構。
1、基本概念
圖(Graph)是由頂點的有窮非空集合和頂點之間邊的集合組成,通常表示為:G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。
注意:線性表中可以沒有元素,稱為空表。樹中可以沒有結點,叫做空樹。但是在圖中不允許沒有頂點,可以沒有邊。
基本術語:
-
無向邊:若頂點Vi和Vj之間的邊沒有方向,稱這條邊為無向邊(Edge),用
(Vi,Vj)
來表示。 -
無向圖(Undirected graphs):圖中任意兩個頂點的邊都是無向邊。
-
有向邊:若從頂點Vi到Vj的邊有方向,稱這條邊為有向邊,也稱為弧(Arc),用
<Vi, Vj>
來表示,其中Vi稱為弧尾(Tail),Vj稱為弧頭(Head)。 -
有向圖(Directed graphs):圖中任意兩個頂點的邊都是有向邊。
-
簡單圖:不存在自環(頂點到其自身的邊)和重邊(完全相同的邊)的圖
-
無向完全圖:無向圖中,任意兩個頂點之間都存在邊。
-
有向完全圖:有向圖中,任意兩個頂點之間都存在方向相反的兩條弧。
-
稀疏圖;有很少條邊或弧的圖稱為稀疏圖,反之稱為稠密圖。
-
權(Weight):表示從圖中一個頂點到另一個頂點的距離或耗費。
-
網:帶有權重的圖
-
度:與特定頂點相連接的邊數;
-
出度、入度:有向圖中的概念,出度表示以此頂點為起點的邊的數目,入度表示以此頂點為終點的邊的數目;
-
環:第一個頂點和最后一個頂點相同的路徑;
-
簡單環:除去第一個頂點和最后一個頂點后沒有重復頂點的環;
-
連通圖:任意兩個頂點都相互連通的圖;
-
極大連通子圖:包含竟可能多的頂點(必須是連通的),即找不到另外一個頂點,使得此頂點能夠連接到此極大連通子圖的任意一個頂點;
-
連通分量:極大連通子圖的數量;
-
強連通圖:此為有向圖的概念,表示任意兩個頂點a,b,使得a能夠連接到b,b也能連接到a 的圖;
-
生成樹:n個頂點,n-1條邊,並且保證n個頂點相互連通(不存在環);
-
最小生成樹:此生成樹的邊的權重之和是所有生成樹中最小的;
-
AOV網(
Activity On Vertex Network ):在有向圖中若以頂點表示活動,有向邊表示活動之間的先后關系 -
AOE網(
Activity On Edge Network):在帶權有向圖中若以頂點表示事件,有向邊表示活動,邊上的權值表示該活動持續的時間
2、圖的存儲結構
由於圖的結構比較復雜,任意兩個頂點之間都可能存在關系,因此用簡單的順序存儲來表示圖是不可能,而若使用多重鏈表的方式(即一個數據域多個指針域的結點來表示),這將會出現嚴重的空間浪費或操作不便。這里總結一下常用的表示圖的方法:
2.1、鄰接矩陣
圖的鄰接矩陣(Adjacency Matrix)存儲方式是用兩個數組來表示圖。一個一維數組存儲圖中頂點信息,一個二維數組(稱鄰接矩陣)存儲圖中的邊或弧的信息。
無向圖由於邊不區分方向,所以其鄰接矩陣是一個對稱矩陣。鄰接矩陣中的0表示邊不存在,主對角線全為0表示圖中不存在自環。
帶權有向圖的鄰接矩陣:
在帶權有向圖的鄰接矩陣中,數字表示權值weight,「無窮」表示弧不存在。由於權值可能為0,所以不能像在無向圖的鄰接矩陣中那樣使用0來表示弧不存在。
代碼:
/**
* 有向圖的鄰接矩陣實現
*/
public class Digraph {
private int vertexsNum;
private int edgesNum;
private int[][] arc;
public Digraph(int[][] data, int vertexsNum) {
this.vertexsNum = vertexsNum;
this.edgesNum = data.length;
arc = new int[vertexsNum][vertexsNum];
for (int i = 0; i < vertexsNum; i++) {
for (int j = 0; j < vertexsNum; j++) {
arc[i][j] = Integer.MAX_VALUE;
}
}
for (int i = 0; i < data.length; i++) {
int tail = data[i][0];
int head = data[i][1];
arc[tail][head] = 1;
}
}
//用於測試,返回一個頂點的鄰接點
public Iterable<Integer> adj(int vertex) {
Set<Integer> set = new HashSet<>();
for (int i = 0; i < vertexsNum; i++) {
if (arc[vertex][i] != Integer.MAX_VALUE)
set.add(i);
}
return set;
}
public static void main(String[] args) {
int[][] data = {
{0,3},
{1,0},
{1,2},
{2,0},
{2,1},
};
Digraph wd = new Digraph(data,4);
for(int i :wd.adj(1)) {
System.out.println(i);
}
}
}
優缺點:
- 優點:結構簡單,操作方便
- 缺點:對於稀疏圖,這種實現方式將浪費大量的空間。
2.2、鄰接表
鄰接表是一種將數組與鏈表相結合的存儲方法。其具體實現為:將圖中頂點用一個一維數組存儲,每個頂點Vi的所有鄰接點用一個單鏈表來存儲。這種方式和樹結構中孩子表示法一樣。
對於有向圖其鄰接表結構如下:
有向圖的鄰接表是以頂點為弧尾來存儲邊表的,這樣很容易求一個頂點的出度(頂點對應單鏈表的長度),但若求一個頂點的入度,則需遍歷整個圖才行。這時可以建立一個有向圖的逆鄰接表即對每個頂點v都建立一個弧頭尾v的單鏈表。如上圖所示。
代碼:
/**
* 有向圖的鄰接表實現
*
*/
public class AdjListDigraph {
private class EdgeNode {
int index;
EdgeNode next;
EdgeNode(int index, EdgeNode next){
this.index = index;
this.next = next;
}
}
private class VertexNode {
int id;
EdgeNode headNode;
}
private VertexNode[] vertexs;
private int vertexsNum;
private int edgesNum;
public AdjListDigraph(int[][] data, int vertexsNum) {
this.vertexsNum = vertexsNum;
this.edgesNum = data.length;
vertexs = new VertexNode[vertexsNum];
for (int i = 0; i < vertexs.length; i++) {
vertexs[i] = new VertexNode();
vertexs[i].id = i; //
}
for (int i = 0; i < data.length; i++) {
int index = data[i][1];
EdgeNode next = vertexs[data[i][0]].headNode;
EdgeNode eNode = new EdgeNode(index,next);
vertexs[data[i][0]].headNode = eNode; //頭插法
}
}
//用於測試,返回一個頂點的鄰接點
public Iterable<Integer> adj(int index) {
Set<Integer> set = new HashSet<>();
EdgeNode current = vertexs[index].headNode;
while(current != null) {
VertexNode node = vertexs[current.index];
set.add(node.id);
current = current.next;
}
return set;
}
public static void main(String[] args) {
int[][] data = {
{0,3},
{1,0},
{1,2},
{2,0},
{2,1},
};
AdjListDigraph ald = new AdjListDigraph(data,4);
for(int i :ald.adj(1)) {
System.out.println(i);
}
}
}
本算法的時間復雜度為 O(N + E),其中N、E分別為頂點數和邊數,鄰接表實現比較適合表示稀疏圖。
2.3、十字鏈表
十字鏈表(Orthogonal List)是將鄰接表和逆鄰接表相結合的存儲方法,它解決了鄰接表(或逆鄰接表)的缺陷,即求入度(或出度)時必須遍歷整個圖。
十字鏈表的結構如下:
圖中:
- firstIn表示入邊表(即是逆鄰接表中的單鏈表)頭指針,firstOut表示出邊表(即是鄰接表中的單鏈表)頭指針,data表示頂點數據。
- tailVex表示邊的起點在頂點數組中的下標,tailNext值出邊表指針域,指向起點相同的下一條邊。
- headVex表示邊的終點在頂點數組中的下標,headNext指入邊表指針域,指向終點相同的下一條邊。
代碼實現:
/**
* 有向圖的十字鏈表實現
*
*/
public class OrthogonalList {
private class EdgeNode {
int tailVex;
int headVex;
EdgeNode headNext;
EdgeNode tailNext;
public EdgeNode(int tailVex, int headVex, EdgeNode headNext, EdgeNode tailNext) {
super();
this.tailVex = tailVex;
this.headVex = headVex;
this.headNext = headNext;
this.tailNext = tailNext;
}
}
private class VertexNode {
int data;
EdgeNode firstIn;
EdgeNode firstOut;
}
private VertexNode[] vertexs;
private int vertexsNum;
private int edgesNum;
public OrthogonalList(int[][] data, int vertexsNum) {
this.vertexsNum = vertexsNum;
this.edgesNum = data.length;
vertexs = new VertexNode[vertexsNum];
for (int i = 0; i < vertexs.length; i++) {
vertexs[i] = new VertexNode();
vertexs[i].data = i; //
}
//關鍵
for (int i = 0; i < data.length; i++) {
int tail = data[i][0];
int head = data[i][1];
EdgeNode out = vertexs[tail].firstOut;
EdgeNode in = vertexs[head].firstIn;
EdgeNode eNode = new EdgeNode(tail,head,in,out);
vertexs[tail].firstOut = eNode;
vertexs[head].firstIn = eNode;
}
}
//返回一個頂點的出度
public int outDegree(int index) {
int result = 0;
EdgeNode current = vertexs[index].firstOut;
while(current != null) {
current = current.tailNext;
result++;
}
return result;
}
//返回一個頂點的入度
public int inDegree(int index) {
int result = 0;
EdgeNode current = vertexs[index].firstIn;
while(current != null) {
current = current.headNext;
result++;
}
return result;
}
public static void main(String[] args) {
int[][] data = {
{0,3},
{1,0},
{1,2},
{2,0},
{2,1},
};
OrthogonalList orth = new OrthogonalList(data,4);
System.out.println("頂點1的出度為" + orth.outDegree(1));
System.out.println("頂點1的入度為" + orth.inDegree(1));
}
}
十字鏈表創建圖算法的時間復雜度和鄰接表相同都為O(N + E)。在有圖的應用中推薦使用。
3、圖的遍歷
從圖的某個頂點出發,遍歷圖中其余頂點,且使每個頂點僅被訪問一次,這個過程叫做圖的遍歷(Traversing Graph)。對於圖的遍歷通常有兩種方法:深度優先遍歷和廣度優先遍歷。
3.1、深度優先遍歷
深度優先遍歷(Depth First Search,簡稱DFS),也成為深度優先搜索。
遍歷思想:基本思想:首先從圖中某個頂點v0出發,訪問此頂點,然后依次從v相鄰的頂點出發深度優先遍歷,直至圖中所有與v路徑相通的頂點都被訪問了;若此時尚有頂點未被訪問,則從中選一個頂點作為起始點,重復上述過程,直到所有的頂點都被訪問。
深度優先遍歷用遞歸實現比較簡單,只需用一個遞歸方法來遍歷所有頂點,在訪問某一個頂點時:
- 將它標為已訪問
- 遞歸的訪問它的所有未被標記過的鄰接點
深度優先遍歷的過程:
代碼如下:
public class DFSTraverse {
private boolean[] visited;
//從頂點index開始遍歷
public DFSTraverse(Digraph graph, int index) {
visited = new boolean[graph.getVertexsNum()];
dfs(graph,index);
}
private void dfs(Digraph graph, int index) {
visited[index] = true;
for(int i : graph.adj(index)) {
if(!visited[i])
dfs(graph,i);
}
}
}
3.2、廣度優先遍歷
廣度優先遍歷(Breadth First Search,簡稱BFS),又稱為廣度優先搜索
遍歷思想:首先,從圖的某個頂點v0出發,訪問了v0之后,依次訪問與v0相鄰的未被訪問的頂點,然后分別從這些頂點出發,廣度優先遍歷,直至所有的頂點都被訪問完。
廣度優先遍歷的過程:
代碼:
public class BFSTraverse {
private boolean[] visited;
public BFSTraverse(AdjListDigraph graph, int index) {
visited = new boolean[graph.getVertexsNum()];
bfs(graph,index);
}
private void bfs(AdjListDigraph graph, int index) {
//在JSE中LinkedList實現了Queue接口
Queue<Integer> queue = new LinkedList<>();
visited[index] = true;
queue.add(index);
while(!queue.isEmpty()) {
int vertex = queue.poll();
for(int i : graph.adj(vertex)) {
if(!visited[i]) {
visited[i] = true;
queue.offer(i);
}
}
}
}
}
4、最小生成樹
圖的生成樹是它的一棵含有所有頂點的無環連通子圖。一棵加權圖的最小生成樹(MST)是它的一棵權值(所有邊的權值之和)最小的生成樹。
計算最小生成樹可能遇到的情況:
- 非連通的無向圖,不存在最小生成樹
- 權重不一定和距離成正比
- 權重可能是0或負數
- 若存在相等的權重,那么最小生成樹可能不唯一
圖的切分是將圖的所有頂點分為兩個非空且不重疊的兩個集合。橫切邊是一條連接兩個屬於不同集合的頂點的邊。
切分定理:在一幅加權圖中,給定任意的切分,它的橫切邊中的權重最小者必然屬於圖的最小生成樹。
切分定理是解決最小生成樹問題的所有算法的基礎。這些算法都是貪心算法。
首先先構造一個帶權的無向圖,其代碼如下:
//定義邊
public class Edge implements Comparable<Edge>{
private final int ver1;
private final int ver2;
private final Integer weight;
public Edge(int ver1, int ver2, int weight) {
super();
this.ver1 = ver1;
this.ver2 = ver2;
this.weight = weight;
}
//返回一個頂點
public int either() {
return ver1;
}
//返回另一個頂點
public int other(int vertex) {
if (vertex == ver1)
return ver2;
else if(vertex == ver2)
return ver1;
else
throw new RuntimeException("邊不一致");
}
@Override
public int compareTo(Edge e) {
return this.weight.compareTo(e.weight);
}
public Integer getWeight() {
return weight;
}
@Override
public String toString() {
return "Edge [" + ver1 + "," + ver2 +"]";
}
}
/**
* 帶權無向圖的實現
*/
public class WeightedGraph {
private final int vertexsNum;
private final int edgesNum;
private List<Edge>[] adj;
public WeightedGraph(int[][] data, int vertexsNum) {
this.vertexsNum = vertexsNum;
this.edgesNum = data.length;
adj = (List<Edge>[]) new ArrayList[vertexsNum];
for(int i=0; i<vertexsNum; i++) {
adj[i] = new ArrayList<>();
}
for (int i = 0; i < data.length; i++) {
Edge edge = new Edge(data[i][0],data[i][1],data[i][2]);
int v = edge.either();
adj[v].add(edge);
adj[edge.other(v)].add(edge);
}
}
public Iterable<Edge> adj(int vertex) {
return adj[vertex];
}
public int getVertexsNum() {
return vertexsNum;
}
public int getEdgesNum() {
return edgesNum;
}
public Iterable<Edge> getEdges() {
List<Edge> edges = new ArrayList<>();
for(int i=0; i<vertexsNum; i++) {
for(Edge e : adj[i]) {
if(i > e.other(i)) { //無向圖,防止將一條邊加入兩次
edges.add(e);
}
}
}
return edges;
}
}
4.1、Prim算法
每次將權值最小的橫切邊加入生成樹中
1)、Prim算法的延遲實現
實現過程如下圖:
從頂點0開始,首先將頂點0加入到樹中(標記),頂點0和其它點的橫切邊(這里即為頂點0的鄰接邊)加入優先隊列,將權值最小的橫切邊出隊,加入生成樹中。此時相當於也向樹中添加了一個頂點2,接着將集合(頂點1,2組成)和另一個集合(除1,2的頂點組成)間的橫切邊加入到優先隊列中,如此這般,直到隊列為空。
注意:若橫切邊中另一個頂點在樹中,則此邊失效。
代碼如下:
public class LazyPrimMST {
private boolean[] visited; //標記頂點
private LinkedQueue<Edge> mst; //存儲最小生成樹的邊
private MinPQ<Edge> pq; //優先隊列,權值越最小優先級越高
public LazyPrimMST(WeightedGraph wg) {
visited = new boolean[wg.getVertexsNum()];
mst = new LinkedQueue<Edge>();
pq = new MinPQ<>(wg.getVertexsNum());
visit(wg, 0); //從0點開始
while(!pq.isEmpty()) {
Edge e = pq.deQueue();
int ver1 = e.either();
int ver2 = e.other(ver1);
if(visited[ver1] && visited[ver2]) {
continue; //邊失效
}
mst.enQueue(e);
if(!visited[ver1])
visit(wg, ver1);
if(!visited[ver2])
visit(wg, ver2);
}
}
private void visit(WeightedGraph wg, int ver) {
visited[ver] = true; //標記頂點
for(Edge e : wg.adj(ver)) {
if(!visited[e.other(ver)])
pq.enQueue(e);
}
}
public Iterable<Edge> getMST() {
return mst;
}
public static void main(String[] args) {
int[][] data = {
{0, 2, 2},
{0, 1, 4},
{0, 5, 5},
{1, 2, 3},
{1, 5, 11},
{1, 3, 7},
{2, 3, 8},
{2, 4, 10},
{3, 5, 6},
{3, 4, 1},
{4, 5, 9}
};
WeightedGraph wg = new WeightedGraph(data,6);
LazyPrimMST lpm = new LazyPrimMST(wg);
for(Edge e : lpm.getMST()) {
System.out.println(e);
}
}
}
其中,LinkedQueue類的代碼在《數據結構與算法(三),棧與隊列》中;而MinPQ類的代碼與《數據結構與算法(五),優先隊列》中MaxPQ類的代碼幾乎一樣,只需將方法less中的小於號改為大於號即可。這里就不在給出代碼了
此方法的時間復雜度為 O(ElogE),空間復雜度為 O(E)。其中,V為頂點個數,E為邊數
2)、Prim算法即時實現
基於Prim算法的延遲實現,我們可以在優先隊列中只保存每個非樹頂點V的一條邊(即它與樹中的頂點連接起來的權重最小的那條邊),因為其他權重較大的邊遲早都會失效。
實現過程如下圖:
代碼實現:
/**
* prim的即時實現
*/
public class PrimMST {
private Edge[] edgeTo; //點離生成樹最近的邊
private int[] distTo; //點到生成樹的距離
private boolean[] visited;
private IndexMinPQ<Integer> pq; //索引優先隊列,關聯頂點與distTo
public PrimMST(WeightedGraph wg) {
//初始化
edgeTo = new Edge[wg.getVertexsNum()];
distTo = new int[wg.getVertexsNum()];
visited = new boolean[wg.getVertexsNum()];
for(int i=0; i<wg.getVertexsNum(); i++) {
distTo[i] = Integer.MAX_VALUE;
}
pq = new IndexMinPQ<>(wg.getVertexsNum());
distTo[0] = 0;
pq.insert(0, 0);
while(!pq.isEmpty()) {
visit(wg, pq.delMin());
}
}
private void visit(WeightedGraph wg, int ver) {
visited[ver] = true;
for(Edge e : wg.adj(ver)) {
int vertex = e.other(ver); //邊的另一個點
if(visited[vertex])
continue;
if(e.getWeight() < distTo[vertex]) {
edgeTo[vertex] = e; //被覆蓋的邊失效
distTo[vertex] = e.getWeight();
if(pq.contains(vertex)) {
pq.change(vertex, distTo[vertex]);
}else {
pq.insert(vertex, distTo[vertex]);
}
}
}
}
public Iterable<Edge> getMST() {
return Arrays.asList(edgeTo);
}
}
此方法的時間復雜度為 O(ElogV),空間復雜度為 O(V)。其中,V為頂點個數,E為邊數。
可以看出Prim算法的即時實現比延遲實現明顯要快,特別是對於稠密矩陣(E>>>V)的情況。
4.2、Kruskal算法
Kruskal算法的思想是按照邊的權重順序來生成最小生成樹,首先將圖中所有邊加入優先隊列,將權重最小的邊出隊加入最小生成樹,保證加入的邊不與已經加入的邊形成環,直到樹中有V-1到邊為止。
實現過程如下圖:
/**
* Kruskal算法的實現
*/
public class KruskalMST {
private List<Edge> mst; //存儲最小生成樹的邊
private MinPQ<Edge> pq; //優先隊列
private int[] parent; //用來判斷邊與邊是否形成回路
public KruskalMST(WeightedGraph wg) {
mst = new ArrayList<Edge>();
pq = new MinPQ<>(wg.getEdgesNum());
parent = new int[wg.getVertexsNum()];
for(Edge e : wg.getEdges()) {
pq.enQueue(e);
}
//最小生成樹的邊最多為V-1個
while(!pq.isEmpty() && mst.size() < wg.getVertexsNum() - 1) {
Edge e = pq.deQueue();
int v = e.either();
int n = find(parent, v);
int m = find(parent, e.other(v));
if(n != m) { //表示此邊沒有與生成樹形成環路
parent[n] = m;
mst.add(e);
}
}
}
//查找連接樹的尾部下標
private int find(int[] data, int v) {
while(parent[v] > 0) {
v = parent[v];
}
return v;
}
public Iterable<Edge> getMST() {
return mst;
}
}
Kruskal算法的時間復雜度最壞情況下為O(ElogE)。空間復雜度為O(E)。
對比Prim算法和Kruskal算法,Kruskal算法主要根據邊來生成樹,邊數少時效率比較高,適合稀疏圖;而Prim算法對邊數多的稠密圖效果更好一些。
5、最短路徑
最短路徑指兩頂點之間經過的邊上權值之和最少的路徑,並且稱路徑上的第一個頂點為源點,最后一個頂點為終點。
為了操作方便,首先使用面向對象的方法,來實現一個加權的有向圖,其代碼如下:
/**
* 有向邊
*/
public class Edge{
private final int from;
private final int to;
private final int weight;
public Edge(int from, int to, int weight) {
super();
this.from = from;
this.to = to;
this.weight = weight;
}
public int getFrom() {
return from;
}
public int getTo() {
return to;
}
public int getWeight() {
return weight;
}
}
//帶權有向圖的實現
public class WeightedDigraph {
private final int vertexsNum;
private final int edgesNum;
private List<Edge>[] adj; //鄰接表
public WeightedDigraph(int[][] data, int vertexsNum) {
this.vertexsNum = vertexsNum;
this.edgesNum = data.length;
adj = (List<Edge>[]) new ArrayList[vertexsNum];
for(int i=0; i<vertexsNum; i++) {
adj[i] = new ArrayList<>();
}
for (int i = 0; i < data.length; i++) {
Edge edge = new Edge(data[i][0],data[i][1],data[i][2]);
int v = edge.getFrom();
adj[v].add(edge);
}
}
public Iterable<Edge> adj(int vertex) {
return adj[vertex];
}
public int getVertexsNum() {
return vertexsNum;
}
public int getEdgesNum() {
return edgesNum;
}
//有向圖中所有的邊
public Iterable<Edge> getEdges() {
List<Edge> edges = new ArrayList<>();
for(List<Edge> list : adj) {
for(Edge e : list) {
edges.add(e);
}
}
return edges;
}
}
頂點到源點s的最短路徑,我們使用一個用頂點索引的Edge數組(edgeTo[])來存儲,使用數組distTo[]來存儲最短路徑樹(包含了源點S到所有可達頂點的最短路徑)。
邊的松弛操作:
邊的松弛過程如下圖:
松弛邊【1,4】就是檢查頂點0到4的最短路徑是否是先從頂點0到1,然后在由頂點1到4。如果是則【0,4】邊失效,將【1,4】加入最短路徑樹。
代碼:
private void relax(WeightedDigraph wd,Edge e) {
int v = e.getFrom();
int w = e.getTo();
if(distTo[w] > distTo[v] + e.getWeight()) {
distTo[w] = distTo[v] + e.getWeight();
edgeTo[w] = e;
}
}
頂點的松弛操作:
頂點的松弛就是松弛頂點的所有鄰接邊,這里就不給出過程了,實現代碼在Dijkstra實現中。
5.1、Dijkstra算法
算的的實現過程:
Dijkstra算法的代碼實現:
//Dijkstra算法的實現
public class Dijkstra {
private Edge[] edgeTo; //最短路徑樹
private int[] distTo; //存儲每個頂點到源點的距離
//索引優先隊列,建立distTo和頂點索引,distTo越小,優先級越高
private IndexMinPQ<Integer> pq;
public Dijkstra(WeightedDigraph wd, int s) {
edgeTo = new Edge[wd.getVertexsNum()];
distTo = new int[wd.getVertexsNum()];
pq = new IndexMinPQ<>(wd.getVertexsNum());
for(int i=0; i<wd.getVertexsNum(); i++) {
distTo[i] = Integer.MAX_VALUE;
}
distTo[s] = 0; //源點s的distTo為0
pq.insert(s, 0);
while(pq.isEmpty()) {
relax(wd, pq.delMin());
}
}
//頂點的松弛
private void relax(WeightedDigraph wd, int ver) {
for(Edge e : wd.adj(ver)) {
int v = e.getTo();
if(distTo[v] > distTo[ver] + e.getWeight()) {
distTo[v] = distTo[ver] + e.getWeight();
edgeTo[v] = e;
if(pq.contains(v)) {
pq.change(v, distTo[v]);
}else {
pq.insert(v, distTo[v]);
}
}
}
}
}
Dijkstra算法的局限性:圖中邊的權重必須為正,但可以是有環圖。時間復雜度為O(ElogV),空間復雜度O(V)。
這篇文章寫了好久,陸陸續續差不多快10天了,至今還有以下內容沒有總結:
- 圖的鄰接多重表
- 圖的邊集數組實現
- 最短路徑的Floyd算法
- 拓撲排序
- union-find算法
- 無環加權有向圖的最短路徑算法
- 關鍵路徑
- 計算無向圖中連通分量的Kosaraju算法
- 有向圖中含必經點的最短路徑問題
- TSP問題
- 還有A*算法
圖的知識實在是太多了,就先總結到這里吧,有時間在寫。