`
FengShen_Xia
  • 浏览: 273166 次
  • 性别: 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模式构成二叉树,并用迭代器模式封装访问,前序、中序和后序的遍历。JAVA 编写。 Main中直接运行

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

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

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

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

    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 扩展系统功能——装饰模式(一) 扩展系统...

    Java设计模式 版本2

    对象的克隆——原型模式,复杂对象的组装与创建——建造者模式,不兼容结构的协调——适配器模式,处理多维度变化——桥接模式,树形结构的处理——组合模式,扩展系统功能——装饰模式,深入浅出外观模式,实现对象...

    23种设计模式入门到精通详解.txt

    组合模式:将对象组合成树形结构以表示“”部分-整体“”的层次结构。 装饰模式:动态的给对象添加新的功能。 代理模式:为其他对象提供一个代理以便控制这个对象的访问。 亨元(蝇量)模式:通过共享技术来有效...

    《设计模式》中文版(23个设计模式的介绍与运用)

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

    OBJECTIVE-C编程之道 IOS设计模式解析电子书+源代码

    组合13.1 何为组合模式13.2 何时使用组合模式13.3 理解TouchPainter中Mark的使用13.4 在Cocoa Touch框架中使用组合模式13.5 总结第14章 迭代器14.1 何为迭代器模式14.2 何时使用迭代器模式14.3 在Cocoa Touch框架中...

    设计模式--C++

    2.2.3 组合模式 272.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor 和 Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持...

    设计模式可复用面向对象软件的基础.zip

    5.4 ITERATOR(迭代器)—对象行为型 模式 171 5.5 MEDIATOR(中介者)—对象行为型 模式 181 5.6 MEMENTO(备忘录)—对象行为型 模式 188 5.7 OBSERVER(观察者)—对象行为型 模式 194 5.8 STATE(状态)—对象...

    GOLF设计模式(C++语言版)

    5.4 ITERATOR(迭代器)—对象行为型 模式 171 5.5 MEDIATOR(中介者)—对象行为型 模式 181 5.6 MEMENTO(备忘录)—对象行为型 模式 188 5.7 OBSERVER(观察者)—对象行为型 模式 194 5.8 STATE(状态)...

    设计模式(.PDF)

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

    Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides23种设计模式

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

    《国外写的,翻译版本》设计模式

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

    设计模式文档

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

    设计模式 design pattern

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

    JAVA经典设计模式大全

    5.4 ITERATOR(迭代器)—对象行为型 模式 171 5.5 MEDIATOR(中介者)—对象行为型 模式 181 5.6 MEMENTO(备忘录)—对象行为型 模式 188 5.7 OBSERVER(观察者)—对象行为型 模式 194 5.8 STATE(状态)...

    设计模式___

    2.2.3 组合模式 27 2.3 格式化 27 2.3.1 封装格式化算法 27 2.3.2 Compositor和Composition 27 2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种...

Global site tag (gtag.js) - Google Analytics