遍历算法的重用
如何重用实现的遍历算法,这是我在面试中常问的一个问题。其实问题并不难,主要考察对语言特性的掌握,以及处理这种常见场景的经验。或许是交谈中没有实例的原因,满意的答案不多。
假设用Node类来表示树形结构中的节点。
1 public class Node 2 { 3 public Node(string name) 4 { 5 this.Name = name; 6 this.Children = new List<Node>(); 7 } 8 9 public string Name { get; set; } 10 public Node Parent { get; set; } 11 public IList<Node> Children { get; set; } 12 13 public void Add(Node child) 14 { 15 child.Parent = this; 16 this.Children.Add(child); 17 } 18 }
使用这个类来构建我们的行政区域。
1 var nodes = new Dictionary<string, Node> () 2 { 3 {"China", new Node("China")}, 4 {"SiChuan", new Node("Si Chuan")}, 5 {"GuangDong", new Node("Guang Dong")}, 6 {"ChengDu", new Node("Cheng Du")}, 7 {"ChongQin", new Node("Chong Qin")} 8 }; 9 nodes["China"].Add(nodes["SiChuan"]); 10 nodes["China"].Add(nodes["GuangDong"]); 11 nodes["SiChuan"].Add(nodes["ChengDu"]); 12 nodes["SiChuan"].Add(nodes["ChongQin"])
现在遍历给定的一个节点,将其及所有子节点的名称打印出来。
1 public void Iterate(Node node) 2 { 3 Console.WriteLine(node.Name); 4 foreach (var child in node.Children) 5 { 6 this.Iterate(child); 7 } 8 }
这个例子中的迭代算法很简单,仅使用递归来遍历所有的子节点。实际情况会复杂,比如同时要遍历其兄弟节点。如果我们还需要遍历节点做其他事情,那就要考虑如何重用这个遍历算法。
- 函数参数
C#是以委托的形式把函数作为参数来使用。
1 public class NodeIterator 2 { 3 public void Iterate(Node node, Action<Node> action) 4 { 5 action(node); 6 foreach (var child in node.Children) 7 { 8 this.Iterate(child, action); 9 } 10 } 11 }
定义一个参数为Node返回值为空的函数,作为Iterate的第二个参数。为省去函数定义,可以使用匿名函数。
1 var it = new NodeIterator(); 2 it.Iterate(nodes["China"], delegate(node node) 3 { 4 Console.WriteLine(node.Name); 5 } 6 );
使用lambda更简洁。
1 it.Iterate(nodes["China"], (node) => 2 { 3 Console.WriteLine(node.name); 4 } 5 );
大多编程语言都支持这种方式。对于脚本语言来说,使用起来更方便。以Python为例。
1 def print_node(node): 2 print node.name 3 4 def iterate(node, fn): 5 fn(node) 6 for child in node.children: 7 iterate(child, fn) 8 9 iterate(nodes["China"], print_node)
不过Python不支持匿名函数,lambda也仅仅支持单行表达式。所以往往还需要函数定义。而JavaScript是可以使用匿名函数的。
1 iterate(nodes["China"], function(node){ 2 console.log(node.name); 3 });
对于C/C++来说就得用到函数指针和仿函数了。
- 观察者模式
就是GoF 23种模式之一。
1 public interface IObserver 2 { 3 void Notify(Node node); 4 } 5 6 public class NodeObserver : IObserver 7 { 8 public void Notify(Node node) 9 { 10 Console.WriteLine(node.Name); 11 } 12 } 13 14 public class NodeIterator 15 { 16 public NodeIterator() 17 { 18 this.Observers = new List<IObserver>(); 19 } 20 21 public IList<IObserver> Observers {get; set;} 22 23 public void Iterate(Node node) 24 { 25 foreach (var observer in this.Observers) 26 { 27 observer.Notify(node); 28 } 29 30 foreach (var child in node.Children) 31 { 32 this.ObserverIterate(child); 33 } 34 } 35 }
代码有点多,但也支持了遍历时调用多个函数。
1 var it = new NodeIterator(); 2 3 it.Observers.Add(new NodeObserver()); 4 it.Observers.Add(new NodeObserver()); 5 6 it.Iterate(nodes["China"]);
因为添加了两个观察者,所以上面代码会打印两次节点名称。
- 迭代器
迭代器可以使数据看起来更像一个集合.
1 public IEnumerable<Node> Iterate(Node node) 2 { 3 yield return node; 4 foreach (var child in node.Children) 5 { 6 foreach (var grandChild in this.Iterate(child)) 7 { 8 yield return grandChild; 9 } 10 } 11 }
因为Iterate是有返回值的,并由这个IEnumerable返回值完成遍历的。所以在递归调用中,需要多一个foreach。
1 foreach (var note in it.Iterate(nodes["China"])) 2 { 3 Console.WriteLine(node.Name); 4 } 5 6 IEnumerator<Node> iter = it.Iterate(nodes["China"]).GetEnumerator(); 7 while (iter.MoveNext()) 8 { 9 Console.WriteLine(iter.Current.Name); 10 }
上面是两种调用方式。不仅使用起来更简洁了,而且遍历算法和操作节点的函数完全解耦了。在函数参数方式中,遍历算法需要知道操作节点函数。在观察者模式中,遍历算法也是需要知道操作接口的。
Python实现迭代器也很容易。
1 def iterate(node): 2 for child in node.children: 3 yield child 4 for item in iterate(child): 5 yield item 6 7 for node in iterate(nodes["China"]): 8 print node.name
- 事件
事件或者其他消息机制也能很好地完成复用。
1 public delegate void IterateEventHandler(object sender, IterateEventArgs e); 2 3 public class IterateEventArgs : EventArgs 4 { 5 public IterateEventArgs(Node node) 6 { 7 this.Target = node; 8 } 9 10 public Node Target {get; set;} 11 } 12 13 public class NodeIterator 14 { 15 public event IterateEventHandler Iterated; 16 17 public void Iterate(Node node) 18 { 19 this.Iterated(this, new IterateEventArgs(node)); 20 foreach (var child in node.Children) 21 { 22 Iterate(child); 23 } 24 } 25 }
绑定事件处理函数,这里同样使用最方便的lambda为例。
1 var it = new NoteIterator(); 2 3 it.Iterated += new IterateEventHandler((sender, e) => 4 { 5 Console.WriteLine(e.Target.Name); 6 } 7 ); 8 9 it.Iterate(nodes["China"]);
还有其他方式吗?但那种遍历后,把所有节点放到一个集合中的方式就不用列举了。