用迭代器與組合模式對樹進行遍歷

相信大家對迭代器模式還是比較熟悉的,在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.     protected AbstractNode(String name)
  4.     {
  5.         this.name = name;
  6.     }
  7.     public String toString()
  8.     {
  9.         return name;
  10.     }
  11. }

接下來,是表示兩種不同類型的節點,樹枝節點和葉子節點。它們都繼承自AbstractNode,不同的是葉子節點沒有子節點。

  1. public class LeafNode extends AbstractNode {
  2.     public LeafNode(String name) {
  3.         super(name);
  4.     }
  5.     public void addNode(Node node) {
  6.         throw new UnsupportedOperationException("Can't add a node to leaf.");
  7.     }
  8.     public Iterator<Node> iterator() {
  9.         return new NullIterator<Node>();
  10.     }
  11. }

可以看到,試圖往葉子節點添加節點會導致異常拋出。而獲取葉子節點的迭代器,則返回……NullIterator。這個是什麼?因爲葉子節點沒有子節點,所以葉子節點的迭代器沒有什麼意義。我們可以簡單地返回null,但是那樣處理節點的代碼就需要區分是葉子節點還是其他節點。如果我們返回的是一個NullIterator,一個看起來和其他的迭代器差不多,但是hasNext()永遠返回false,而next()永遠返回null的迭代器,那麼葉子節點的遍歷也就和其他節點沒有什麼兩樣了。

 

接下來是樹枝節點:

  1. public class BranchNode extends AbstractNode {
  2.     public BranchNode(String name) {
  3.         super(name);
  4.     }
  5.     private Collection<Node> childs = new ArrayList<Node>();
  6.     public void addNode(Node node) {
  7.         childs.add(node);
  8.     }
  9.     public Iterator<Node> iterator() {
  10.         return childs.iterator();
  11.     }
  12. }

樹枝節點可以添加子節點,葉子節點或者另外一個樹枝節點。但是我們注意到,這裏的迭代器只是簡單地返回了該節點直屬子節點集合的迭代器。什麼意思呢?這意味着如果你通過這個迭代器去遍歷這個節點,你能獲得是隻是所有直接添加到這個節點上的子節點。要想遞歸地遍歷子節點的子節點,以及子節點的子節點的子節點……我們就需要一個特殊的迭代器。

 

用一個深度優先遍歷的迭代器怎麼樣?所謂深度優先,通俗的講就是沿着一條路走下去,直到走不通爲止,再回過頭來看看有沒有別的路可走。

 

  1. public class DepthFirstIterator implements Iterator<Node> {
  2.     private Stack<Iterator<Node>> stack = new Stack<Iterator<Node>>();
  3.     public DepthFirstIterator(Iterator<Node> it) {
  4.         stack.push(it);
  5.     }
  6.     public boolean hasNext() {
  7.         if (stack.isEmpty()) {
  8.             return false;
  9.         } else {
  10.             Iterator<Node> it = stack.peek();
  11.             if (it.hasNext()) {
  12.                 return true;
  13.             } else {
  14.                 stack.pop();
  15.                 return hasNext();
  16.             }
  17.         }
  18.     }
  19.     public Node next() {
  20.         if (hasNext()) {
  21.             Iterator<Node> it = stack.peek();
  22.             Node next = it.next();
  23.             if (next instanceof BranchNode) {
  24.                 stack.push(next.iterator());
  25.             }
  26.             return next;
  27.         } else {
  28.             return null;
  29.         }
  30.     }
  31.     public void remove() {
  32.         throw new UnsupportedOperationException("Can't remove node, yet");
  33.     }
  34. }

代碼不是很長,理解起來可比較費勁。這不僅是因爲我很懶沒有寫註釋,更是因爲有可惡的遞歸在。先從構造函數看起,一個包含了迭代器的構造函數,也就是說,這是個迭代器的迭代器……這該怎麼理解呢?可以把它理解成樹枝節點迭代器的一個改良的迭代器。樹枝節點的迭代器只知道如何找到第一級子節點,而這個迭代器則可以沿着子節點一直尋找下去,直到找到葉子節點,然後再返回來繼續尋找。好吧,說起來簡單,怎麼做呢?

 

先看如何找到“下一個”節點吧,看next()方法:

如果存在下一個節點,那麼開始下一個節點的尋找之旅。否則返回null結束。

首先,通過樹枝節點自帶的迭代器,找到樹枝節點的第一個子節點,這個子節點就是我們要找的“第一個”節點。這很簡單,對吧?那麼下一個節點是哪一個?這取決於我們找到的第一個節點是什麼類型,如果是葉子節點,那麼很簡單,下一個節點跟樹枝節點迭代器定義的下一個節點一樣,也就是樹枝節點的第二個直屬子節點。如果是樹枝節點呢?這個時候它將被當成一個新的起點,被壓入堆棧,下一次遍歷將從這個節點開始重複上面的邏輯,也就是遞歸。聽起來並不複雜,不是嗎?

 

讓我們來一個例子,如果我們要遍歷這樣一棵樹

  1. /**
  2.      * Create a tree like this 
  3.      *          Root 
  4.      *          /    |   / 
  5.      *         A  B  C
  6.      *        /            /
  7.      *       D            F
  8.      *      / 
  9.      *     E
  10.      * 
  11.      * @return The tree
  12.      */
  13.     static Node createTree() {
  14.         Node root = new BranchNode("Root");
  15.         Node a = new BranchNode("A");
  16.         Node b = new LeafNode("B");
  17.         Node c = new BranchNode("C");
  18.         Node d = new BranchNode("D");
  19.         Node e = new LeafNode("E");
  20.         Node f = new LeafNode("F");
  21.         a.addNode(d);
  22.         d.addNode(e);
  23.         c.addNode(f);
  24.         root.addNode(a);
  25.         root.addNode(b);
  26.         root.addNode(c);
  27.         return root;
  28.     }

從根節點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.     private Queue<Iterator<Node>> queue = new LinkedList<Iterator<Node>>();
  3.     public BreadthFirstIterator(Iterator<Node> it) {
  4.         queue.offer(it);
  5.     }
  6.     public boolean hasNext() {
  7.         if (queue.isEmpty()) {
  8.             return false;
  9.         } else {
  10.             Iterator<Node> it = queue.peek();
  11.             if (it.hasNext()) {
  12.                 return true;
  13.             } else {
  14.                 queue.poll();
  15.                 return hasNext();
  16.             }
  17.         }
  18.     }
  19.     public Node next() {
  20.         if (hasNext()) {
  21.             Iterator<Node> it = queue.peek();
  22.             Node next = it.next();
  23.             if (next instanceof BranchNode) {
  24.                 queue.offer(next.iterator());
  25.             }
  26.             return next;
  27.         } else {
  28.             return null;
  29.         }
  30.     }
  31.     public void remove() {
  32.         throw new UnsupportedOperationException("Can't remove node, yet");
  33.     }
  34. }

可以看到代碼和深度優先遍歷幾乎完全一樣,除了把堆棧(Stack)換成了隊列(Queue)

 

參考了《HeadFirst設計模式》,但遺憾的是那裏面的示例代碼是錯誤的

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章