遍历算法的重用

如何重用实现的遍历算法,这是我在面试中常问的一个问题。其实问题并不难,主要考察对语言特性的掌握,以及处理这种常见场景的经验。或许是交谈中没有实例的原因,满意的答案不多。

假设用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 }

这个例子中的迭代算法很简单,仅使用递归来遍历所有的子节点。实际情况会复杂,比如同时要遍历其兄弟节点。如果我们还需要遍历节点做其他事情,那就要考虑如何重用这个遍历算法。

  1. 函数参数
    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++来说就得用到函数指针和仿函数了。

  2. 观察者模式
    就是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"]);

    因为添加了两个观察者,所以上面代码会打印两次节点名称。

  3. 迭代器
    迭代器可以使数据看起来更像一个集合.
     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
  4. 事件
    事件或者其他消息机制也能很好地完成复用。
     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"]);

还有其他方式吗?但那种遍历后,把所有节点放到一个集合中的方式就不用列举了。

posted @ 2013-09-15 01:41  cypine  阅读(305)  评论(0编辑  收藏  举报