引言
最近我在讀 Robert Sedgewick 和 Kevin Wayne 的經典著作《算法(第4版)》:
這本書第4章第1節討論無向圖,下面就是無向圖的 API(英文版第522頁):
對於非稠密的無向圖,標准表示是使用鄰接表,將無向圖的每個頂點的所有相鄰頂點都保存在該頂點對應的元素所指向的一張鏈表中。所有的頂點保存在一個數組中,使用這個數組就可以快速訪問給定頂點的鄰接頂點列表。下面就是非稠密無向圖的一個例子(英文版第525頁):
這種 Graph 的實現的性能有如下特點:
- 使用的空間和 V + E 成正比
- 添加一條邊所需的時間為常數
- 遍歷頂點 v 的所有相鄰頂點所需的時間和 v 的度數成正比(處理每個相鄰頂點所需的時間為常數)
對於這些操作,這樣的特性已經是最優的了,已經可以滿足圖處理應用的需要。
Java 程序
下面就是 Graph.java 程序(英文版第526頁):
1 public class Graph 2 { 3 private final int V; // number of vertices 4 private int E; // number of edges 5 private Bag<Integer>[] adj; // adjacency lists 6 7 public Graph(int V) 8 { 9 this.V = V; this.E = 0; 10 adj = (Bag<Integer>[]) new Bag[V]; // Create array of lists. 11 for (int v = 0; v < V; v++) // Initialize all lists 12 adj[v] = new Bag<Integer>(); // to empty. 13 } 14 15 public Graph(In in) 16 { 17 this(in.readInt()); // Read V and construct this graph. 18 int E = in.readInt(); // Read E. 19 for (int i = 0; i < E; i++) 20 { // A an edge. 21 int v = in.readInt(); // Read a vertex, 22 int w = in.readInt(); // read another vertex, 23 addEdge(v, w); // and add edge connecting them. 24 } 25 } 26 27 public int V() { return V; } 28 public int E() { return E; } 29 30 public void addEdge(int v, int w) 31 { 32 adj[v].add(w); // Add w to v's list. 33 adj[w].add(v); // Add v to w's list. 34 E++; 35 } 36 37 public Iterable<Integer> adj(int v) 38 { return adj[v]; } 39 40 public String toString() 41 { 42 StringBuilder s = new StringBuilder(); 43 String NEWLINE = System.getProperty("line.separator"); 44 s.append(V + " vertices, " + E + " edges" + NEWLINE); 45 for (int v = 0; v < V; v++) 46 { 47 s.append(v + ": "); 48 for (int w : adj[v]) s.append(w + " "); 49 s.append(NEWLINE); 50 } 51 return s.toString(); 52 } 53 54 public static void main(String[] args) 55 { 56 Graph G = new Graph(new In(args[0])); 57 StdOut.println(G); 58 } 59 }
在上述程序中:
- 第 3 行的字段表示該無向圖的頂點數 V,這個值在構造函數中初始化后就不能修改了。
- 第 4 行的字段表示該無向圖的邊數 E 。
- 第 5 行的字段表示該無向圖的鄰接表數組。
- 第 7 至 13 行的 Graph 構造函數創建一幅含有 V 個頂點但沒有邊的無向圖。
- 第 10 行創建鄰接表數組,每個頂點都對應數組的一項,其元素就是該頂點的鄰接表,這個鄰接表使用 Bag 抽象數據類型來實現。
- Bag 數據類型請參見《算法(第4版)》1.3 節,該數據類型使得我們可以在常數時間內添加新的邊或遍歷任意頂點的所有相鄰頂點。
- 由於 Java 語言固有的缺點,無法創建泛型數組,所以第 10 行中只能創建普通數組后強制轉型為泛型數組。這導致在編譯時出現警告信息。
- 由於 Java 語言固有的缺點,泛型的參數類型不能是原始數據類型,所以第 5、10、12 和 37 行的泛型的參數類型是 Integer,而不是 int 。這導致了一些性能損失。
- 第 15 至 25 行的構造函數從輸入流生成無向圖。輸入流的內容首先是頂點數 V,接着是邊數 E,然后是每條邊的頂點。輸入流的示例請參見引言中的 tinyG.txt 。表示輸入流的 In 類型請參見《算法(第4版)》1.1 節。
- 第 30 至 35 行的 addEdge 方法添加一條連接 v 與 w 的邊,其做法是將 w 添加到 v 的鄰接表中並把 v 添加到 w 的鄰接表中。因此,在這個數據結構中每條邊都會出現兩次。
- 第 40 至 52 行的 toString 方法返回該無向圖的鄰接表表示,首先是頂點數 V 和邊數 E,然后是各頂點的鄰接表。
- 第 54 至 58 行的 main 是測試用例。它從命令行參數獲得輸入流的名稱。
- 第 56 行從輸入流構造無向圖。
- 第 57 行(隱式)調用 toString 方法輸出該無向圖。
編譯和運行:
work$ javac Graph.java 注: Graph.java使用了未經檢查或不安全的操作。 注: 有關詳細信息, 請使用 -Xlint:unchecked 重新編譯。 work$ java Graph tinyG.txt 13 vertices, 13 edges 0: 6 2 1 5 1: 0 2: 0 3: 5 4 4: 5 6 3 5: 3 4 0 6: 0 4 7: 8 8: 7 9: 11 10 12 10: 9 11: 9 12 12: 11 9
這里的 tinyG.txt 文件的內容如下所示(內容和引言中的一樣,列在這里是為了方便各位復制粘貼。引言中的是圖片,無法復制粘貼):
work$ cat tinyG.txt 13 13 0 5 4 3 0 1 9 12 6 4 5 4 0 2 11 12 9 10 0 6 7 8 9 11 5 3
C# 程序
將上一節的 Graph.java 翻譯為 C# 程序,得到 Graph.cs :
1 using System; 2 using System.Text; 3 using System.Collections.Generic; 4 5 namespace Skyiv 6 { 7 public class Graph 8 { 9 public int V { get; private set; } // number of vertices 10 public int E { get; private set; } // number of edges 11 Stack<int>[] adj; // adjacency lists 12 13 public Graph(int V) 14 { Initialize(V); } 15 16 public Graph(In @in) 17 { 18 Initialize(@in.ReadInt()); // Read V and construct this graph. 19 int E = @in.ReadInt(); // Read E. 20 for (var i = 0; i < E; i++) 21 { // Add an edge. 22 var v = @in.ReadInt(); // Read a vertex, 23 var w = @in.ReadInt(); // read another vertex, 24 AddEdge(v, w); // and add edge connecting them. 25 } 26 } 27 28 void Initialize(int V) 29 { 30 this.V = V; 31 adj = new Stack<int>[V]; // Create array of lists. 32 for (int v = 0; v < V; v++) // Initialize all lists 33 adj[v] = new Stack<int>(); // to empty. 34 } 35 36 public void AddEdge(int v, int w) 37 { 38 adj[v].Push(w); // Add w to v's list. 39 adj[w].Push(v); // Add v to w's list. 40 E++; 41 } 42 43 public IEnumerable<int> Adj(int v) 44 { return adj[v]; } 45 46 public override string ToString() 47 { 48 var s = new StringBuilder(); 49 s.AppendLine(V + " vertices, " + E + " edges"); 50 for (var v = 0; v < V; v++) 51 { 52 s.Append(v + ": "); 53 foreach (var w in Adj(v)) s.Append(w + " "); 54 s.AppendLine(); 55 } 56 return s.ToString(); 57 } 58 59 static void Main(string[] args) 60 { 61 var G = new Graph(new In(args[0])); 62 Console.Write(G); 63 } 64 } 65 }
上述 C# 程序基本上是 Java 程序的翻譯:
- 第 9 至 10 行使用 C# 的屬性代替 Java 字段,表示頂點數 V 和邊數 E 。
- 第 11 行使用泛型的 Stack<int> 類型代替 Java 的 Bag<Integer> 類型,表示各頂點的鄰接表。
- C# 語言沒有上面說的 Java 語言的缺點,可以直接使用 int 類型作為泛型參數。
- 第 13 至 26 行的兩個構造函數基本上是 Java 語言版本的翻譯。
- 第 28 至 34 行的 Initialize 方法是 Java 語言版本第一個構造函數的翻譯。
- 第 31 行創建鄰接表數組,它沒有 Java 語言的缺點,可以直接創建泛型數組,不用強制轉型。
- 第 46 至 57 行的 ToString 方法是 Java 語言版本的 toString 方法的翻譯。C# 語言的 StringBuilder 類有 AppendLine 方法,就不需要 Java 語言版本中的 NEWLINE 了。
- 其他各個方法也基本上是 Java 語言版本的對應翻譯。
編譯和運行的結果如下所示:
work$ dmcs Graph.cs In.cs work$ mono Graph.exe tinyG.txt 13 vertices, 13 edges 0: 6 2 1 5 1: 0 2: 0 3: 5 4 4: 5 6 3 5: 3 4 0 6: 0 4 7: 8 8: 7 9: 11 10 12 10: 9 11: 9 12 12: 11 9
可以看到,運行結果和 Java 程序一模一樣。
Java 和 C# 程序的比較
前兩節 Graph.java 和 Graph.cs 這兩個程序是不是非常相像?還可以更像一點,在 Graph.java 使用了《算法(第4版)》作者寫的 Bag 數據類型,這其實可以替換為 Java 標准庫中的 Stack 數據類型。在 Graph.java 中:
- 把所有的 Bag 都替換為 Stack
- 把第 32 行和第 33 行的兩個 add 替換為 push
這樣修改后的 Java 程序運行結果不變。其實,誕生於 2000 年的 C# 語言受誕生於 1995 年的 Java 語言的影響非常大,並且利用其后發優勢,繼承 Java 語言的優點,拋棄 Java 的缺點。比如前面提到的 Java 語言在泛型方面的兩個缺點(《算法(第4版)》這本書中也對這兩個缺點引以為憾),在 C# 語言中就不在存在了。Java 語言要照顧以前寫的代碼,向前兼容,歷史包袱太大了。Scala 語言是 Java 平台上的新興語言,很有發展前途。不過我個人更看好 C# 語言,主要是用 C# 語言寫程序。
加料的 C# 程序
在引言中的 Output for list-of-edges input 的圖中,每條邊出現第二次時被標記為紅色,這是《算法(第4版)》的作者手工標記的,而不是程序的實際運行結果。如果我們的程序要做到這一點,可以在 Graph.cs 中作如下修改:
1. 在第 11 行之后增加一條語句(repeated 作為 Graph 類的字段,用於標記第二次出現的邊):
HashSet<Tuple<int, int>> repeated = new HashSet<Tuple<int, int>>();
2. 在第 40 行之后增加一條語句(在 AddEdge 方法中,將第二次出現的邊標記為紅色):
repeated.Add(Tuple.Create(w, v));
3. 在第 58 行之后增加以下 Display 方法(將第二次出現的邊使用紅色顯示):
public void Display() { var defaultColor = Console.ForegroundColor; Console.WriteLine("{0} vertices, {1} edges", V, E); for (var v = 0; v < V; v++) { Console.Write(v + ": "); foreach (var w in Adj(v)) { var red = repeated.Contains(Tuple.Create(v, w)); if (red) Console.ForegroundColor = ConsoleColor.Red; Console.Write(w + " "); if (red) Console.ForegroundColor = defaultColor; } Console.WriteLine(); } }
4. 將第 62 行的語句改為(在 Main 方法中,使用 Display 方法代替隱式的 ToString 方法):
G.Display();
下面就是修改后的程序的運行結果:
讀取輸入的 C# 輔助程序
下面是前面用到的 In.cs 的源程序:
1 using System; 2 using System.IO; 3 4 namespace Skyiv 5 { 6 public class In : IDisposable 7 { 8 static readonly byte[] BLANKS = { 9, 10, 13, 32 }; // tab,lf,cr,space 9 static readonly byte EOF = 0; // assume '\0' not in input file 10 11 byte[] buffer = new byte[64 * 1024]; 12 int current = 0, count = 0; 13 Stream reader; 14 15 public In() : this(Console.OpenStandardInput()) {} 16 public In(string name) : this(File.OpenRead(name)) {} 17 public In(Stream stream) { reader = stream; } 18 19 byte ReadByte() 20 { 21 if (current >= count) 22 { 23 count = reader.Read(buffer, current = 0, buffer.Length); 24 if (count == 0) return EOF; 25 } 26 return buffer[current++]; 27 } 28 29 public int ReadInt() 30 { 31 var n = 0; 32 var ok = false; 33 for (byte b; (b = ReadByte()) != EOF; ) 34 { 35 if (Array.IndexOf(BLANKS, b) >= 0) 36 if (ok) break; 37 else continue; 38 n = n * 10 + (b - '0'); 39 ok = true; 40 } 41 return n; 42 } 43 44 public void Dispose() 45 { 46 if (reader != null) reader.Close(); 47 } 48 } 49 }
這個程序來源於參考資料[7]。
運行環境
我們的程序是在 Arch Linux 64-bit 操作系統下運行的,Java 環境是 OpenJDK 1.7.0,.NET 環境是 Mono 3.0.6:
work$ uname -a Linux m6100t 3.7.10-1-ARCH #1 SMP PREEMPT Thu Feb 28 09:50:17 CET 2013 x86_64 GNU/Linux work$ java -version java version "1.7.0_15" OpenJDK Runtime Environment (IcedTea7 2.3.7) (ArchLinux build 7.u13_2.3.7-2-x86_64) OpenJDK 64-Bit Server VM (build 23.7-b01, mixed mode) work$ javac -version javac 1.7.0_15 work$ echo $CLASSPATH /home/ben/src/algs/stdlib.jar:/home/ben/src/algs/algs4.jar work$ mono --version Mono JIT compiler version 3.0.6 (tarball 2013年 03月 11日 星期一 11:54:36 CST) Copyright (C) 2002-2012 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com TLS: __thread SIGSEGV: altstack Notifications: epoll Architecture: amd64 Disabled: none Misc: softdebug LLVM: supported, not enabled. GC: Included Boehm (with typed GC and Parallel Mark) work$ dmcs --version Mono C# compiler version 3.0.6.0
注意,要編譯和運行本文中 Java 程序,需要設置 CLASSPATH 環境變量,請見參考資料[3] 。
參考資料
- 算法(第4版),[美] Robert Sedgewick, Kevin Wayne 著,謝路雲譯,人民郵電出版社,2012年10月第1版
- 算法(英文版 第4版),[美] Robert Sedgewick, Kevin Wayne 著,人民郵電出版社,2012年3月第1版
- Algorithms (Fourth Edition, Robert Sedgewick, Kevin Wayne): Java Algorithms and Clients
- Algorithms (Fourth Edition, Robert Sedgewick, Kevin Wayne): Undirected Graphs
- Wikipedia: Graph theory
- Wikipedia: Adjacency list
- 博客園:C# I/O 助手類