`
FengShen_Xia
  • 浏览: 279419 次
  • 性别: Icon_minigender_1
  • 来自: 东方水城
社区版块
存档分类
最新评论

用迭代器与组合模式对树进行遍历

    博客分类:
  • Java
阅读更多

相信大家对迭代器模式还是比较熟悉的,在Java的集合中有比较多的应用。比如你想使用迭代器遍历一个集合,代码可能是这样:

   1. for (Iterator it = collection.iterator(); it.hasNext();)
   2. {
   3.     doSomething(it.next());
   4. }

 

迭代器的作用在于对数据的遍历与数据的内部表示进行了分离。通过迭代器,你知道调用hasNext()来确认是否还有下一个元素。如果有,那么就可以调用next()取得下一个元素。至于数据内部如何表示,我们并不关心。我们关心的只是能以一种预先定义的顺序来访问每个元素。

 

组合模式用来表示一个节点内部包含若干个其它的节点,而被包含的节点同样也可以再包含另外的节点。因此是一种递归的结构。不包含其他节点的节点叫做叶子节点,这通常是递归的终结点。树形结构比较适合用来说明组合模式。

 

假设我们用Node接口表示一个节点。可以往这个节点上添加节点,也可以获得这个节点的迭代器,用来遍历节点中的其他节点。

   1. public interface Node {
   2.     void addNode(Node node);
   3.     Iterator<Node> iterator();
   4. }

 

抽象类AbstractNode实现这个接口,并不做多余的事情。只是让这个节点有个名字,以区分于其他节点。

   1. public abstract class AbstractNode implements Node {
   2.     protected String name;
   3.

   4.     protected AbstractNode(String name)
   5.     {
   6.         this.name = name;
   7.     }
   8.

   9.     public String toString()
  10.     {
  11.         return name;
  12.     }
  13. }

 

接下来,是表示两种不同类型的节点,树枝节点和叶子节点。它们都继承自AbstractNode,不同的是叶子节点没有子节点。

   1. public class LeafNode extends AbstractNode {
   2.

   3.     public LeafNode(String name) {
   4.         super(name);
   5.     }
   6.

   7.     public void addNode(Node node) {
   8.         throw new UnsupportedOperationException("Can't add a node to leaf.");
   9.     }
  10.

  11.     public Iterator<Node> iterator() {
  12.         return new NullIterator<Node>();
  13.     }
  14. }

 

可以看到,试图往叶子节点添加节点会导致异常抛出。而获取叶子节点的迭代器,则返回……NullIterator。这个是什么?因为叶子节点没有子节点,所以叶子节点的迭代器没有什么意义。我们可以简单地返回null,但是那样处理节点的代码就需要区分是叶子节点还是其他节点。如果我们返回的是一个NullIterator,一个看起来和其他的迭代器差不多,但是hasNext()永远返回false,而next()永远返回null的迭代器,那么叶子节点的遍历也就和其他节点没有什么两样了。

 

接下来是树枝节点:

   1. public class BranchNode extends AbstractNode {
   2.     public BranchNode(String name) {
   3.         super(name);
   4.     }
   5.

   6.     private Collection<Node> childs = new ArrayList<Node>();
   7.

   8.     public void addNode(Node node) {
   9.         childs.add(node);
  10.     }
  11.

  12.     public Iterator<Node> iterator() {
  13.         return childs.iterator();
  14.     }
  15. }

 

树枝节点可以添加子节点,叶子节点或者另外一个树枝节点。但是我们注意到,这里的迭代器只是简单地返回了该节点直属子节点集合的迭代器。什么意思呢?这意味着如果你通过这个迭代器去遍历这个节点,你能获得是只是所有直接添加到这个节点上的子节点。要想递归地遍历子节点的子节点,以及子节点的子节点的子节点……我们就需要一个特殊的迭代器。

 

用一个深度优先遍历的迭代器怎么样?所谓深度优先,通俗的讲就是沿着一条路走下去,直到走不通为止,再回过头来看看有没有别的路可走。

 

   1. public class DepthFirstIterator implements Iterator<Node> {
   2.

   3.     private Stack<Iterator<Node>> stack = new Stack<Iterator<Node>>();
   4.

   5.     public DepthFirstIterator(Iterator<Node> it) {
   6.         stack.push(it);
   7.     }
   8.

   9.     public boolean hasNext() {
  10.         if (stack.isEmpty()) {
  11.             return false;
  12.         } else {
  13.             Iterator<Node> it = stack.peek();
  14.             if (it.hasNext()) {
  15.                 return true;
  16.             } else {
  17.                 stack.pop();
  18.                 return hasNext();
  19.             }
  20.         }
  21.     }
  22.

  23.     public Node next() {
  24.         if (hasNext()) {
  25.             Iterator<Node> it = stack.peek();
  26.             Node next = it.next();
  27.             if (next instanceof BranchNode) {
  28.                 stack.push(next.iterator());
  29.             }
  30.             return next;
  31.         } else {
  32.             return null;
  33.         }
  34.     }
  35.

  36.     public void remove() {
  37.         throw new UnsupportedOperationException("Can't remove node, yet");
  38.     }
  39. }

 

代码不是很长,理解起来可比较费劲。这不仅是因为我很懒没有写注释,更是因为有可恶的递归在。先从构造函数看起,一个包含了迭代器的构造函数,也就是说,这是个迭代器的迭代器……这该怎么理解呢?可以把它理解成树枝节点迭代器的一个改良的迭代器。树枝节点的迭代器只知道如何找到第一级子节点,而这个迭代器则可以沿着子节点一直寻找下去,直到找到叶子节点,然后再返回来继续寻找。好吧,说起来简单,怎么做呢?

 

先看如何找到“下一个”节点吧,看next()方法:

如果存在下一个节点,那么开始下一个节点的寻找之旅。否则返回null结束。

首先,通过树枝节点自带的迭代器,找到树枝节点的第一个子节点,这个子节点就是我们要找的“第一个” 节点。这很简单,对吧?那么下一个节点是哪一个?这取决于我们找到的第一个节点是什么类型,如果是叶子节点,那么很简单,下一个节点跟树枝节点迭代器定义的下一个节点一样,也就是树枝节点的第二个直属子节点。如果是树枝节点呢?这个时候它将被当成一个新的起点,被压入堆栈,下一次遍历将从这个节点开始重复上面的逻辑,也就是递归。听起来并不复杂,不是吗?

 

让我们来一个例子,如果我们要遍历这样一棵树

# /**
#      * Create a tree like this 
#      *          Root 
#      *          /  |   \ 
#      *         A  B    C
#      *        /            \
#      *       D             F
#      *      / 
#      *     E
#      * 
#      * @return The tree
#      */
#     static Node createTree() {
#         Node root = new BranchNode("Root");
#         Node a = new BranchNode("A");
#         Node b = new LeafNode("B");
#         Node c = new BranchNode("C");
#         Node d = new BranchNode("D");
#         Node e = new LeafNode("E");
#         Node f = new LeafNode("F");
#
#         a.addNode(d);
#         d.addNode(e);
#
#         c.addNode(f);
#
#         root.addNode(a);
#         root.addNode(b);
#         root.addNode(c);
#         return root;
#     }

 

从根节点Root开始遍历,第一个子节点,也就是Root自己的第一个直属子节点,是A。下一个呢?因为A是一个树枝节点,所以我们把它先压入堆栈。下一次从A开始,我们可以把从A开始的子节点遍历看成一次全新的遍历,所以A的第一个子节点是什么呢?D!很简单不是?然后是E。因为E没有子节点,所以我们返回找D的下一个子节点,但是D除了E是它的子节点之外,没有另外的子节点了。所以D也没有子节点了,又返回到A。A也没有多余的子节点了,所以这个时候轮到B……

所以,最终的顺序是Root -> A -> D -> E -> B -> C -> F。

 

回过头来看看hasNext()做了什么。还记得吗?我们把每一个遇到的树枝节点压入堆栈,当堆栈中不存在任何的树枝节点时,遍历就完成了。如果有,我们就取出一个,看它是不是还有子节点,如果有,那么我们就说还有下一个节点。如果没有了,那我们就取出堆栈中的下一个树枝节点,并以这个树枝节点为起点,看是否存在下一个节点。

 

试试这个迭代器威力如何:

   1. static void depthFirstIterate(Node tree) {
   2.         doSomething(tree);
   3.         for (Iterator<Node> it = new DepthFirstIterator(tree.iterator()); it.hasNext();) {
   4.             doSomething(it.next());
   5.         }
   6.     }

 

如果doSomething(Node node)只是简单地打印这个节点,像这样:

   1. static void doSomething(Node node) {
   2.         System.out.println(node);
   3.     }

 

那么你可以看到前面所述的顺序被打印出来 Root -> A -> D -> E -> B -> C -> F。当然,没有箭头,而且是分行显示的。

 

好的,这看起来确实不错,那么广度优先遍历呢?所谓广度优先,通俗来讲就是层层推进。首先遍历所有的第一级子节点,然后是第二层,第三层……结果就像是这样:Root -> A -> B -> C -> D -> F -> E

听起来更加简单,是不是?事实上做起来并不简单,除非你已经正确理解了上面深度优先遍历。

 

如果你理解了深度优先遍历,那么广度优先遍历和深度优先唯一不同的地方就是树枝节点的存取顺序。在深度优先遍历中,树枝节点使用堆栈,存取顺序是后进先出。先就是说,最后遇到(也就是后进)的树枝节点先拿出来用(就像插队一样,不得不承认这有点不公平)。那么,我们最先遇到的树枝节点是Root自己,然后是A,最后是D(不是E,因为E不是树枝节点)。根据后进先出的原则,我们先把D拿出来遍历,最终得到D的子节点是E。然后是A,最后才是Root,所以Root的第二个子节点B会在Root的第一个子节点遍历完成之后才能遍历到。

 

所以,只要我们将堆栈换成公平的强力支持者,先进先出的队列(Queue),问题就解决了:

因为Root最先进入队列,所以它的所有直属子节点会被先遍历,然后才轮到A,然后是C,然后是D。所以最终顺序会是这样:

Root -> A -> B -> C -> D -> F -> E

 

广度优先代码:

   1. public class BreadthFirstIterator implements Iterator<Node> {
   2.

   3.     private Queue<Iterator<Node>> queue = new LinkedList<Iterator<Node>>();
   4.

   5.     public BreadthFirstIterator(Iterator<Node> it) {
   6.         queue.offer(it);
   7.     }
   8.

   9.     public boolean hasNext() {
  10.         if (queue.isEmpty()) {
  11.             return false;
  12.         } else {
  13.             Iterator<Node> it = queue.peek();
  14.             if (it.hasNext()) {
  15.                 return true;
  16.             } else {
  17.                 queue.poll();
  18.                 return hasNext();
  19.             }
  20.         }
  21.     }
  22.

  23.     public Node next() {
  24.         if (hasNext()) {
  25.             Iterator<Node> it = queue.peek();
  26.             Node next = it.next();
  27.             if (next instanceof BranchNode) {
  28.                 queue.offer(next.iterator());
  29.             }
  30.             return next;
  31.         } else {
  32.             return null;
  33.         }
  34.     }
  35.

  36.     public void remove() {
  37.         throw new UnsupportedOperationException("Can't remove node, yet");
  38.     }
  39. }

 

可以看到代码和深度优先遍历几乎完全一样,除了把堆栈(Stack)换成了队列(Queue)

 

参考了《HeadFirst设计模式》

分享到:
评论

相关推荐

    组合模式二叉树,前序、中序、后续,迭代器模式访问遍历

    在这个主题中,我们主要探讨了如何利用组合模式(Composite Pattern)构建二叉树,并通过迭代器模式(Iterator Pattern)来实现对树的遍历,包括前序、中序和后序遍历。这些是设计模式中的经典应用,对于理解和掌握...

    Head First 设计模式 (九) 迭代器与组合模式(Iterator & Composite pattern) C++实现

    迭代器模式(Iterator Pattern)和组合模式(Composite Pattern)是设计模式中的两种重要结构型模式,它们在软件设计中有着广泛的应用。这两种模式都属于GoF(Gang of Four)设计模式,旨在解决特定的问题,提升代码...

    树的构件与遍历---使用组合模式与迭代模式

    通过组合模式,我们可以以统一的方式对待单个节点和包含多个节点的分支,简化了对树结构的操作。例如,在Java中,我们可以创建一个抽象的`Component`类,表示树中的通用节点,然后创建具体节点类如`Leaf`(叶子节点...

    设计模式之迭代器模式(Iterator)

    在实际应用中,迭代器模式常用于各种容器(如数组、链表、树等)的遍历,以及模板方法模式、策略模式等设计模式的组合。例如,在Java的`Collections`类中,有许多方法(如`sort()`、`shuffle()`)都依赖于迭代器来...

    JAVA 设计模式 工厂模式 代理模式 迭代模式 责任链模式 源码

    3. **迭代器模式**:迭代器模式是行为型设计模式,它允许我们遍历集合对象的元素而无需暴露其底层表示。迭代器模式提供了一种方法顺序访问聚合对象中的元素,而无需暴露其底层结构。在Java中,迭代器模式广泛应用于...

    Java 23种设计模式20迭代器模式.pdf

    ### Java 23种设计模式之...- **组合迭代器**:可以组合多个迭代器,用于遍历由多个聚合对象组成的复合结构。 通过上述内容,我们可以看到迭代器模式不仅解决了访问聚合对象的问题,还提高了代码的可读性和可维护性。

    2 迭代器模式-MOOC课程内容.pdf

    而与Composite(组合)模式的联系在于迭代器通常用于递归地遍历组合结构。 迭代器模式在Java中有着广泛的应用。Java内置了对迭代器模式的支持,其中java.util.Enumeration和java.util.Iterator接口就是迭代器接口。...

    组合模式-------树形模式

    - 组合模式与其他设计模式(如装饰器模式、代理模式等)的区别和结合使用。 在使用组合模式时,我们需要注意: - 避免在客户端代码中直接依赖于具体类,而是依赖于组件接口,以保持代码的灵活性和可扩展性。 - 为了...

    C#面向对象设计模式纵横谈-行为模式(合集)

    4. **迭代器模式与其他模式的交互**:讨论迭代器模式如何与工厂模式、组合模式等其他设计模式结合使用,以增强系统的灵活性和扩展性。 5. **实例分析**:通过具体的C#代码示例展示如何实现和使用迭代器模式,可能...

    Rust编程艺术:迭代器与闭包的精妙运用

    ### Rust 编程艺术:迭代器与闭包的精妙运用 #### 一、Rust 语言概述 Rust 是一种高性能的系统级编程语言,它由 Mozilla 研究院发起,Graydon Hoare 设计,并于 2010 年首次发布。Rust 的设计目标在于提供内存安全...

    组合模式的实践demo

    - **迭代器**:为了遍历整个组合,我们可以使用迭代器模式。这使得客户端代码能够轻松地访问所有组件,而无需知道它们是叶子还是组合。迭代器提供了一种顺序访问聚合元素的方式,而无需暴露它们的底层表示。 - **...

    遍历所有文件

    - 在高级编程语言中,遍历文件常被封装为迭代器模式。例如,Python的`os.walk`函数就提供了一个方便的迭代器,它可以遍历指定路径下的所有文件和子目录,包括子目录中的文件。 6. 并行遍历: - 对于大型文件系统...

    designPattern4:设计模式:组合模式,公司管理系统,迭代器模式(买车票)

    组合模式帮助我们构建和操作对象的树状结构,公司管理系统可以结合多种设计模式来实现灵活的业务逻辑,而迭代器模式则简化了聚合对象的遍历。学习和运用这些模式,能显著提高Java程序员的代码质量和效率。在实际项目...

    C++设计模式编程中的迭代器模式应用解析

    迭代器模式应该是最为熟悉的模式了,最简单的证明就是我在实现组合模式、享元模式、观察者模式中就直接用到了 STL 提供的迭代器来遍历 Vector 或者 List数据结构。 迭代器模式也正是用来解决对一个聚合对象的遍历...

    JAVA设计模式学习10——组合模式

    在实际应用中,组合模式通常与迭代器模式一起使用,允许客户端遍历整个树形结构。通过定义一个统一的迭代接口,客户端可以遍历所有的组件,无论它们是叶子节点还是组合节点。 标签中提到的“源码”和“工具”可能指...

    《设计模式实训教程》【PPT+类图与代码+样章】

    6.2.6访问者模式、组合模式与迭代器模式联用 6.3综合实例实训 6.3.1多人联机射击游戏 6.3.2数据库同步系统 6.4实训练习 附录A参考答案 A.1第1章实训练习参考答案 A.2第2章实训练习参考答案 A.3第3章实训练习...

    设计模式Java版

    组合模式将对象组合成树形结构,使得用户可以对单个对象和组合对象统一处理,而外观模式为子系统提供了更简单的接口。 行为型模式中,策略模式定义了一系列算法,并使它们可以相互替换,让算法独立于使用它的客户。...

    Ruby设计模式(中文版+英文版).pdf

    本书以通俗易懂的方式介绍了Ruby设计模式,主要包括Ruby概述、使用模板方法变换算法、使用策略替换算法、通过观察器保持协调、通过迭代器遍历集合、使用命令模式完成任务、使用适配器填补空隙、使用装饰器改善对象、...

    java设计模式

    21.4.3 组合模式的遍历 21.5 最佳实践 第22章 观察者模式 22.1 韩非子身边的卧底是谁派来的 22.2 观察者模式的定义 22.3 观察者模式的应用 22.3.1 观察者模式的优点 22.3.2 观察者模式的缺点 22.3.3 观察者模式的...

    design-pattern-java.pdf

    树形结构的处理——组合模式(二) 树形结构的处理——组合模式(三) 树形结构的处理——组合模式(四) 树形结构的处理——组合模式(五) 装饰模式-Decorator Pattern 扩展系统功能——装饰模式(一) 扩展系统...

Global site tag (gtag.js) - Google Analytics