2012-01-11 60 views
4

有人可以給我建議如何創建一個遞歸版本的GetEnumerator()? 衆所周知的Towers of Hanoi problem可能會作爲一個例子,可以與我的實際問題相媲美。一個簡單的算法,以示對高n個磁盤組成的堆疊中的所有動作是:C#如何使遞歸版本的GetEnumerator()

void MoveTower0 (int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    MoveTower0 (n - 1, start, temp, finish); 
    Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
    MoveTower0 (n - 1, temp, finish, start); 
    } 
} 

我真正想要做的是建立實現IEnumerable類HanoiTowerMoves並且能夠讓我按如下方式遍歷所有移動:

foreach (Move m in HanoiTowerMoves) Console.WriteLine (m); 

走向的GetEnumerator()實現的第一步,似乎擺脫了MoveTower參數。這可以通過使用堆棧來輕鬆完成。我還介紹了一個將Move參數組合成單個變量的Move類。

class Move 
{ 
    public int N { private set; get; } 
    public Needle Start { private set; get; } 
    public Needle Finish { private set; get; } 
    public Needle Temp { private set; get; } 

    public Move (int n, Needle start, Needle finish, Needle temp) 
    { 
    N = n; 
    Start = start; 
    Finish = finish; 
    Temp = temp; 
    } 

    public override string ToString() 
    { 
    return string.Format ("Moving disk from {0} to {1}", Start, Finish); 
    } 
} 

現在MoveTower可以如下改寫:

varStack.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
MoveTower1(); 

朝着一個迭代版本的下一步是實現類:

void MoveTower1() 
{ 
    Move m = varStack.Pop(); 

    if (m.N > 0) 
    { 
    varStack.Push (new Move (m.N - 1, m.Start, m.Temp, m.Finish)); 
    MoveTower1(); 
    Console.WriteLine (m); 
    varStack.Push (new Move (m.N - 1, m.Temp, m.Finish, m.Start)); 
    MoveTower1(); 
    } 
} 

如下這個版本必須調用:

class HanoiTowerMoves : IEnumerable<Move> 
{ 
    Stack<Move> varStack; 
    int n; // number of disks 

    public HanoiTowerMoves (int n) 
    { 
    this.n = n; 
    varStack = new Stack<Move>(); 
    } 

    public IEnumerator<Move> GetEnumerator() 
    { 
    // ???????????????????????????? } 

    // required by the compiler: 
    IEnumerator IEnumerable.GetEnumerator() 
    { 
    return GetEnumerator(); 
    } 
} 

現在對我來說最大的問題是:GetEnumerator()的主體是什麼樣的? 有人能爲我解開這個謎嗎?

下面是我創建的控制檯應用程序的Program.cs代碼。

using System; 
using System.Collections.Generic; 
using System.Collections; 

/* Towers of Hanoi 
* =============== 
* Suppose you have a tower of N disks on needle A, which are supposed to end up on needle B. 
* The big picture is to first move the entire stack of the top N-1 disks to the Temp needle, 
* then move the N-th disk to B, then move the Temp stack to B using A as the new Temp needle. 
* This is reflected in the way the recursion is set up. 
*/ 

namespace ConsoleApplication1 
{ 
    static class main 
    { 
    static void Main (string [] args) 
    { 
     int n; 
     Console.WriteLine ("Towers of Hanoi"); 

     while (true) 
     { 
     Console.Write ("\r\nEnter number of disks: "); 

     if (!int.TryParse (Console.ReadLine(), out n)) 
     { 
      break; 
     } 

     HanoiTowerMoves moves = new HanoiTowerMoves (n); 
     moves.Run (1); // algorithm version number, see below 
     } 
    } 
    } 

    class Move 
    { 
    public int N { private set; get; } 
    public Needle Start { private set; get; } 
    public Needle Finish { private set; get; } 
    public Needle Temp { private set; get; } 

    public Move (int n, Needle start, Needle finish, Needle temp) 
    { 
     N = n; 
     Start = start; 
     Finish = finish; 
     Temp = temp; 
    } 

    public override string ToString() 
    { 
     return string.Format ("Moving disk from {0} to {1}", Start, Finish); 
    } 
    } 

    enum Needle { A, B, Temp } 

    class HanoiTowerMoves : IEnumerable<Move> 
    { 
    Stack<Move> varStack; 
    int n;   // number of disks 

    public HanoiTowerMoves (int n) 
    { 
     this.n = n; 
     varStack = new Stack<Move>(); 
    } 

    public void Run (int version) 
    { 
     switch (version) 
     { 
     case 0: // Original version 
      MoveTower0 (n, Needle.A, Needle.B, Needle.Temp); 
      break; 

     case 1: // No parameters (i.e. argument values passed via stack) 
      varStack.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
      MoveTower1(); 
      break; 

     case 2: // Enumeration 
      foreach (Move m in this) 
      { 
      Console.WriteLine (m); 
      } 

      break; 
     } 
    } 

    void MoveTower0 (int n, Needle start, Needle finish, Needle temp) 
    { 
     if (n > 0) 
     { 
     MoveTower0 (n - 1, start, temp, finish); 
     Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
     MoveTower0 (n - 1, temp, finish, start); 
     } 
    } 

    void MoveTower1() 
    { 
     Move m = varStack.Pop(); 

     if (m.N > 0) 
     { 
     varStack.Push (new Move (m.N - 1, m.Start, m.Temp, m.Finish)); 
     MoveTower1(); 
     Console.WriteLine (m); 
     varStack.Push (new Move (m.N - 1, m.Temp, m.Finish, m.Start)); 
     MoveTower1(); 
     } 
    } 

    public IEnumerator<Move> GetEnumerator() 
    { 
     yield break; // ???????????????????????????? 
    } 

    /* 
     void MoveTower1() 
     { 
     Move m = varStack.Pop(); 

     if (m.N > 0) 
     { 
      varStack.Push (new Move (m.N - 1, m.Start, m.Temp, m.Finish)); 
      MoveTower1(); 
      Console.WriteLine (m); ? yield return m; 
      varStack.Push (new Move (m.N - 1, m.Temp, m.Finish, m.Start)); 
      MoveTower1(); 
     } 
     } 
    */ 

    // required by the compiler: 
    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return GetEnumerator(); 
    } 
    } 
} 
+1

我不明白遞歸調用GetEnumerator的意義。只需編寫一個遞歸函數,即調用這個方法,並不難。 – 2012-01-11 15:43:45

回答

12

你的方法是相當不錯的,但我認爲你在某種程度上超越了這個問題。讓我們退後一步。你有一個遞歸算法:

void MoveTowerConsole (int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    MoveTowerConsole (n - 1, start, temp, finish); 
    Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
    MoveTowerConsole (n - 1, temp, finish, start); 
    } 
} 

該算法的輸出是一堆控制檯輸出。假設您希望算法的輸出爲將要輸出到控制檯的字符串序列讓我們來看看這樣的方法是什麼樣子的。

首先,我們將重命名它。其次,它的返回類型不能爲空。它必須是IEnumerable<string>

IEnumerable<string> MoveTower(int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    MoveTower(n - 1, start, temp, finish); 
    Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
    MoveTower(n - 1, temp, finish, start); 
    } 
} 

這是正確的?不,我們沒有返回任何東西,我們仍然傾向於控制檯。 我們希望迭代器產生什麼?我們希望迭代產生:

  • 的種種舉動必要使第一遞歸步驟
  • 當前移動
  • 的種種舉動必要的第二遞歸步驟

所以我們修改產生這些算法的算法:

IEnumerable<string> MoveTower(int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    foreach(string move in MoveTower(n - 1, start, temp, finish)) 
     yield return move; 
    yield return string.Format("Moving disk from {0} to {1}", start, finish); 
    foreach(string move in MoveTower(n - 1, temp, finish, start)) 
     yield return move; 
    } 
} 

我們完成了!那樣容易。沒有必要定義一個整體類來將遞歸算法轉換爲遞歸枚舉器;讓編譯器爲你工作。

如果您想更換成枚舉「動作」的方法這一點,那麼這樣做:現在

IEnumerable<Move> MoveTower(int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    foreach(Move move in MoveTower(n - 1, start, temp, finish)) 
     yield return move; 
    yield return new Move(start, finish); 
    foreach(Move move in MoveTower(n - 1, temp, finish, start)) 
     yield return move; 
    } 
} 

,我會批評效率的基礎上,該代碼。通過以這種方式製作遞歸枚舉器,您正在做的是構建一個n個枚舉器鏈。當你需要下一個項目時,頂級枚舉器調用下一個枚舉器調用下一個枚舉器......直到底部,深度爲n。所以現在每一步實際上都需要n個步驟才能完成。出於這個原因,我傾向於解決這個問題而不遞歸。

練習:重寫上面的迭代器塊,以便它沒有遞歸在所有。您使用顯式堆棧的解決方案朝着正確的方向邁出了一步,但它仍然是遞歸的。你能適應它,以便不會遞歸嗎?

如果您在編寫實現IEnumerable<Move>那麼您可以在一個簡單的方式適應上面的代碼的類彎曲:

class MoveIterator : IEnumerable<Move> 
{ 
    public IEnumerator<Move> GetEnumerator() 
    { 
     foreach(Move move in MoveTower(whatever)) 
      yield return move; 
    } 

您可以使用收益的回報來實現,它返回一個枚舉的方法一個可枚舉的

+0

1+比輸入解決方案更快。 – 2012-01-11 16:11:15

+0

埃裏克你好, 非常感謝你提供你的解決方案的洞察力和一步一步的方式。這確實是以一種錯誤的方式來看待問題,這使我無法看清哪條路。非常有教育意義和信息! 我實際上已經有了一個使用兩個堆棧的非遞歸解決方案:請參閱下面的答案。 再次感謝! John Pool Amersfoort 荷蘭 – 2012-01-12 07:27:20

+0

...不幸的是,在我原來的問題中,刪除遞歸併不那麼直截了當。遞歸調用隱藏在多級條件語句中,將代碼轉換爲狀態引擎使其非常難以讀取。所以我認爲我必須接受嵌套的枚舉器解決方案 - 只要性能可以接受,這不會成爲問題。 – 2012-01-12 16:13:43

0

非遞歸版本:

// Non-recursive version -- state engine 
//rta.Push (State.Exit); 
//parameters.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
//MoveTower3(); 

enum State { Init, Call1, Call2, Rtrn, Exit } 

{ 
    ... 

    #region Non-recursive version -- state engine 
    static void MoveTower3() 
    { 
    State s = State.Init; 
    Move m = null; 

    while (true) 
     switch (s) 
     { 
     case State.Init: 
      m = moveStack.Pop(); 
      s = (m.n <= 0) ? State.Rtrn : State.Call1; 
      break; 
     case State.Call1: 
      rta.Push (State.Call2); // where do I want to go after the call is finished 
      moveStack.Push (m); // save state for second call 
      moveStack.Push (new Move (m.n-1, m.start, m.temp, m.finish)); // parameters 
      s = State.Init; 
      break; 
     case State.Call2: 
      m = moveStack.Pop(); // restore state from just before first call 
      Console.WriteLine (m); 
      rta.Push (State.Rtrn); 
      moveStack.Push (new Move (m.n-1, m.temp, m.finish, m.start)); 
      s = State.Init; 
      break; 
     case State.Rtrn: 
      s = rta.Pop(); 
      break; 
     case State.Exit: 
      return; 
     } 
    } 
    #endregion 

    #region Enumeration 
    static IEnumerable<Move> GetEnumerable (int n) 
    { 
    Stack<Move> moveStack = new Stack<Move>(); 
    Stack<State> rta = new Stack<State>(); // 'return addresses' 
    rta.Push (State.Exit); 
    moveStack.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
    State s = State.Init; 
    Move m = null; 

    while (true) 
     switch (s) 
     { 
     case State.Init: 
      m = moveStack.Pop(); 
      s = (m.n <= 0) ? State.Rtrn : State.Call1; 
      break; 
     case State.Call1: 
      rta.Push (State.Call2); // where do I want to go after the call is finished 
      moveStack.Push (m); // save state for second call 
      moveStack.Push (new Move (m.n-1, m.start, m.temp, m.finish)); // parameters 
      s = State.Init; 
      break; 
     case State.Call2: 
      m = moveStack.Pop(); // restore state from just before first call 
      yield return m; 
      rta.Push (State.Rtrn); 
      moveStack.Push (new Move (m.n-1, m.temp, m.finish, m.start)); 
      s = State.Init; 
      break; 
     case State.Rtrn: 
      s = rta.Pop(); 
      break; 
     case State.Exit: 
      yield break; 
     } 
    } 
    #endregion 
} 
1

你的非遞歸解決方案是好的 - 建立一個下推自動機(狀態機用棧,本質上)是建設的迭代版本的標準技術遞歸解決方案。事實上,這與我們如何爲迭代器和異步塊生成代碼非常相似。

但是在這種特殊情況下,您不需要使用開關和當前狀態來拉出下推式自動機的重型機械。你可能只是這樣做:

IEnumerable<Move> MoveTowerConsole (int size, Needle start, Needle finish, Needle temp) 
{ 
    if (size <= 0) yield break; 
    var stack = new Stack<Work>(); 
    stack.Push(new Work(size, start, finish, temp)); 
    while(stack.Count > 0) 
    { 
    var current = stack.Pop(); 
    if (current.Size == 1) 
     yield return new Move(current.Start, current.Finish); 
    else 
    { 
     // Push the work in the *opposite* order that it needs to be done. 
     stack.Push(new Work(current.Size - 1, current.Temp, current.Finish, current.Start)); 
     stack.Push(new Work(1, current.Start, current.Finish, current.Temp)); 
     stack.Push(new Work(current.Size - 1, current.Start, current.Temp, current.Finish)); 

    } 
} 

你已經知道什麼工作,你需要在當前遞歸步驟後要幹什麼,所以沒有必要蹦蹦跳跳開關把工作的三位堆棧。只需排隊全部立即完成一個給定的步驟。

+0

你好埃裏克,謝謝你的四個非遞歸解決方案!我必須承認它對我來說有點像魔術。我對這個大想法有個模糊的概念:當你從隊列中選擇一個不平凡的大小(即大於1)的「作品」時,將它分成三部分 - 分別小於原始部分,否則算法將不會終止 - 並且將這些算法推回到隊列中... – 2012-01-13 14:19:30

+0

...每次遇到一個小塊時,都會丟棄它,然後重複此操作,直到隊列中沒有任何內容。 Work類只是所有局部變量的容器,當使用遞歸時,它們通常會在堆棧上運行。這是看待它的正確方法嗎?這種方式是否可以重寫*任何遞歸函數?你有更多的例子,或對這個問題的文章的參考? 謝謝 - 約翰 – 2012-01-13 14:21:09

+0

@JohnPool:首先,我的解決方案是錯誤的 - 我應該使用堆棧,而不是隊列。但是,是的,這是基本的想法。並非每個遞歸函數都可以像這樣輕鬆地重寫。 – 2012-01-13 14:50:22