zoukankan      html  css  js  c++  java
  • 遍历算法的重用

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

    假设用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"]);

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

  • 相关阅读:
    ubuntu+VS code+launch.json+task.json
    C++——运行时类型识别RTTI
    C++——模板
    C++——class类和struct结构体的唯一区别
    C++——右值引用
    C++——智能指针
    身份证号码格式检测
    PHP获取图片主题颜色
    PHP 压缩图片质量
    redis3.2装完后 其它机子访问爆protocol error, got 'n' as reply type byte
  • 原文地址:https://www.cnblogs.com/cypine/p/3318255.html
Copyright © 2011-2022 走看看