圖的鄰接鏈表實現
public class LinkedGraph<T> {
// 頂點的訪問狀態
private static final int UNDISCOVERED = 0;
private static final int DISCOVERED = 1;
private static final int VISITED = 2;
// 頂點類
private static class Vertex<T> {
T data;
int inDegree, outDegree; // 入度、出度
int status; // 訪問狀態
// 求最短路徑時有用
int distance; // 從某個起點到當前頂點的最短路徑的距離
// 或在最小生成樹中,當前頂點到父頂點的最小距離
int from; // 最短路徑上,通往當前頂點的前一個頂點
// 或在最小生成樹中,當前頂點的父頂點
Vertex(T data) {
this.data = data;
from = -1;
}
}
// 鏈表節點類
private static class LinkedNode {
int index; // 鄰居頂點的索引
int weight; // 邊的權重
LinkedNode(int index, int weight) {
this.index = index;
this.weight = weight;
}
}
// 鏈表頭部
private static class LinkedHead<T> {
Vertex<T> vertex; // 頂點
List<LinkedNode> neighbors; // 頂點的鄰居
LinkedHead(Vertex<T> vertex) {
this.vertex = vertex;
neighbors = new LinkedList<>();
}
}
private List<LinkedHead<T>> heads; // 所有鏈表的頭
public LinkedGraph() {
heads = new ArrayList<>();
}
// 插入頂點
public void insertVertex(T data) {
Vertex<T> vertex = new Vertex<>(data);
LinkedHead<T> head = new LinkedHead<>(vertex);
heads.add(head);
}
// 刪除頂點
public void removeVertex(int index) {
// 更新頂點的度-所有出邊
List<LinkedNode> neighbors = getNeighbors(index);
for (int i = 0; i < neighbors.size(); i++) {
getVertex(neighbors.get(i).index).inDegree--;
}
heads.remove(index);
// 更新頂點的度-所有入邊
for (int i = 0; i < heads.size(); i++) {
neighbors = getNeighbors(i);
Iterator<LinkedNode> iterator = neighbors.iterator();
while (iterator.hasNext()) {
LinkedNode node = iterator.next();
if (node.index == index) {
iterator.remove();
getVertex(i).outDegree--;
break;
}
}
}
}
// 插入邊
public void insertEdge(int from, int to, int weight) {
LinkedNode node = new LinkedNode(to, weight);
getNeighbors(from).add(node);
// 更新頂點的度
getVertex(from).outDegree++;
getVertex(to).inDegree++;
}
// 是否存在指定的邊
public boolean exists(int from, int to) {
List<LinkedNode> neighbors = getNeighbors(from);
for (int i = 0; i < neighbors.size(); i++) {
if (neighbors.get(i).index == to) {
return true;
}
}
return false;
}
// 刪除邊
public void removeEdge(int from, int to) {
if (!exists(from, to)) {
return;
}
// 更新頂點的度
getVertex(from).outDegree--;
getVertex(to).inDegree--;
// 刪除鄰居
List<LinkedNode> neighbors = getNeighbors(from);
Iterator<LinkedNode> iterator = neighbors.iterator();
while (iterator.hasNext()) {
LinkedNode node = iterator.next();
if (node.index == to) {
iterator.remove();
break;
}
}
}
// 獲取頂點 vertex 的鄰居
private List<LinkedNode> getNeighbors(int vertex) {
return heads.get(vertex).neighbors;
}
// 獲取 index 處的頂點
private Vertex<T> getVertex(int index) {
return heads.get(index).vertex;
}
...
}
廣度優先搜索
始自圖中頂點 s 的BFS搜索,將首先訪問頂點 s;再依次訪問 s 所有尚未被訪問到的鄰居;再按後者被訪問的先後次序,逐個訪問它們的鄰居; …;如此不斷。
BFS搜索總體僅需 O(n + e) 時間。(n 爲頂點數,e 爲邊數)
// 廣度優先搜索
public void breadthFirstSearch(int startVertex) {
reset(); // 重置頂點的訪問狀態
int vertex = startVertex;
int n = heads.size();
do {
if (getVertex(vertex).status == UNDISCOVERED) {
breadthFirstSearchHelper(vertex);
}
vertex = ++vertex % n; // 確保所有頂點都被訪問過
} while (vertex != startVertex);
}
// 重置頂點的訪問狀態
private void reset() {
for (int i = 0; i < heads.size(); i++) {
heads.get(i).vertex.status = UNDISCOVERED;
}
}
// startVertex:搜索過程的起始頂點
private void breadthFirstSearchHelper(int startVertex) {
Queue<Integer> queue = new ArrayDeque<>();
// 起點入隊列
getVertex(startVertex).status = DISCOVERED;
queue.offer(startVertex);
while (!queue.isEmpty()) {
int vertex = queue.poll();
// 將未被發現的鄰居入隊列
List<LinkedNode> neighbors = getNeighbors(vertex);
for (int i = 0; i < neighbors.size(); i++) {
int neighbor = neighbors.get(i).index;
if (getVertex(neighbor).status == UNDISCOVERED) {
getVertex(neighbor).status = DISCOVERED;
queue.offer(neighbor);
}
}
// 對頂點執行操作...
System.out.println(getVertex(vertex).data);
// 當前節點訪問完畢
getVertex(vertex).status = VISITED;
}
}
深度優先搜索
以頂點 s 爲基點的DFS搜索,將首先訪問頂點 s;再從 s 所有尚未訪問到的鄰居中任取其一, 並以之爲基點, 遞歸地執行DFS搜索。如果 s 所有的鄰居都已經被訪問過了,則回溯至 s 的上一個頂點,然後繼續 DFS 搜索。
深度優先搜索算法也可在 O(n + e) 時間內完成。
// 深度優先搜索
public void depthFirstSearch(int startVertex) {
reset(); // 重置頂點的訪問狀態
int vertex = startVertex;
int n = heads.size();
do {
if (getVertex(vertex).status == UNDISCOVERED) {
depthFirstSearchHelper(vertex);
}
vertex = ++vertex % n; // 確保所有頂點都被訪問過
} while (vertex != startVertex);
}
private void depthFirstSearchHelper(int startVertex) {
ArrayDeque<Integer> stack = new ArrayDeque<>();
int vertex = startVertex;
boolean moved = true; // 是否移動過
do {
// 如果沒有向前移動,則說明無頂點可繼續深入,此時應該回溯
if (!moved) {
getVertex(vertex).status = VISITED;
// 如果棧不爲空,則回溯上一個頂點
if (!stack.isEmpty()) {
vertex = stack.pop();
} else {
break;
}
}
// 已向前移動,當前頂點是一個未被訪問的新頂點,則訪問之
else {
getVertex(vertex).status = DISCOVERED;
// 對頂點操作...
System.out.println(getVertex(vertex).data);
// 在這一輪中,當前還沒有向前移動
moved = false;
}
// 有未被訪問的鄰居,則將當前頂點入棧,然後深入下一個頂點
List<LinkedNode> neighbors = getNeighbors(vertex);
for (int i = 0; i < neighbors.size(); i++) {
int neighbor = neighbors.get(i).index;
if (getVertex(neighbor).status == UNDISCOVERED) {
stack.push(vertex);
vertex = neighbor;
moved = true;
break;
}
}
} while (true);
}
拓撲排序
有向無環圖的拓撲排序必然存在;反之亦然。
對於有向無環圖 G,只要將入度爲 0 的頂點 m(及其關聯邊) 從圖 G 中取出, 則剩餘的 G’ 依然是有向無環圖,故其拓撲排序也必然存在。 從遞歸的角度看, 一旦得到了 G’ 的拓撲排序,只需將 m 作爲最大頂點插入,即可得到 G 的拓撲排序。
拓撲排序算法也可在 O(n + e) 時間內完成。
// 拓撲排序
public void topologicalSort() {
List<Integer> vertices = topologicalSortHelper();
// 可以進行拓撲排序
if (vertices != null) {
for (int i = 0; i < vertices.size(); i++) {
// 對頂點執行操作 ...
System.out.println(getVertex(vertices.get(i)).data);
}
}
// 非 DAG,不可進行拓撲排序
else {
System.out.println("NOT DAG!!");
}
}
private List<Integer> topologicalSortHelper() {
resetDegree(); // 重置頂點的度
Queue<Integer> queue = new ArrayDeque<>();
List<Integer> list = new ArrayList<>(heads.size()); // 保存排序結果
// 入度爲 0 的頂點入隊列
for (int i = 0; i < heads.size(); i++) {
if (heads.get(i).vertex.inDegree == 0) {
queue.offer(i);
}
}
while (!queue.isEmpty()) {
// 從圖中移除入度爲 0 的頂點
int vertex = queue.poll();
list.add(vertex);
// 更新鄰居的入度,入度爲 0 的頂點入隊列
List<LinkedNode> neighbors = getNeighbors(vertex);
for (int i = 0; i < neighbors.size(); i++) {
int neighbor = neighbors.get(i).index;
if (--getVertex(neighbor).inDegree == 0) {
queue.offer(neighbor);
}
}
}
if (list.size() == heads.size()) {
return list;
}
return null; // 非 DAG
}
// 重新設置頂點的度
private void resetDegree() {
for (int i = 0; i < heads.size(); i++) {
List<LinkedNode> neighbors = getNeighbors(i);
// 出度
getVertex(i).outDegree = neighbors.size();
// 入度
for (int j = 0; j < neighbors.size(); j++) {
getVertex(neighbors.get(j).index).inDegree++;
}
}
}
單源最短路徑:Dijkstra
以頂點 s 爲起點,設 爲頂點 v 到頂點 s 的最短距離。
-
初始時。每個頂點都還是處於未被發現的狀態,且它們到 s 的距離都爲 ,除了 s 到 s 的距離爲 0。
-
Dijkstra 算法從所有未被發現的頂點中選擇一個頂點 v,該頂點在所有未被發現的頂點中具有最小的 ,同時將該頂點標記爲已發現的。
-
對頂點 v 的所有鄰居頂點 w,將 設爲 ,其中 表示 v 到 w 的邊的權重。
-
繼續步驟 2,直到所有頂點都已被發現。
時間複雜度爲 。
// 最短路徑:從 from 到 to 頂點
public ArrayDeque<Integer> shortestPath(int from, int to) {
dijkstra(from);
// 保存最短的路徑:逆序保存
ArrayDeque<Integer> stack = new ArrayDeque<>();
int current = to;
while (current != -1) {
stack.push(current);
current = getVertex(current).from;
}
return stack;
}
// Dijkstra 算法
private void dijkstra(int startVertex) {
// 初始化
for (int i = 0; i < heads.size(); i++) {
Vertex<T> vertex = getVertex(i);
vertex.distance = Integer.MAX_VALUE;
vertex.status = UNDISCOVERED;
}
Vertex<T> v = getVertex(startVertex);
v.distance = 0;
int undiscoverd = heads.size(); // 未被發現的頂點的數量
while (undiscoverd > 0) {
// 找到距離最短的、還未被發現的頂點 v
int smallestIndex = -1;
int smallestDistance = Integer.MAX_VALUE;
for (int i = 0; i < heads.size(); i++) {
Vertex<T> vertex = getVertex(i);
if (vertex.status == UNDISCOVERED && vertex.distance < smallestDistance) {
smallestDistance = vertex.distance;
smallestIndex = i;
}
}
// 發現之
v = getVertex(smallestIndex);
v.status = DISCOVERED;
undiscoverd--;
// 更新頂點 v 的鄰居的距離
List<LinkedNode> neighbors = getNeighbors(smallestIndex);
for (int i = 0; i < neighbors.size(); i++) {
LinkedNode neighbor = neighbors.get(i);
Vertex<T> w = getVertex(neighbor.index);
if (w.status == UNDISCOVERED) {
int costOfv2w = neighbor.weight;
if (v.distance + costOfv2w < w.distance) {
w.distance = v.distance + costOfv2w;
w.from = smallestIndex;
}
}
}
}
}
最小生成樹:Prim
連通圖 G 的某一無環連通子圖 T 若覆蓋 G 中所有的頂點,則稱作 G 的一棵支撐樹或生成樹(spanning tree) 。
Prim 算法的大致過程是,每次選擇一條邊 (u, v),其中,頂點 u 在樹上,頂點 v 不在樹上,且 (u, v) 邊的權在所有候選者中是最小的。然後,將 (u, v) 添加到樹上。
以頂點 s 爲起點,設 爲頂點 v 連接到已知頂點(已在樹上)的最短邊的權重。
-
初始時。每個頂點都還是處於未被發現的狀態(即不在樹上),且它們到已知頂點的距離都爲 ,除了 s 到 s 的距離爲 0。
-
Prim 算法從所有未被發現的頂點中選擇一個頂點 v,該頂點在所有未被發現的頂點中具有最小的 ,同時將該頂點標記爲已發現的。
-
對頂點 v 的所有鄰居頂點 w,將 設爲 ,其中 表示 v 到 w 的邊的權重。
-
繼續步驟 2,直到所有頂點都已被發現。
時間複雜度爲 。
public void minimumSpanningTreeByPrim(int startVertex) {
prim(startVertex);
// 打印每個頂點的父頂點
for (int i = 0; i < heads.size(); i++) {
Vertex<T> vertex = getVertex(i);
System.out.println(vertex.data + " parent: " + getVertex(vertex.from).data);
}
}
// Prim:和 Dijkstra 的代碼非常相似,只是 distance 表示的意思不同,且在更新 distance 部分不同
private void prim(int startVertex) {
// 初始化
for (int i = 0; i < heads.size(); i++) {
Vertex<T> vertex = getVertex(i);
vertex.distance = Integer.MAX_VALUE;
vertex.status = UNDISCOVERED;
}
Vertex<T> v = getVertex(startVertex);
v.distance = 0;
int undiscoverd = heads.size();
while (undiscoverd > 0) {
// 找到邊最短的、還未被發現的頂點 v
int smallestIndex = -1;
int smallestDistance = Integer.MAX_VALUE;
for (int i = 0; i < heads.size(); i++) {
Vertex<T> vertex = getVertex(i);
if (vertex.status == UNDISCOVERED && vertex.distance < smallestDistance) {
smallestDistance = vertex.distance;
smallestIndex = i;
}
}
// 發現之
v = getVertex(smallestIndex);
v.status = DISCOVERED;
undiscoverd--;
// 更新頂點 v 的鄰居的距離
List<LinkedNode> neighbors = getNeighbors(smallestIndex);
for (int i = 0; i < neighbors.size(); i++) {
LinkedNode neighbor = neighbors.get(i);
Vertex<T> w = getVertex(neighbor.index);
if (w.status == UNDISCOVERED) {
int costOfv2w = neighbor.weight;
if (costOfv2w < w.distance) {
w.distance = costOfv2w;
w.from = smallestIndex;
}
}
}
}
}
最小生成樹:Kruskal
Kruskal 算法的大致過程是:每次選擇具有最小權重的邊,並且當所選的邊不產生環時,纔將其添加到樹上。
具體而言,就是對於候選邊 (u, v),如果 u、v 在同一個集合中,則放棄該邊(因爲兩者已經連通,再添加該邊將產生環)。如果 u、v 不在同一集合中,則接受該邊,併合並 u、v 所在的兩個集合。
時間複雜度爲 。
// 表示圖的邊(作爲內部類)
private static class Edge implements Comparable<Edge> {
int u, v; // 邊 (u, v)
int weight; // 邊的權重
Edge(int u, int v, int weight) {
this.u = u;
this.v = v;
this.weight = weight;
}
// 比較邊時有用
public int compareTo(Edge other) {
return weight - other.weight;
}
}
public void minimumSpanningTreeByKruskal() {
// 獲取所有的邊 (u, v)
List<Edge> edges = new ArrayList<>();
for (int u = 0; u < heads.size(); u++) {
List<LinkedNode> neighbors = getNeighbors(u);
for (int i = 0; i < neighbors.size(); i++) {
int v = neighbors.get(i).index;
int weight = neighbors.get(i).weight;
edges.add(new Edge(u, v, weight));
}
}
List<Edge> tree = kruskal(edges);
for (int i = 0; i < tree.size(); i++) {
System.out.println(getVertex(tree.get(i).u).data + " - " + getVertex(tree.get(i).v).data);
}
}
// 不相交集合
// union-find 算法
private static class DisjointSet {
// 當 sets[i] >= 0 時,即表示 i 的父親
// 當 sets[i] < 0 時,即表示 i 爲根,且該樹高度爲 -sets[i]
private int[] sets;
// 初始時有 numElements 個集合
DisjointSet(int numElements) {
sets = new int[numElements];
for (int i = 0; i < numElements; i++) {
sets[i] = -1; // 樹高爲 1
}
}
// 合併兩個不相交集合
// root1 和 root2 分別是兩個集合所形成的樹的根
// -sets[root1] 表示 root1 集合所形成的樹的高度
// 誰高誰就是新根,同高則增加合併後的樹的高
void union(int root1, int root2) {
if (sets[root2] < sets[root1]) { // root2 高
sets[root1] = root2; // 以 root2 爲根
} else {
if (sets[root1] == sets[root2]) { // 同高
sets[root1]--; // 增加樹高
}
sets[root2] = root1;
}
}
// 找到 x 所在集合的根
int find(int x) {
// 向上追尋
while (sets[x] >= 0) {
x = sets[x];
}
return x;
}
}
// Kruskal 算法
private List<Edge> kruskal(List<Edge> edges) {
int numVertices = heads.size();
DisjointSet disjSet = new DisjointSet(numVertices); // 初始化 numVertices 個不相交集合,一個頂點即爲一個集合
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(edges); // 將所有邊放到堆中
List<Edge> mininumSpanningTree = new ArrayList<>(); // 保存結果
while (mininumSpanningTree.size() != numVertices - 1) {
Edge edge = priorityQueue.poll(); // 獲取、移除最小的邊
int rootu = disjSet.find(edge.u); // 找到集合的根
int rootv = disjSet.find(edge.v);
if (rootu != rootv) { // u、v 不屬於同一個集合
mininumSpanningTree.add(edge); // 接受該邊,成爲生成樹的一條邊
disjSet.union(rootu, rootv); // 合併兩個集合
}
}
return mininumSpanningTree;
}