最近我在读 Robert Sedgewick 和 Kevin Wayne 的经典著作《算法(第4版)》:
这本书第4章第1节讨论无向图,下面就是无向图的 API(英文版第522页):
对于非稠密的无向图,标准表示是使用邻接表,将无向图的每个顶点的所有相邻顶点都保存在该顶点对应的元素所指向的一张链表中。所有的顶点保存在一个数组中,使用这个数组就可以快速访问给定顶点的邻接顶点列表。下面就是非稠密无向图的一个例子(英文版第525页):
这种 Graph 的实现的性能有如下特点:
对于这些操作,这样的特性已经是最优的了,已经可以满足图处理应用的需要。
下面就是 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 }
在上述程序中:
编译和运行:
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
将上一节的 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 程序的翻译:
编译和运行的结果如下所示:
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 程序一模一样。
前两节 Graph.java 和 Graph.cs 这两个程序是不是非常相像?还可以更像一点,在 Graph.java 使用了《算法(第4版)》作者写的 Bag 数据类型,这其实可以替换为 Java 标准库中的 Stack 数据类型。在 Graph.java 中:
这样修改后的 Java 程序运行结果不变。其实,诞生于 2000 年的 C# 语言受诞生于 1995 年的 Java 语言的影响非常大,并且利用其后发优势,继承 Java 语言的优点,抛弃 Java 的缺点。比如前面提到的 Java 语言在泛型方面的两个缺点(《算法(第4版)》这本书中也对这两个缺点引以为憾),在 C# 语言中就不在存在了。Java 语言要照顾以前写的代码,向前兼容,历史包袱太大了。Scala 语言是 Java 平台上的新兴语言,很有发展前途。不过我个人更看好 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();
下面就是修改后的程序的运行结果:
下面是前面用到的 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] 。